Compare commits

..
Author SHA1 Message Date
Evan MattsonandGitHub 0b69d7fd15 Python: Bump Python package versions for 1.2.0 release (#5468)
* Bump Python package versions for 1.2.0 release

Released tier bumps 1.1.1 -> 1.2.0 (core, openai, foundry, root) to
reflect additive public APIs landed since 1.1.0: functional workflow API
(#4238) and FunctionTool SKIP_PARSING sentinel (#5424). All beta packages
stamped 1.0.0b260424, alpha packages 1.0.0a260424. All 26 non-core
agent-framework-core floors raised to >=1.2.0,<2. CHANGELOG consolidates
the never-tagged 1.1.1 entries with the post-merge additions into [1.2.0].

* Update CHANGELOG footer links for 1.2.0

Advance [Unreleased] comparison base from python-1.1.0 to python-1.2.0
and add a [1.2.0] reference link comparing python-1.1.0...python-1.2.0
so the heading links resolve correctly.

* Fix CHANGELOG: restore [1.1.1] section and add proper [1.2.0]

Previous commit incorrectly renamed the [1.1.1] header to [1.2.0], which
wiped the historical 1.1.1 entries and wrongly attributed them to 1.2.0.
This restores [1.1.1] to its origin/main content and adds a new [1.2.0]
section above containing only the commits in python-1.1.1..HEAD:

- #4238 functional workflow API
- #5142 GitHub Copilot OpenTelemetry
- #2403 A2A bridge support
- #5070 oauth_consent_request events in Foundry clients
- #5447 FoundryAgent hosted agent sessions
- #5459 hosting server dependency upgrade + types
- #5389 AG-UI reasoning/multimodal parsing fix
- #5440 stop [TOOLBOXES] warning spam
- #5455 user agent prefix fix

Also corrects the [1.2.0] compare base to python-1.1.1 (not 1.1.0) and
adds the missing [1.1.1] reference link.
2026-04-24 19:54:59 +09:00
7b70f80036 Python: Surface oauth_consent_request events from Responses API in Foundry clients (#5070)
* Fix Foundry clients not surfacing oauth_consent_request events (#5054)

Override _parse_chunk_from_openai in both RawFoundryChatClient and
RawFoundryAgentChatClient to intercept response.output_item.added
events with item.type == 'oauth_consent_request'. The consent link
is validated (HTTPS required) and converted to
Content.from_oauth_consent_request, which the AG-UI layer already
knows how to emit as a CUSTOM event.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review feedback for #5054 OAuth consent parsing

- Extract shared helper (try_parse_oauth_consent_event) to avoid
  duplicated logic between RawFoundryChatClient and
  RawFoundryAgentChatClient
- Use urllib.parse.urlparse() for HTTPS validation instead of
  case-sensitive startswith check
- Sanitize log messages to avoid leaking consent_link tokens;
  log only item id
- Add model=self.model to ChatResponseUpdate to match parent behavior
- Add assertions on role, raw_representation, and model in happy-path
  tests
- Add test for empty-string consent_link
- Add test verifying non-oauth events delegate to super()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Handle response.oauth_consent_requested top-level event (#5054)

Add support for the top-level response.oauth_consent_requested stream
event in addition to the response.output_item.added variant. The
service may emit either form; handle both so the consent link is
reliably surfaced.

Extract _validate_consent_link helper within _oauth_helpers.py to
reduce nesting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5054: Python: [Bug]: `FoundryAgent` (Responses API) Does Not Surface `oauth_consent_request` as a CUSTOM AG-UI Event

* Address review feedback: defensive getattr and dedicated helper tests (#5054)

- Use getattr(event, 'type', None) in try_parse_oauth_consent_event
  for defensive access against malformed events without a type attribute
- Add test_oauth_helpers.py with unit tests for _validate_consent_link
  and try_parse_oauth_consent_event covering edge cases:
  - HTTPS URL with empty netloc (https:///path)
  - Warning log messages for rejected consent links
  - Event objects missing 'type' attribute

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #5054: Python: [Bug]: `FoundryAgent` (Responses API) Does Not Surface `oauth_consent_request` as a CUSTOM AG-UI Event

* Fix mypy: match _parse_chunk_from_openai signature with superclass

Add seen_reasoning_delta_item_ids parameter to _parse_chunk_from_openai
overrides in both RawFoundryChatClient and RawFoundryAgentChatClient to
match the updated superclass signature on main. Update super() calls and
test assertions accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <evan.mattson@microsoft.com>
2026-04-24 09:59:14 +00:00
da32e8cf80 Python: (core): Add functional workflow API (#4238)
* Add functional workflow api

* cleanup

* More cleanup

* address copilot feedback

* Address PR feedbacK

* updates

* PR feedback

* Address review comments on functional workflow samples

- Swap 05/06 get-started samples: agent workflow first (motivates
  why workflows exist), simple text workflow second
- Rename text_pipeline → text_workflow, poem_pipeline → poem_workflow
- Add @step to agent workflow sample (05) to demonstrate caching
- Switch agent samples to AzureOpenAIResponsesClient with Foundry
- Remove .as_agent() from agent_integration.py to focus on the key
  difference between inline agent calls vs @step-cached calls
- Add commented-out Agent.run example in hitl_review.py
- Add clarifying comment in _functional.py that event streaming is
  buffered (not true per-token streaming)
- Add naive_group_chat.py functional sample: round-robin group chat
  as a plain Python loop
- Update READMEs to reflect new file names and group chat sample

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix pyright type errors

* Address PR review comments on functional workflow API

1. Allow request_info inside @step: Auto-inject RunContext into step
   functions that declare a RunContext parameter (by type or name 'ctx'),
   and expose get_run_context() for programmatic access.

2. Handle None responses: Log a warning when a response value is None,
   and document the behavior in request_info docstring.

3. Add executor_bypassed event type: Replace executor_invoked +
   executor_completed with a single executor_bypassed event when a step
   replays from cache, making cached vs live execution explicit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add regression tests for PR review comments on functional workflow API

The three review comments (request_info in @step, None response handling,
executor_bypassed event type) were already addressed in 7da7db4e. This
commit adds cross-cutting regression tests that exercise the interactions
between these features:

- HITL in step with caching: preceding step bypassed on resume
- Full checkpoint lifecycle with HITL step (interrupt -> resume -> restore)
- None response inside step-level request_info logs warning
- WorkflowInterrupted from step does not emit executor_failed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR #4238 review comments on functional workflow API

Comment 1 (request_info in @step): Already supported. Added comment in
StepWrapper.__call__ explaining why WorkflowInterrupted (BaseException)
safely bypasses the except Exception handler.

Comment 2 (None response): Added docstring to _get_response clarifying
the (found, value) return tuple semantics and None handling.

Comment 3 (bypass event type): executor_bypassed is already a dedicated
event type in WorkflowEventType. Updated comment at the bypass site to
make the deliberate event type choice explicit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add experimental API warnings to functional workflow module

Mark all public classes and decorators (workflow, step, RunContext,
FunctionalWorkflow, StepWrapper, FunctionalWorkflowAgent) as
experimental and subject to change or removal.

* Address PR #4238 review comments from @eavanvalkenburg

- RunContext docstring leads with purpose (opt-in handle for HITL,
  custom events, state) so readers importing it from the public surface
  understand its role before the mechanics (#2993513452).
- Rename `06_first_functional_workflow.py` to
  `06_functional_workflow_basics.py`; the previous filename was
  confusing since it followed `05_functional_workflow_with_agents.py`
  (#2993531979).
- Simplify `05_functional_workflow_with_agents.py` to call agents
  directly without a @step wrapper; the step-vs-no-step contrast lives
  in `03-workflows/functional/agent_integration.py`, keeping the
  get-started sample minimal (#2993525532).
- Switch functional samples to `FoundryChatClient` for consistency with
  the rest of 01-get-started and 03-workflows (follow-up on #2876988570).
- Use walrus in `hitl_review.py` final-state assertion (#2993572182).
- Add expected-output block to `basic_streaming_pipeline.py` (#2993557609).
- Clarify in `parallel_pipeline.py` that `@step` composes with
  `asyncio.gather` (#2993597282).
- `naive_group_chat.py` threads `list[Message]` between turns instead
  of stringifying the transcript, preserving role/authorship (#2993583231).

Drive-by: pre-commit hook sorts an unrelated import block in
`samples/04-hosting/foundry-hosted-agents/responses/02_local_tools/main.py`.

* Fix 10 functional-workflow API bugs from /ultrareview pass

- bug_001: `ctx.request_info()` without an explicit `request_id` now derives
  a deterministic `auto::<index>` id from the call-counter, so HITL resume
  works correctly on the documented default path.  A uuid was regenerated on
  every replay, making resume impossible.

- bug_002: `StepWrapper.__call__` no longer deepcopies arguments on the
  cache-hit replay branch.  The copy is only performed on the live-execution
  path (for the event log) and falls back to the original mapping if deepcopy
  fails, so steps whose args aren't deepcopyable (locks, sockets, sessions)
  can still resume from checkpoint.

- bug_007: `_set_responses` now prunes each resolved `request_id` from
  `_pending_requests`, and the cache-hit branch in `request_info` does the
  same.  Previously, answered requests were re-serialized into every
  subsequent checkpoint and the final checkpoint falsely claimed pending
  requests even after the workflow completed.

- bug_008: `_compute_signature_hash` now mixes the function's `co_code` and
  `co_names` into the checkpoint signature, so changes to the workflow body
  invalidate older checkpoints even when steps are accessed via module /
  class attributes (which `_discover_step_names` can't see statically).
  `RunContext._record_observed_step` records observed step names for
  diagnostics.

- bug_010: `FunctionalWorkflow.run()` docstring corrected — says "at least
  one of message/responses/checkpoint_id" and explicitly notes `responses`
  may be combined with `checkpoint_id` (the validator already allowed this).

- bug_013: `FunctionalWorkflowAgent` now surfaces `request_info` events as
  `FunctionApprovalRequestContent` items (mirroring graph `WorkflowAgent`),
  threads `responses=` and `checkpoint_id=` through to the underlying
  workflow, and exposes `pending_requests`.  Previously `.as_agent()`
  returned empty `AgentResponse` for HITL workflows — effectively unusable.

- bug_014: `FunctionalWorkflow` now clears `_last_message`,
  `_last_step_cache`, and `_last_pending_request_ids` on clean completion.
  `run()` validates that `responses=` keys intersect the currently-pending
  request set (or raises with a clear error) instead of silently replaying
  against stale singleton state from a prior run.

- bug_015: `FunctionalWorkflow.as_agent` signature now matches graph
  `Workflow.as_agent`: accepts `name`, `description`, `context_providers`,
  and `**kwargs`.  `FunctionalWorkflowAgent` stores the overrides.

- bug_017: `RunContext.set_state` raises `ValueError` for underscore-
  prefixed keys (the framework's `_step_cache` / `_original_message` keys
  would silently clobber user state on checkpoint save and user
  underscore-prefixed state was dropped on restore).  Docstring documents
  the reserved prefix.

- merged_bug_003: Workflow function arity is validated at decoration time.
  Multiple non-ctx parameters raise `ValueError` immediately (previously
  every arg past the first was silently dropped at call time).  Passing a
  non-None `message` to a ctx-only workflow raises `ValueError` instead of
  silently discarding the message.

Test coverage: +18 regression tests covering every fix.  Full workflow
suite now 766 passed, 1 skipped, 2 xfailed; full core suite 2338 passed.

* Deslop functional.py fix commit

- Remove dead instrumentation added in the prior commit that was never
  consumed: `RunContext._observed_step_names`,
  `RunContext._record_observed_step`, `FunctionalWorkflow._runtime_step_names`,
  and `FunctionalWorkflowAgent._extra_kwargs`.  The signature hash relies on
  `co_code` alone, which covers the attribute-access case without the
  collection-scaffolding.
- Trim over-explanatory comments that restated what the code does or what
  it no longer does.  Keep only the comments that answer "why" for the
  non-obvious bits (deterministic id contract, defensive deepcopy, stale
  replay guard).
- Compress the `_compute_signature_hash` and FunctionalWorkflow `__init__`
  block docstrings without losing the user-facing reasoning.

Net -49 lines.  Regression lock preserved (766 passed, 1 skipped, 2 xfailed).

* Fix functional workflow review feedback

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 09:41:20 +00:00
62e02da698 Python: update FoundryAgent for hosted agent sessions (#5447)
* fixes to FoundryAgent to connect to new hosted agents

Co-authored-by: Copilot <copilot@github.com>

* fix mypy

Co-authored-by: Copilot <copilot@github.com>

* Python: remove Foundry service session helpers

Remove the public hosted-agent service session CRUD helpers from FoundryAgent and drop the related feature-stage inventory entry.

Update the hosted-agent sample to create and delete service sessions directly through the preview AIProjectClient APIs, and tighten a few test harnesses surfaced by full workspace validation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix from merge

* fix hosted env detection

Co-authored-by: Copilot <copilot@github.com>

* reverted sample update

* fix tests and code

Co-authored-by: Copilot <copilot@github.com>

* remove aenter

* skipping some tests

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 09:25:03 +00:00
70 changed files with 5287 additions and 282 deletions
+21 -3
View File
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.2.0] - 2026-04-24
### Added
- **agent-framework-core**: Add functional workflow API ([#4238](https://github.com/microsoft/agent-framework/pull/4238))
- **agent-framework-core**, **agent-framework-github-copilot**: Add OpenTelemetry integration for `GitHubCopilotAgent` ([#5142](https://github.com/microsoft/agent-framework/pull/5142))
- **agent-framework-a2a**: Add Agent Framework to A2A bridge support ([#2403](https://github.com/microsoft/agent-framework/pull/2403))
- **agent-framework-foundry**: Surface `oauth_consent_request` events from Responses API in Foundry clients ([#5070](https://github.com/microsoft/agent-framework/pull/5070))
### Changed
- **agent-framework-core**, **agent-framework-foundry**: Update `FoundryAgent` for hosted agent sessions ([#5447](https://github.com/microsoft/agent-framework/pull/5447))
- **agent-framework-foundry-hosting**: Upgrade hosting server dependency and add more type support ([#5459](https://github.com/microsoft/agent-framework/pull/5459))
### Fixed
- **agent-framework-ag-ui**: Fix reasoning role and multimodal media parsing to follow specification ([#5389](https://github.com/microsoft/agent-framework/pull/5389))
- **agent-framework-foundry**: Stop emitting `[TOOLBOXES]` warning for every `FoundryChatClient` call ([#5440](https://github.com/microsoft/agent-framework/pull/5440))
- **agent-framework-anthropic**, **agent-framework-azure-ai-search**, **agent-framework-azure-cosmos**: Fix user agent prefix ([#5455](https://github.com/microsoft/agent-framework/pull/5455))
## [1.1.1] - 2026-04-23
### Added
@@ -26,8 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **agent-framework-openai**: Exclude null `file_id` from `input_image` payload to prevent schema 400 errors ([#5125](https://github.com/microsoft/agent-framework/pull/5125))
- **agent-framework-foundry**: Reconcile Toolbox hosted-tool payloads with the Responses API ([#5414](https://github.com/microsoft/agent-framework/pull/5414))
- **agent-framework-ag-ui**: Pass client `thread_id` as `session_id` when constructing `AgentSession` ([#5384](https://github.com/microsoft/agent-framework/pull/5384))
- **agent-framework-hyperlight**: Thread-confine `WasmSandbox` interactions via per-entry `ThreadPoolExecutor` to eliminate the PyO3 `unsendable` panic when touched from asyncio worker threads
([#5424](https://github.com/microsoft/agent-framework/pull/5424))
- **agent-framework-hyperlight**: Thread-confine `WasmSandbox` interactions via per-entry `ThreadPoolExecutor` to eliminate the PyO3 `unsendable` panic when touched from asyncio worker threads ([#5424](https://github.com/microsoft/agent-framework/pull/5424))
## [1.1.0] - 2026-04-21
@@ -961,7 +977,9 @@ Release candidate for **agent-framework-core** and **agent-framework-azure-ai**
For more information, see the [announcement blog post](https://devblogs.microsoft.com/foundry/introducing-microsoft-agent-framework-the-open-source-engine-for-agentic-ai-apps/).
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.1.0...HEAD
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.2.0...HEAD
[1.2.0]: https://github.com/microsoft/agent-framework/compare/python-1.1.1...python-1.2.0
[1.1.1]: https://github.com/microsoft/agent-framework/compare/python-1.1.0...python-1.1.1
[1.1.0]: https://github.com/microsoft/agent-framework/compare/python-1.0.1...python-1.1.0
[1.0.1]: https://github.com/microsoft/agent-framework/compare/python-1.0.0...python-1.0.1
[1.0.0]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc6...python-1.0.0
+2 -2
View File
@@ -4,7 +4,7 @@ description = "A2A integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"a2a-sdk>=0.3.5,<0.3.24",
]
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
version = "1.0.0b260423"
version = "1.0.0b260424"
description = "AG-UI protocol integration for Agent Framework"
readme = "README.md"
license-files = ["LICENSE"]
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"ag-ui-protocol>=0.1.16,<0.2",
"fastapi>=0.115.0,<0.133.1",
"uvicorn[standard]>=0.30.0,<0.42.0"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Anthropic integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"anthropic>=0.80.0,<0.80.1",
]
@@ -4,7 +4,7 @@ description = "Azure AI Search integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"azure-search-documents>=11.7.0b2,<11.7.0b3",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Azure Cosmos DB history provider integration for Microsoft Agent
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"azure-cosmos>=4.3.0,<5",
]
@@ -4,7 +4,7 @@ description = "Azure Functions integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"agent-framework-durabletask",
"azure-functions>=1.24.0,<2",
"azure-functions-durable>=1.3.1,<2",
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Amazon Bedrock integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"boto3>=1.35.0,<2.0.0",
"botocore>=1.35.0,<2.0.0",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "OpenAI ChatKit integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"openai-chatkit>=1.4.1,<2.0.0",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Claude Agent SDK integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"claude-agent-sdk>=0.1.36,<0.1.49",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Copilot Studio integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"microsoft-agents-copilotstudio-client>=0.3.1,<0.3.2",
]
@@ -213,6 +213,15 @@ from ._workflows._executor import (
handler,
)
from ._workflows._function_executor import FunctionExecutor, executor
from ._workflows._functional import (
FunctionalWorkflow,
FunctionalWorkflowAgent,
RunContext,
StepWrapper,
get_run_context,
step,
workflow,
)
from ._workflows._request_info_mixin import response_handler
from ._workflows._runner import Runner
from ._workflows._runner_context import (
@@ -332,6 +341,8 @@ __all__ = [
"FunctionMiddleware",
"FunctionMiddlewareTypes",
"FunctionTool",
"FunctionalWorkflow",
"FunctionalWorkflowAgent",
"GeneratedEmbeddings",
"GraphConnectivityError",
"HistoryProvider",
@@ -354,6 +365,7 @@ __all__ = [
"ResponseStream",
"Role",
"RoleLiteral",
"RunContext",
"Runner",
"RunnerContext",
"SecretString",
@@ -366,6 +378,7 @@ __all__ = [
"SkillScriptRunner",
"SkillsProvider",
"SlidingWindowStrategy",
"StepWrapper",
"SubWorkflowRequestMessage",
"SubWorkflowResponseMessage",
"SummarizationStrategy",
@@ -424,6 +437,7 @@ __all__ = [
"evaluator",
"executor",
"function_middleware",
"get_run_context",
"handler",
"included_messages",
"included_token_count",
@@ -439,6 +453,7 @@ __all__ = [
"register_state_type",
"resolve_agent_id",
"response_handler",
"step",
"tool",
"tool_call_args_match",
"tool_called_check",
@@ -447,4 +462,5 @@ __all__ = [
"validate_tool_mode",
"validate_tools",
"validate_workflow_graph",
"workflow",
]
@@ -48,6 +48,7 @@ class ExperimentalFeature(str, Enum):
EVALS = "EVALS"
FILE_HISTORY = "FILE_HISTORY"
FUNCTIONAL_WORKFLOWS = "FUNCTIONAL_WORKFLOWS"
SKILLS = "SKILLS"
TOOLBOXES = "TOOLBOXES"
@@ -2,6 +2,7 @@
from __future__ import annotations
import contextlib
import logging
import os
from typing import Any, Final
@@ -60,13 +61,12 @@ def _detect_hosted_environment() -> None:
global _hosted_env_detected
if _hosted_env_detected:
return
_hosted_env_detected = True
env_value = os.environ.get(_FOUNDRY_HOSTING_ENV_VAR)
if env_value is not None:
if (env_value := os.environ.get(_FOUNDRY_HOSTING_ENV_VAR)) is not None:
# Env var exists — trust its value and skip the fallback.
if env_value:
_add_user_agent_prefix(_HOSTED_USER_AGENT_PREFIX)
_hosted_env_detected = True
return
# Env var not set — fall back to AgentConfig as a second layer of defense.
@@ -78,13 +78,12 @@ def _detect_hosted_environment() -> None:
return
except (ModuleNotFoundError, ValueError):
return
try:
with contextlib.suppress(ImportError, AttributeError):
from azure.ai.agentserver.core import AgentConfig # pyright: ignore[reportMissingImports]
if AgentConfig.from_env().is_hosted:
_add_user_agent_prefix(_HOSTED_USER_AGENT_PREFIX)
except (ImportError, AttributeError):
pass
_hosted_env_detected = True
def get_user_agent() -> str:
@@ -120,6 +120,7 @@ WorkflowEventType = Literal[
"executor_invoked", # Executor handler was called (use .executor_id, .data)
"executor_completed", # Executor handler completed (use .executor_id, .data)
"executor_failed", # Executor handler raised error (use .executor_id, .details)
"executor_bypassed", # Executor skipped via cache hit during replay (use .executor_id, .data)
# Orchestration event types (use .data for typed payload)
"group_chat", # Group chat orchestrator events (use .data as GroupChatRequestSentEvent | GroupChatResponseReceivedEvent) # noqa: E501
"handoff_sent", # Handoff routing events (use .data as HandoffSentEvent)
@@ -148,6 +149,7 @@ class WorkflowEvent(Generic[DataT]):
- `WorkflowEvent.executor_invoked(executor_id)` - executor handler called
- `WorkflowEvent.executor_completed(executor_id)` - executor handler completed
- `WorkflowEvent.executor_failed(executor_id, details)` - executor handler failed
- `WorkflowEvent.executor_bypassed(executor_id)` - executor skipped via cache hit
The generic parameter DataT represents the type of the event's data payload:
- Lifecycle events: `WorkflowEvent[None]` (data is None)
@@ -318,6 +320,11 @@ class WorkflowEvent(Generic[DataT]):
"""Create an 'executor_failed' event when an executor handler raises an error."""
return WorkflowEvent("executor_failed", executor_id=executor_id, data=details, details=details)
@classmethod
def executor_bypassed(cls, executor_id: str, data: DataT | None = None) -> WorkflowEvent[DataT]:
"""Create an 'executor_bypassed' event when a step is skipped via cache hit during replay."""
return cls("executor_bypassed", executor_id=executor_id, data=data)
# ==========================================================================
# Property for type-safe access
# ==========================================================================
File diff suppressed because it is too large Load Diff
@@ -340,10 +340,10 @@ class Workflow(DictConvertible):
# Emit explicit start/status events to the stream
with _framework_event_origin():
started = WorkflowEvent.started()
yield started
yield started # noqa: RUF070
with _framework_event_origin():
in_progress = WorkflowEvent.status(WorkflowRunState.IN_PROGRESS)
yield in_progress
yield in_progress # noqa: RUF070
# Reset context for a new run if supported
if reset_context:
@@ -388,7 +388,7 @@ class Workflow(DictConvertible):
emitted_in_progress_pending = True
with _framework_event_origin():
pending_status = WorkflowEvent.status(WorkflowRunState.IN_PROGRESS_PENDING_REQUESTS)
yield pending_status
yield pending_status # noqa: RUF070
# Workflow runs until idle - emit final status based on whether requests are pending
if saw_request:
with _framework_event_origin():
@@ -409,10 +409,10 @@ class Workflow(DictConvertible):
details = WorkflowErrorDetails.from_exception(exc)
with _framework_event_origin():
failed_event = WorkflowEvent.failed(details)
yield failed_event
yield failed_event # noqa: RUF070
with _framework_event_origin():
failed_status = WorkflowEvent.status(WorkflowRunState.FAILED)
yield failed_status
yield failed_status # noqa: RUF070
span.add_event(
name=OtelAttr.WORKFLOW_ERROR,
attributes={
@@ -14,6 +14,7 @@ from typing import Any
_IMPORTS: dict[str, tuple[str, str]] = {
"AnthropicFoundryClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"FoundryAgent": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryAgentOptions": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryChatOptions": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryEmbeddingClient": ("agent_framework_foundry", "agent-framework-foundry"),
+1 -1
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.1.1"
version = "1.2.0"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -529,11 +529,12 @@ class TestFunctionExecutor:
assert "@handler on instance methods" in str(exc_info.value)
async def test_async_staticmethod_detection_behavior(self):
"""Document the behavior of asyncio.iscoroutinefunction with staticmethod descriptors.
"""Document the behavior of inspect.iscoroutinefunction with staticmethod descriptors.
This test explains why the unwrapping is necessary when decorators are stacked.
"""
import asyncio
import inspect
# When @staticmethod is applied, it creates a descriptor
async def my_async_func():
@@ -544,19 +545,19 @@ class TestFunctionExecutor:
static_wrapped = staticmethod(my_async_func)
# Direct check on descriptor object fails (this is the bug)
assert not asyncio.iscoroutinefunction(static_wrapped) # type: ignore[reportDeprecated]
assert not inspect.iscoroutinefunction(static_wrapped)
assert isinstance(static_wrapped, staticmethod)
# But unwrapping __func__ reveals the async function
unwrapped = static_wrapped.__func__
assert asyncio.iscoroutinefunction(unwrapped) # type: ignore[reportDeprecated]
assert inspect.iscoroutinefunction(unwrapped)
# When accessed via class attribute, Python's descriptor protocol
# automatically unwraps it, so it works:
class C:
async_static = static_wrapped
assert asyncio.iscoroutinefunction(C.async_static) # type: ignore[reportDeprecated] # Works via descriptor protocol
assert inspect.iscoroutinefunction(C.async_static) # Works via descriptor protocol
class TestExecutorExplicitTypes:
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Declarative specification support for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
"pyyaml>=6.0,<7.0",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Debug UI for Microsoft Agent Framework with OpenAI-compatible API
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"openai>=1.99.0,<3",
"opentelemetry-sdk>=1.39.0,<2",
"fastapi>=0.115.0,<0.133.1",
@@ -655,7 +655,13 @@ async def test_devui_streaming_renderer_memory_is_bounded(
)
try:
websocket_url = await _get_devtools_websocket_url(debug_port)
try:
websocket_url = await _get_devtools_websocket_url(debug_port)
except RuntimeError as exc:
return_code = browser_process.poll()
if return_code is not None:
pytest.skip(f"Chromium exited before DevTools became available (code {return_code}).")
pytest.skip(str(exc))
async with websocket_connect(websocket_url, max_size=None) as websocket:
client = _CDPClient(websocket)
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Durable Task integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"durabletask>=1.3.0,<2",
"durabletask-azuremanaged>=1.3.0,<2",
"python-dateutil>=2.8.0,<3",
@@ -2,7 +2,7 @@
import importlib.metadata
from ._agent import FoundryAgent, RawFoundryAgent, RawFoundryAgentChatClient
from ._agent import FoundryAgent, FoundryAgentOptions, RawFoundryAgent, RawFoundryAgentChatClient
from ._chat_client import FoundryChatClient, FoundryChatOptions, RawFoundryChatClient
from ._embedding_client import (
FoundryEmbeddingClient,
@@ -25,6 +25,7 @@ except importlib.metadata.PackageNotFoundError:
__all__ = [
"FoundryAgent",
"FoundryAgentOptions",
"FoundryChatClient",
"FoundryChatOptions",
"FoundryEmbeddingClient",
@@ -16,8 +16,10 @@ from typing import TYPE_CHECKING, Any, ClassVar, Generic, cast
from agent_framework import (
AgentMiddlewareLayer,
AgentSession,
ChatAndFunctionMiddlewareTypes,
ChatMiddlewareLayer,
ChatResponseUpdate,
ContextProvider,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
@@ -34,6 +36,8 @@ from azure.ai.projects.aio import AIProjectClient
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
from agent_framework_foundry._oauth_helpers import try_parse_oauth_consent_event
from ._tools import _sanitize_foundry_response_tool # pyright: ignore[reportPrivateUsage]
if sys.version_info >= (3, 13):
@@ -52,11 +56,13 @@ else:
if TYPE_CHECKING:
from agent_framework import (
Agent,
AgentRunInputs,
ChatAndFunctionMiddlewareTypes,
ContextProvider,
MiddlewareTypes,
ToolTypes,
)
from agent_framework._agents import _RunContext # pyright: ignore[reportPrivateUsage]
logger: logging.Logger = logging.getLogger("agent_framework.foundry")
@@ -81,14 +87,54 @@ class FoundryAgentSettings(TypedDict, total=False):
agent_version: str | None
class FoundryAgentOptions(OpenAIChatOptions, total=False):
"""Microsoft Foundry agent-specific chat options.
Extends ``OpenAIChatOptions`` with hosted-agent session configuration used by
``FoundryAgent`` / ``RawFoundryAgent``.
Keyword Args:
extra_body: Additional request body values sent to the Responses API.
isolation_key: Isolation key used when lazily creating a hosted-agent
session through ``project_client.beta.agents.create_session(...)``.
"""
extra_body: dict[str, Any]
isolation_key: str
FoundryAgentOptionsT = TypeVar(
"FoundryAgentOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="OpenAIChatOptions",
default="FoundryAgentOptions",
covariant=True,
)
def _merge_extra_body(extra_body: Any | None, *, additions: Mapping[str, Any] | None = None) -> dict[str, Any]:
"""Normalize and merge provider-specific extra_body values."""
if extra_body is None:
merged: dict[str, Any] = {}
elif isinstance(extra_body, Mapping):
merged = dict(cast(Mapping[str, Any], extra_body))
else:
raise TypeError(f"extra_body must be a mapping when provided, got {type(extra_body).__name__}.")
if additions:
merged.update(additions)
return merged
def _uses_foundry_agent_session(conversation_id: Any) -> bool:
"""Return whether a conversation_id should be treated as a Foundry agent session id."""
return (
isinstance(conversation_id, str)
and bool(conversation_id)
and not conversation_id.startswith("resp_")
and not conversation_id.startswith("conv_")
)
class RawFoundryAgentChatClient( # type: ignore[misc]
RawOpenAIChatClient[FoundryAgentOptionsT],
Generic[FoundryAgentOptionsT],
@@ -167,13 +213,15 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
)
resolved_endpoint = settings.get("project_endpoint")
self.agent_name = settings.get("agent_name")
self.agent_version = settings.get("agent_version")
agent_name_setting = settings.get("agent_name")
self.agent_version: str | None = settings.get("agent_version")
self.allow_preview = allow_preview or False
if not self.agent_name:
if not agent_name_setting:
raise ValueError(
"Agent name is required. Set via 'agent_name' parameter or 'FOUNDRY_AGENT_NAME' environment variable."
)
self.agent_name = agent_name_setting
# Create or use provided project client
self._should_close_client = False
@@ -197,11 +245,13 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
self.project_client = AIProjectClient(**project_client_kwargs)
self._should_close_client = True
# Get OpenAI client from project
async_client = self.project_client.get_openai_client()
openai_client_kwargs: dict[str, Any] = {}
if default_headers:
openai_client_kwargs["default_headers"] = dict(default_headers)
if allow_preview:
openai_client_kwargs["agent_name"] = self.agent_name
super().__init__(
async_client=async_client,
async_client=self.project_client.get_openai_client(**openai_client_kwargs),
default_headers=default_headers,
instruction_role=instruction_role,
compaction_strategy=compaction_strategy,
@@ -209,13 +259,6 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
additional_properties=additional_properties,
)
def _get_agent_reference(self) -> dict[str, str]:
"""Build the agent reference dict for the Responses API."""
ref: dict[str, str] = {"name": self.agent_name, "type": "agent_reference"} # type: ignore[dict-item]
if self.agent_version:
ref["version"] = self.agent_version
return ref
@override
def as_agent(
self,
@@ -270,7 +313,7 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
options: Mapping[str, Any],
**kwargs: Any,
) -> dict[str, Any]:
"""Prepare options for the Responses API, injecting agent reference and validating tools."""
"""Prepare options for the Responses API and validate client-side tools."""
# Validate tools — only FunctionTool allowed
tools = options.get("tools", [])
if tools:
@@ -292,18 +335,61 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
if "input" in run_options and isinstance(run_options["input"], list):
run_options["input"] = self._transform_input_for_azure_ai(cast(list[dict[str, Any]], run_options["input"]))
# Inject agent reference
run_options["extra_body"] = {"agent_reference": self._get_agent_reference()}
# Merge caller-supplied extra_body with any agent-specific request payload.
conversation_id = options.get("conversation_id")
extra_body = _merge_extra_body(run_options.pop("extra_body", None))
if _uses_foundry_agent_session(conversation_id):
run_options.pop("previous_response_id", None)
run_options.pop("conversation", None)
extra_body["agent_session_id"] = conversation_id
if extra_body:
run_options["extra_body"] = extra_body
run_options.pop("isolation_key", None)
# Strip tools from request body - Foundry API rejects requests with both
# agent_reference and tools present. FunctionTools are invoked client-side
# agent endpoint and tools present. FunctionTools are invoked client-side
# by the function invocation layer, not sent to the service.
run_options.pop("tools", None)
run_options.pop("tool_choice", None)
run_options.pop("parallel_tool_calls", None)
run_options.pop("model", None)
if not self.allow_preview:
run_options.pop("tools", None)
run_options.pop("tool_choice", None)
run_options.pop("parallel_tool_calls", None)
return run_options
@override
def _parse_response_from_openai(
self,
response: Any,
options: dict[str, Any],
) -> Any:
parsed_response = super()._parse_response_from_openai(response, options)
if _uses_foundry_agent_session(options.get("conversation_id")):
parsed_response.conversation_id = None
return parsed_response
@override
def _parse_chunk_from_openai(
self,
event: Any,
options: dict[str, Any],
function_call_ids: dict[int, tuple[str, str]],
seen_reasoning_delta_item_ids: set[str] | None = None,
) -> ChatResponseUpdate:
"""Parse streaming events while preserving hosted-agent session state."""
update = try_parse_oauth_consent_event(event, self.model)
if update is None:
update = super()._parse_chunk_from_openai(
event,
options,
function_call_ids,
seen_reasoning_delta_item_ids,
)
if _uses_foundry_agent_session(options.get("conversation_id")):
update.conversation_id = None
return update
@override
def _check_model_presence(self, options: dict[str, Any]) -> None:
"""Skip model check — model is configured on the Foundry agent."""
@@ -368,6 +454,26 @@ class RawFoundryAgentChatClient( # type: ignore[misc]
return transformed
async def get_agent_version(self) -> str | None:
"""Return the agent version if available, else None."""
if self.agent_version is not None:
return self.agent_version
if not self.allow_preview:
return None
agent_details = await cast(Any, self.project_client.beta.agents).get( # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
agent_name=self.agent_name
)
versions_object = getattr(agent_details, "versions", None)
if not isinstance(versions_object, Mapping):
raise TypeError("Foundry agent details did not include a versions mapping.")
versions = cast(Mapping[str, Any], versions_object)
latest_version = versions.get("latest")
agent_version = getattr(cast(Any, latest_version), "version", None)
if not isinstance(agent_version, str):
raise TypeError("Foundry agent details did not include a latest version string.")
self.agent_version = agent_version
return agent_version
async def close(self) -> None:
"""Close the project client if we created it."""
if self._should_close_client:
@@ -395,7 +501,7 @@ class _FoundryAgentChatClient( # type: ignore[misc]
client = FoundryAgentClient(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-prompt-agent",
agent_version="1.0",
agent_version="1",
credential=AzureCliCredential(),
)
@@ -477,7 +583,7 @@ class RawFoundryAgent( # type: ignore[misc]
agent = RawFoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-prompt-agent",
agent_version="1.0",
agent_version="1",
credential=AzureCliCredential(),
)
result = await agent.run("Hello!")
@@ -570,7 +676,7 @@ class RawFoundryAgent( # type: ignore[misc]
client=client, # type: ignore[arg-type]
instructions=instructions,
id=id,
name=name,
name=name or agent_name,
description=description,
tools=tools, # type: ignore[arg-type]
default_options=cast(FoundryAgentOptionsT | None, default_options),
@@ -582,6 +688,81 @@ class RawFoundryAgent( # type: ignore[misc]
additional_properties=dict(additional_properties) if additional_properties is not None else None,
)
def _resolve_service_session_isolation_key(self, isolation_key: str | None = None) -> str:
"""Resolve the isolation key from an explicit value or default_options."""
resolved_isolation_key = (
isolation_key if isolation_key is not None else self.default_options.get("isolation_key")
)
if resolved_isolation_key is None:
raise ValueError("isolation_key is required. Pass it explicitly or set default_options['isolation_key'].")
return resolved_isolation_key
async def _create_service_session_id(
self,
*,
isolation_key: str | None = None,
) -> str:
"""Create a hosted Foundry service session and return the service session ID."""
if not isinstance(self.client, RawFoundryAgentChatClient):
raise TypeError("_create_service_session_id requires a RawFoundryAgentChatClient-based client.")
if not self.client.allow_preview:
raise RuntimeError("Hosted Foundry service sessions require allow_preview=True.")
create_session_kwargs: dict[str, Any] = {
"agent_name": self.client.agent_name,
"isolation_key": self._resolve_service_session_isolation_key(isolation_key),
}
if version := await self.client.get_agent_version():
from azure.ai.projects.models import VersionRefIndicator
create_session_kwargs["version_indicator"] = VersionRefIndicator(agent_version=version) # type: ignore
service_session = await self.client.project_client.beta.agents.create_session(**create_session_kwargs)
agent_session_id = getattr(service_session, "agent_session_id", None)
if not isinstance(agent_session_id, str) or not agent_session_id:
raise ValueError("Hosted Foundry session creation did not return a non-empty agent_session_id.")
return agent_session_id
@override
async def _prepare_run_context(
self,
*,
messages: AgentRunInputs | None,
session: AgentSession | None,
tools: ToolTypes | Callable[..., Any] | Sequence[ToolTypes | Callable[..., Any]] | None,
options: Mapping[str, Any] | None,
compaction_strategy: CompactionStrategy | None,
tokenizer: TokenizerProtocol | None,
function_invocation_kwargs: Mapping[str, Any] | None,
client_kwargs: Mapping[str, Any] | None,
) -> _RunContext:
runtime_options = dict(options) if options else {}
effective_options = {
**{key: value for key, value in self.default_options.items() if value is not None},
**{key: value for key, value in runtime_options.items() if value is not None},
}
if (
session is not None
and session.service_session_id is None
and effective_options.get("isolation_key") is not None
):
session.service_session_id = await self._create_service_session_id(
isolation_key=cast(str | None, effective_options.get("isolation_key")),
)
return await super()._prepare_run_context(
messages=messages,
session=session,
tools=tools,
options=runtime_options,
compaction_strategy=compaction_strategy,
tokenizer=tokenizer,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
)
async def configure_azure_monitor(
self,
enable_sensitive_data: bool = False,
@@ -708,6 +889,19 @@ class FoundryAgent( # type: ignore[misc]
) -> None:
"""Initialize a Foundry Agent with full middleware and telemetry.
``FoundryAgent`` supports both PromptAgents and HostedAgents. PromptAgents
typically provide ``agent_version`` directly. HostedAgents can omit
``agent_version`` and, when they need preview-only session APIs, should
opt in with ``allow_preview=True`` when this class creates the underlying
``AIProjectClient``. If you pass ``project_client`` explicitly, it must
already be configured for preview APIs before being passed to
``FoundryAgent``.
To lazily create HostedAgent service sessions inside the agent, pass an
``isolation_key`` through ``default_options`` (or per-run options). The
agent stores the resulting HostedAgent session ID in
``AgentSession.service_session_id`` and reuses it on subsequent runs.
Keyword Args:
project_endpoint: The Foundry project endpoint URL.
agent_name: The name of the Foundry agent to connect to.
@@ -715,6 +909,9 @@ class FoundryAgent( # type: ignore[misc]
credential: Azure credential for authentication.
project_client: An existing AIProjectClient to use.
allow_preview: Enables preview opt-in on internally-created AIProjectClient.
Set this to ``True`` for HostedAgents that need preview-only
session APIs, including lazy service session creation from
``isolation_key``.
tools: Function tools to provide to the agent. Only ``FunctionTool`` objects are accepted.
context_providers: Optional context providers.
middleware: Optional agent-level middleware.
@@ -726,6 +923,8 @@ class FoundryAgent( # type: ignore[misc]
description: Optional local description for the local agent wrapper.
instructions: Optional instructions for the local agent wrapper.
default_options: Default chat options for the local agent wrapper.
``FoundryAgentOptions`` can include ``isolation_key`` and
``extra_body`` when working with HostedAgents.
require_per_service_call_history_persistence: Whether to require per-service-call
chat history persistence when using local history providers.
function_invocation_configuration: Optional function invocation configuration override.
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal
from agent_framework import (
ChatMiddlewareLayer,
ChatResponseUpdate,
Content,
FunctionInvocationConfiguration,
FunctionInvocationLayer,
@@ -33,6 +34,8 @@ from azure.ai.projects.models import MCPTool as FoundryMCPTool
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
from agent_framework_foundry._oauth_helpers import try_parse_oauth_consent_event
from ._tools import _sanitize_foundry_response_tool, fetch_toolbox # pyright: ignore[reportPrivateUsage]
if sys.version_info >= (3, 13):
@@ -204,9 +207,13 @@ class RawFoundryChatClient( # type: ignore[misc]
project_client_kwargs["allow_preview"] = allow_preview
project_client = AIProjectClient(**project_client_kwargs)
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(),
async_client=project_client.get_openai_client(**openai_kwargs),
default_headers=default_headers,
instruction_role=instruction_role,
compaction_strategy=compaction_strategy,
@@ -237,6 +244,20 @@ class RawFoundryChatClient( # type: ignore[misc]
response_tools = super()._prepare_tools_for_openai(tools)
return [_sanitize_foundry_response_tool(tool_item) for tool_item in response_tools]
@override
def _parse_chunk_from_openai(
self,
event: Any,
options: dict[str, Any],
function_call_ids: dict[int, tuple[str, str]],
seen_reasoning_delta_item_ids: set[str] | None = None,
) -> ChatResponseUpdate:
"""Parse streaming event, intercepting oauth_consent_request items."""
update = try_parse_oauth_consent_event(event, self.model)
if update is not None:
return update
return super()._parse_chunk_from_openai(event, options, function_call_ids, seen_reasoning_delta_item_ids)
async def configure_azure_monitor(
self,
enable_sensitive_data: bool = False,
@@ -0,0 +1,75 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
from typing import Any
from urllib.parse import urlparse
from agent_framework import ChatResponseUpdate, Content
logger = logging.getLogger(__name__)
def _validate_consent_link(consent_link: str, item_id: str) -> str:
"""Validate a consent link is HTTPS with a valid netloc.
Returns the link unchanged if valid, or an empty string if not.
"""
parsed = urlparse(consent_link)
if parsed.scheme.lower() != "https" or not parsed.netloc:
logger.warning(
"Skipping oauth_consent_request with non-HTTPS consent_link (item id=%s)",
item_id,
)
return ""
return consent_link
def try_parse_oauth_consent_event(event: Any, model: str) -> ChatResponseUpdate | None:
"""Parse an oauth_consent_request from a streaming event, if present.
Returns a ``ChatResponseUpdate`` when *event* is a
``response.output_item.added`` carrying an ``oauth_consent_request`` item
or a top-level ``response.oauth_consent_requested`` event,
or ``None`` so the caller can fall through to the base implementation.
"""
consent_link: str = ""
raw_item: Any = None
event_type = getattr(event, "type", None)
if event_type == "response.output_item.added" and getattr(event.item, "type", None) == "oauth_consent_request":
raw_item = event.item
consent_link = getattr(raw_item, "consent_link", None) or ""
elif event_type == "response.oauth_consent_requested":
raw_item = event
consent_link = getattr(event, "consent_link", None) or ""
else:
return None
item_id = getattr(raw_item, "id", "<unknown>")
if consent_link:
consent_link = _validate_consent_link(consent_link, item_id)
contents: list[Content] = []
if consent_link:
contents.append(
Content.from_oauth_consent_request(
consent_link=consent_link,
raw_representation=raw_item,
)
)
else:
logger.warning(
"Received oauth_consent_request output without valid consent_link (item id=%s)",
item_id,
)
return ChatResponseUpdate(
contents=contents,
role="assistant",
model=model,
raw_representation=event,
)
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Foundry integrations for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.1.1"
version = "1.2.0"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"agent-framework-openai>=1.1.0,<2",
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
"azure-ai-projects>=2.1.0,<3.0",
@@ -5,11 +5,22 @@ from __future__ import annotations
import inspect
import os
import sys
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import AgentResponse, ChatContext, ChatMiddleware, Message, tool
from agent_framework import (
AgentResponse,
AgentSession,
ChatContext,
ChatMiddleware,
ChatResponse,
ChatResponseUpdate,
Message,
tool,
)
from agent_framework_openai._chat_client import RawOpenAIChatClient
from azure.core.exceptions import ResourceNotFoundError
from azure.identity import AzureCliCredential
@@ -54,7 +65,7 @@ def test_raw_foundry_agent_chat_client_init_requires_agent_name() -> None:
def test_raw_foundry_agent_chat_client_init_with_agent_name() -> None:
"""Test construction with agent_name and project_client."""
"""Test construction with agent_name and project_client without preview agent binding."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
@@ -67,6 +78,27 @@ def test_raw_foundry_agent_chat_client_init_with_agent_name() -> None:
assert client.agent_name == "test-agent"
assert client.agent_version == "1.0"
mock_project.get_openai_client.assert_called_once_with()
def test_raw_foundry_agent_chat_client_init_passes_agent_name_when_preview_enabled() -> None:
"""Test preview-enabled clients bind the OpenAI client to the agent endpoint."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="hosted-agent",
allow_preview=True,
default_headers={"x-test": "1"},
)
assert client.agent_name == "hosted-agent"
mock_project.get_openai_client.assert_called_once_with(
agent_name="hosted-agent",
default_headers={"x-test": "1"},
)
def test_raw_foundry_agent_chat_client_init_uses_explicit_parameters() -> None:
@@ -80,38 +112,6 @@ def test_raw_foundry_agent_chat_client_init_uses_explicit_parameters() -> None:
assert all(parameter.kind != inspect.Parameter.VAR_KEYWORD for parameter in signature.parameters.values())
def test_raw_foundry_agent_chat_client_get_agent_reference_with_version() -> None:
"""Test agent reference includes version when provided."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="my-agent",
agent_version="2.0",
)
ref = client._get_agent_reference()
assert ref == {"name": "my-agent", "version": "2.0", "type": "agent_reference"}
def test_raw_foundry_agent_chat_client_get_agent_reference_without_version() -> None:
"""Test agent reference omits version for HostedAgents."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="hosted-agent",
)
ref = client._get_agent_reference()
assert ref == {"name": "hosted-agent", "type": "agent_reference"}
assert "version" not in ref
def test_raw_foundry_agent_chat_client_as_agent_preserves_client_type() -> None:
"""Test that as_agent() wraps the client in FoundryAgent using the same client class."""
@@ -196,12 +196,11 @@ async def test_raw_foundry_agent_chat_client_prepare_options_accepts_function_to
options={"tools": [my_func]},
)
assert "extra_body" in result
assert result["extra_body"]["agent_reference"]["name"] == "test-agent"
assert result == {}
async def test_raw_foundry_agent_chat_client_prepare_options_strips_tools() -> None:
"""Test that _prepare_options strips tools, tool_choice, and parallel_tool_calls from run_options."""
async def test_raw_foundry_agent_chat_client_prepare_options_strips_client_side_fields() -> None:
"""Test that _prepare_options strips model and tool-loop fields from run_options."""
mock_project = MagicMock()
mock_openai = MagicMock()
@@ -222,6 +221,7 @@ async def test_raw_foundry_agent_chat_client_prepare_options_strips_tools() -> N
"agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options",
new_callable=AsyncMock,
return_value={
"model": "gpt-4.1",
"tools": [{"type": "function", "function": {"name": "my_func"}}],
"tool_choice": "auto",
"parallel_tool_calls": True,
@@ -232,11 +232,94 @@ async def test_raw_foundry_agent_chat_client_prepare_options_strips_tools() -> N
options={"tools": [my_func]},
)
assert "model" not in result
assert "tools" not in result
assert "tool_choice" not in result
assert "parallel_tool_calls" not in result
assert "extra_body" in result
assert result["extra_body"]["agent_reference"]["name"] == "test-agent"
assert result == {}
async def test_raw_foundry_agent_chat_client_prepare_options_maps_agent_session_id_to_extra_body() -> None:
"""Test that service_session_id is forwarded as agent_session_id for hosted sessions."""
mock_project = MagicMock()
mock_openai = MagicMock()
mock_project.get_openai_client.return_value = mock_openai
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
with patch(
"agent_framework_openai._chat_client.RawOpenAIChatClient._prepare_options",
new_callable=AsyncMock,
return_value={
"extra_body": {"custom": "value"},
"previous_response_id": "should-be-removed",
},
):
result = await client._prepare_options(
messages=[Message(role="user", contents="hi")],
options={"conversation_id": "agent-session-123", "isolation_key": "iso-key"},
)
assert result["extra_body"] == {
"custom": "value",
"agent_session_id": "agent-session-123",
}
assert "previous_response_id" not in result
assert "conversation" not in result
assert "isolation_key" not in result
def test_raw_foundry_agent_chat_client_parse_response_suppresses_conversation_id_for_agent_sessions() -> None:
"""Test that agent-session continuations do not overwrite session.service_session_id."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
parsed = ChatResponse(conversation_id="resp_123")
with patch(
"agent_framework_openai._chat_client.RawOpenAIChatClient._parse_response_from_openai",
return_value=parsed,
):
result = client._parse_response_from_openai(
response=MagicMock(),
options={"conversation_id": "agent-session-123"},
)
assert result.conversation_id is None
def test_raw_foundry_agent_chat_client_parse_chunk_suppresses_conversation_id_for_agent_sessions() -> None:
"""Test that agent-session stream updates do not overwrite session.service_session_id."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
parsed = ChatResponseUpdate(conversation_id="resp_123")
with patch(
"agent_framework_openai._chat_client.RawOpenAIChatClient._parse_chunk_from_openai",
return_value=parsed,
):
result = client._parse_chunk_from_openai(
event=MagicMock(type="response.output_text.delta"),
options={"conversation_id": "agent-session-123"},
function_call_ids={},
)
assert result.conversation_id is None
def test_raw_foundry_agent_chat_client_check_model_presence_is_noop() -> None:
@@ -366,6 +449,74 @@ def test_raw_foundry_agent_init_with_function_tools() -> None:
assert agent.default_options.get("tools") is not None
async def test_raw_foundry_agent_prepare_run_context_creates_service_session_from_isolation_key() -> None:
"""Test that RawFoundryAgent lazily creates a hosted session and stores it on service_session_id."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
mock_project.beta = SimpleNamespace(
agents=SimpleNamespace(
create_session=AsyncMock(return_value=SimpleNamespace(agent_session_id="agent-session-123"))
)
)
agent = RawFoundryAgent(
project_client=mock_project,
agent_name="test-agent",
agent_version="1.0",
allow_preview=True,
)
session = AgentSession()
with patch(
"agent_framework._agents.RawAgent._prepare_run_context",
new=AsyncMock(return_value={"ok": True}),
) as mock_prepare_run_context:
result = await agent._prepare_run_context(
messages="hi",
session=session,
tools=None,
options={"isolation_key": "iso-key"},
compaction_strategy=None,
tokenizer=None,
function_invocation_kwargs=None,
client_kwargs=None,
)
assert result == {"ok": True}
assert session.service_session_id == "agent-session-123"
mock_project.beta.agents.create_session.assert_awaited_once()
create_session_kwargs = mock_project.beta.agents.create_session.await_args.kwargs
assert create_session_kwargs["agent_name"] == "test-agent"
assert create_session_kwargs["isolation_key"] == "iso-key"
assert "version_indicator" in create_session_kwargs
mock_prepare_run_context.assert_awaited_once()
async def test_raw_foundry_agent_prepare_run_context_requires_preview_for_hosted_sessions() -> None:
"""Test that hosted-agent sessions require allow_preview=True."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
agent = RawFoundryAgent(
project_client=mock_project,
agent_name="test-agent",
)
with pytest.raises(RuntimeError, match="allow_preview=True"):
await agent._prepare_run_context(
messages="hi",
session=AgentSession(),
tools=None,
options={"isolation_key": "iso-key"},
compaction_strategy=None,
tokenizer=None,
function_invocation_kwargs=None,
client_kwargs=None,
)
def test_foundry_agent_init() -> None:
"""Test construction of the full-middleware agent."""
@@ -483,9 +634,10 @@ async def test_foundry_agent_configure_azure_monitor_import_error() -> None:
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_foundry_agent_integration_tests_disabled
@pytest.mark.skip(reason="Test agent seems to have disappeared from the test environment; needs investigation.")
async def test_foundry_agent_basic_run() -> None:
"""Smoke-test FoundryAgent against a real configured agent."""
async with FoundryAgent(credential=AzureCliCredential()) as agent:
async with FoundryAgent(credential=AzureCliCredential(), allow_preview=True) as agent:
response = await agent.run("Please respond with exactly: 'This is a response test.'")
assert isinstance(response, AgentResponse)
@@ -496,6 +648,7 @@ async def test_foundry_agent_basic_run() -> None:
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_foundry_agent_integration_tests_disabled
@pytest.mark.skip(reason="Test agent seems to have disappeared from the test environment; needs investigation.")
async def test_foundry_agent_custom_client_run() -> None:
"""Smoke-test FoundryAgent against a real configured agent."""
async with FoundryAgent(credential=AzureCliCredential(), client_type=RawFoundryAgentChatClient) as agent:
@@ -504,3 +657,158 @@ async def test_foundry_agent_custom_client_run() -> None:
assert isinstance(response, AgentResponse)
assert response.text is not None
assert "response test" in response.text.lower()
def test_parse_chunk_surfaces_oauth_consent_request() -> None:
"""An oauth_consent_request output item surfaces as Content with consent_link."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = "https://consent-host.example.com/login?data=abc123"
mock_item.id = "oauth-item-1"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 1
assert consent_contents[0].consent_link == "https://consent-host.example.com/login?data=abc123"
assert update.role == "assistant"
assert update.raw_representation is mock_event
def test_parse_chunk_skips_non_https_oauth_consent() -> None:
"""An oauth_consent_request with a non-HTTPS link is rejected."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = "http://insecure.example.com/login"
mock_item.id = "oauth-item-2"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 0
def test_parse_chunk_handles_missing_consent_link() -> None:
"""An oauth_consent_request without a consent_link produces no content."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = None
mock_item.id = "oauth-item-3"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 0
def test_parse_chunk_handles_empty_string_consent_link() -> None:
"""An oauth_consent_request with empty-string consent_link produces no content."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = ""
mock_item.id = "oauth-item-4"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 0
def test_parse_chunk_delegates_non_oauth_events_to_super() -> None:
"""Non-oauth events are delegated to super()._parse_chunk_from_openai()."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
mock_event = MagicMock()
mock_event.type = "response.output_text.delta"
with patch.object(
RawOpenAIChatClient,
"_parse_chunk_from_openai",
return_value=MagicMock(),
) as mock_super:
client._parse_chunk_from_openai(mock_event, {}, {})
mock_super.assert_called_once_with(mock_event, {}, {}, None)
def test_parse_chunk_surfaces_oauth_consent_requested_event() -> None:
"""A top-level response.oauth_consent_requested event surfaces as Content."""
mock_project = MagicMock()
mock_project.get_openai_client.return_value = MagicMock()
client = RawFoundryAgentChatClient(
project_client=mock_project,
agent_name="test-agent",
)
mock_event = MagicMock()
mock_event.type = "response.oauth_consent_requested"
mock_event.consent_link = "https://consent-host.example.com/authorize?code=xyz"
mock_event.id = "consent-event-1"
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 1
assert consent_contents[0].consent_link == "https://consent-host.example.com/authorize?code=xyz"
assert update.role == "assistant"
assert update.raw_representation is mock_event
@@ -15,6 +15,7 @@ from agent_framework import ChatResponse, Content, Message, SupportsChatGetRespo
from agent_framework._telemetry import get_user_agent
from agent_framework.exceptions import ChatClientException, ChatClientInvalidRequestException
from agent_framework_openai import OpenAIContentFilterException
from agent_framework_openai._chat_client import RawOpenAIChatClient
from azure.ai.projects.models import MCPTool as FoundryMCPTool
from azure.core.exceptions import ResourceNotFoundError
from azure.identity import AzureCliCredential
@@ -993,3 +994,165 @@ def test_get_mcp_tool_with_connection_id() -> None:
description="GitHub MCP via Foundry",
)
assert tool_obj is not None
def test_parse_chunk_surfaces_oauth_consent_request() -> None:
"""An oauth_consent_request output item surfaces as Content with consent_link."""
mock_project = MagicMock()
mock_openai = _make_mock_openai_client()
mock_project.get_openai_client.return_value = mock_openai
client = RawFoundryChatClient(
project_client=mock_project,
model="test-model",
)
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = "https://consent-host.example.com/login?data=abc123"
mock_item.id = "oauth-item-1"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 1
assert consent_contents[0].consent_link == "https://consent-host.example.com/login?data=abc123"
assert update.role == "assistant"
assert update.raw_representation is mock_event
assert update.model == "test-model"
def test_parse_chunk_skips_non_https_oauth_consent() -> None:
"""An oauth_consent_request with a non-HTTPS link is rejected."""
mock_project = MagicMock()
mock_openai = _make_mock_openai_client()
mock_project.get_openai_client.return_value = mock_openai
client = RawFoundryChatClient(
project_client=mock_project,
model="test-model",
)
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = "http://insecure.example.com/login"
mock_item.id = "oauth-item-2"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 0
def test_parse_chunk_handles_missing_consent_link() -> None:
"""An oauth_consent_request without a consent_link produces no content."""
mock_project = MagicMock()
mock_openai = _make_mock_openai_client()
mock_project.get_openai_client.return_value = mock_openai
client = RawFoundryChatClient(
project_client=mock_project,
model="test-model",
)
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = None
mock_item.id = "oauth-item-3"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 0
def test_parse_chunk_handles_empty_string_consent_link() -> None:
"""An oauth_consent_request with empty-string consent_link produces no content."""
mock_project = MagicMock()
mock_openai = _make_mock_openai_client()
mock_project.get_openai_client.return_value = mock_openai
client = RawFoundryChatClient(
project_client=mock_project,
model="test-model",
)
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "oauth_consent_request"
mock_item.consent_link = ""
mock_item.id = "oauth-item-4"
mock_event.item = mock_item
mock_event.output_index = 0
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 0
def test_parse_chunk_delegates_non_oauth_events_to_super() -> None:
"""Non-oauth events are delegated to super()._parse_chunk_from_openai()."""
mock_project = MagicMock()
mock_openai = _make_mock_openai_client()
mock_project.get_openai_client.return_value = mock_openai
client = RawFoundryChatClient(
project_client=mock_project,
model="test-model",
)
mock_event = MagicMock()
mock_event.type = "response.output_text.delta"
with patch.object(
RawOpenAIChatClient,
"_parse_chunk_from_openai",
return_value=MagicMock(),
) as mock_super:
client._parse_chunk_from_openai(mock_event, {}, {})
mock_super.assert_called_once_with(mock_event, {}, {}, None)
def test_parse_chunk_surfaces_oauth_consent_requested_event() -> None:
"""A top-level response.oauth_consent_requested event surfaces as Content."""
mock_project = MagicMock()
mock_openai = _make_mock_openai_client()
mock_project.get_openai_client.return_value = mock_openai
client = RawFoundryChatClient(
project_client=mock_project,
model="test-model",
)
mock_event = MagicMock()
mock_event.type = "response.oauth_consent_requested"
mock_event.consent_link = "https://consent-host.example.com/authorize?code=xyz"
mock_event.id = "consent-event-1"
update = client._parse_chunk_from_openai(mock_event, {}, {})
consent_contents = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent_contents) == 1
assert consent_contents[0].consent_link == "https://consent-host.example.com/authorize?code=xyz"
assert update.role == "assistant"
assert update.raw_representation is mock_event
@@ -198,6 +198,7 @@ class TestRawFoundryEmbeddingClient:
"FOUNDRY_MODELS_API_KEY": "env-key",
"FOUNDRY_EMBEDDING_MODEL": "env-model",
},
clear=True,
),
patch("agent_framework_foundry._embedding_client.EmbeddingsClient"),
patch("agent_framework_foundry._embedding_client.ImageEmbeddingsClient"),
@@ -0,0 +1,164 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
from typing import Any
from unittest.mock import MagicMock
import pytest
from agent_framework_foundry._oauth_helpers import _validate_consent_link, try_parse_oauth_consent_event
# region _validate_consent_link tests
def test_validate_consent_link_accepts_valid_https() -> None:
"""A valid HTTPS URL with a netloc passes validation."""
link = "https://consent.example.com/auth?code=123"
assert _validate_consent_link(link, "item-1") == link
def test_validate_consent_link_rejects_http(caplog: pytest.LogCaptureFixture) -> None:
"""An HTTP link is rejected and a warning is logged."""
with caplog.at_level(logging.WARNING):
result = _validate_consent_link("http://insecure.example.com/login", "item-2")
assert result == ""
assert "non-HTTPS" in caplog.text
assert "item-2" in caplog.text
def test_validate_consent_link_rejects_empty_netloc(caplog: pytest.LogCaptureFixture) -> None:
"""An HTTPS URL with an empty netloc (e.g. https:///path) is rejected."""
with caplog.at_level(logging.WARNING):
result = _validate_consent_link("https:///path", "item-3")
assert result == ""
assert "non-HTTPS" in caplog.text
assert "item-3" in caplog.text
def test_validate_consent_link_rejects_non_url(caplog: pytest.LogCaptureFixture) -> None:
"""A non-URL string is rejected."""
with caplog.at_level(logging.WARNING):
result = _validate_consent_link("not-a-url", "item-4")
assert result == ""
# endregion
# region try_parse_oauth_consent_event tests
def _make_output_item_event(
*,
item_type: str = "oauth_consent_request",
consent_link: Any = "https://consent.example.com/auth",
item_id: str = "oauth-item-1",
) -> MagicMock:
"""Create a mock ``response.output_item.added`` event."""
event = MagicMock()
event.type = "response.output_item.added"
item = MagicMock()
item.type = item_type
item.consent_link = consent_link
item.id = item_id
event.item = item
return event
def _make_top_level_event(
*,
consent_link: Any = "https://consent.example.com/authorize",
event_id: str = "consent-event-1",
) -> MagicMock:
"""Create a mock ``response.oauth_consent_requested`` event."""
event = MagicMock()
event.type = "response.oauth_consent_requested"
event.consent_link = consent_link
event.id = event_id
return event
def test_returns_none_for_unrelated_event() -> None:
"""An event with a non-oauth type returns None."""
event = MagicMock()
event.type = "response.output_text.delta"
assert try_parse_oauth_consent_event(event, "model-x") is None
def test_returns_none_for_event_without_type() -> None:
"""An event object missing a 'type' attribute returns None."""
event = object() # no type attribute
assert try_parse_oauth_consent_event(event, "model-x") is None
def test_parses_output_item_added_with_valid_link() -> None:
"""A response.output_item.added event with a valid HTTPS link produces Content."""
event = _make_output_item_event()
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert update.role == "assistant"
assert update.model == "test-model"
assert update.raw_representation is event
consent = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent) == 1
assert consent[0].consent_link == "https://consent.example.com/auth"
def test_parses_top_level_consent_requested_event() -> None:
"""A response.oauth_consent_requested event produces Content."""
event = _make_top_level_event()
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
consent = [c for c in update.contents if c.type == "oauth_consent_request"]
assert len(consent) == 1
assert consent[0].consent_link == "https://consent.example.com/authorize"
def test_empty_contents_for_non_https_link(caplog: pytest.LogCaptureFixture) -> None:
"""A non-HTTPS consent_link produces an update with empty contents and logs a warning."""
event = _make_output_item_event(consent_link="http://bad.example.com/login", item_id="item-http")
with caplog.at_level(logging.WARNING):
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert len(update.contents) == 0
assert "non-HTTPS" in caplog.text
def test_empty_contents_for_missing_consent_link(caplog: pytest.LogCaptureFixture) -> None:
"""A None consent_link produces an update with empty contents and logs a warning."""
event = _make_output_item_event(consent_link=None, item_id="item-none")
with caplog.at_level(logging.WARNING):
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert len(update.contents) == 0
assert "without valid consent_link" in caplog.text
def test_empty_contents_for_empty_string_consent_link(caplog: pytest.LogCaptureFixture) -> None:
"""An empty-string consent_link produces an update with empty contents and logs a warning."""
event = _make_output_item_event(consent_link="", item_id="item-empty")
with caplog.at_level(logging.WARNING):
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert len(update.contents) == 0
assert "without valid consent_link" in caplog.text
def test_empty_contents_for_https_empty_netloc(caplog: pytest.LogCaptureFixture) -> None:
"""An HTTPS URL with empty netloc (https:///path) is rejected."""
event = _make_output_item_event(consent_link="https:///path", item_id="item-no-netloc")
with caplog.at_level(logging.WARNING):
update = try_parse_oauth_consent_event(event, "test-model")
assert update is not None
assert len(update.contents) == 0
assert "non-HTTPS" in caplog.text
# endregion
@@ -172,12 +172,7 @@ class ResponsesHostServer(ResponsesAgentServerHost):
self._agent = agent
self.response_handler(self._handle_response) # pyright: ignore[reportUnknownMemberType]
@staticmethod
def _is_streaming_request(request: CreateResponse) -> bool:
"""Check if the request is a streaming request."""
return request.stream is not None and request.stream is True
def _handle_response(
async def _handle_response(
self,
request: CreateResponse,
context: ResponseContext,
@@ -186,11 +181,10 @@ class ResponsesHostServer(ResponsesAgentServerHost):
"""Handle the creation of a response."""
if self._is_workflow_agent:
# Workflow agents are handled differently because they require checkpoint restoration
return self._handle_workflow_agent(request, context)
return self._handle_inner_workflow(request, context)
return self._handle_inner_agent(request, context)
return self._handle_regular_agent(request, context)
async def _handle_regular_agent(
async def _handle_inner_agent(
self,
request: CreateResponse,
context: ResponseContext,
@@ -200,25 +194,24 @@ class ResponsesHostServer(ResponsesAgentServerHost):
input_messages = _items_to_messages(input_items)
history = await context.get_history()
messages: list[str | Content | Message] = [*_output_items_to_messages(history), *input_messages]
run_kwargs: dict[str, Any] = {"messages": [*_output_items_to_messages(history), *input_messages]}
is_streaming_request = request.stream is not None and request.stream is True
chat_options, are_options_set = _to_chat_options(request)
is_streaming_request = self._is_streaming_request(request)
response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model)
yield response_event_stream.emit_created()
yield response_event_stream.emit_in_progress()
if are_options_set and not isinstance(self._agent, RawAgent):
logger.warning("Agent doesn't support runtime options. They will be ignored.")
else:
run_kwargs["options"] = chat_options
if not is_streaming_request:
# Run the agent in non-streaming mode
if isinstance(self._agent, RawAgent):
raw_agent = cast("RawAgent[Any]", self._agent) # type: ignore[redundant-cast] # pyright: ignore[reportUnknownMemberType]
response = await raw_agent.run(messages, stream=False, options=chat_options)
else:
if are_options_set:
logger.warning("Agent doesn't support runtime options. They will be ignored.")
response = await self._agent.run(messages, stream=False)
response = await self._agent.run(stream=False, **run_kwargs) # type: ignore[reportUnknownMemberType]
for message in response.messages:
for content in message.contents:
@@ -228,20 +221,12 @@ class ResponsesHostServer(ResponsesAgentServerHost):
yield response_event_stream.emit_completed()
return
# Run the agent in streaming mode
if isinstance(self._agent, RawAgent):
raw_agent = cast("RawAgent[Any]", self._agent) # type: ignore[redundant-cast] # pyright: ignore[reportUnknownMemberType]
response_stream = raw_agent.run(messages, stream=True, options=chat_options)
else:
if are_options_set:
logger.warning("Agent doesn't support runtime options. They will be ignored.")
response_stream = self._agent.run(messages, stream=True)
# Track the current active output item builder for streaming;
# lazily created on matching content, closed when a different type arrives.
tracker = _OutputItemTracker(response_event_stream)
async for update in response_stream:
# Run the agent in streaming mode
async for update in self._agent.run(stream=True, **run_kwargs): # type: ignore[reportUnknownMemberType]
for content in update.contents:
for event in tracker.handle(content):
yield event
@@ -256,7 +241,7 @@ class ResponsesHostServer(ResponsesAgentServerHost):
yield response_event_stream.emit_completed()
async def _handle_workflow_agent(
async def _handle_inner_workflow(
self,
request: CreateResponse,
context: ResponseContext,
@@ -269,8 +254,7 @@ class ResponsesHostServer(ResponsesAgentServerHost):
"""
input_items = await context.get_input_items()
input_messages = _items_to_messages(input_items)
is_streaming_request = self._is_streaming_request(request)
is_streaming_request = request.stream is not None and request.stream is True
_, are_options_set = _to_chat_options(request)
if are_options_set:
@@ -311,7 +295,8 @@ class ResponsesHostServer(ResponsesAgentServerHost):
response_event_stream = ResponseEventStream(response_id=context.response_id, model=request.model)
# Create a new checkpoint storage for this response based on the following rules:
# - If no previous response ID or conversation ID is provided, create a new checkpoint storage for this response
# - If no previous response ID or conversation ID is provided,
# create a new checkpoint storage for this response
# - If a previous response ID is provided, create a new checkpoint storage for this response
# - If a conversation ID is provided, reuse the existing checkpoint storage for the conversation
context_id = context.conversation_id or context.response_id
@@ -333,14 +318,12 @@ class ResponsesHostServer(ResponsesAgentServerHost):
yield response_event_stream.emit_completed()
return
# Run the agent in streaming mode
response_stream = self._agent.run(input_messages, stream=True, checkpoint_storage=checkpoint_storage)
# Track the current active output item builder for streaming;
# lazily created on matching content, closed when a different type arrives.
tracker = _OutputItemTracker(response_event_stream)
async for update in response_stream:
# Run the workflow agent in streaming mode
async for update in self._agent.run(input_messages, stream=True, checkpoint_storage=checkpoint_storage):
for content in update.contents:
for event in tracker.handle(content):
yield event
@@ -355,7 +338,6 @@ class ResponsesHostServer(ResponsesAgentServerHost):
await self._delete_not_latest_checkpoints(checkpoint_storage, self._agent.workflow.name)
yield response_event_stream.emit_completed()
return
@staticmethod
async def _delete_not_latest_checkpoints(checkpoint_storage: FileCheckpointStorage, workflow_name: str) -> None:
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"azure-ai-agentserver-core==2.0.0b3",
"azure-ai-agentserver-responses==1.0.0b5",
"azure-ai-agentserver-invocations==1.0.0b3",
@@ -41,9 +41,10 @@ def _make_agent(
*,
response: AgentResponse | None = None,
stream_updates: list[AgentResponseUpdate] | None = None,
raw_agent: bool = True,
) -> MagicMock:
"""Create a mock agent implementing SupportsAgentRun."""
agent = MagicMock(spec=RawAgent)
agent = MagicMock(spec=RawAgent) if raw_agent else MagicMock()
agent.id = "test-agent"
agent.name = "Test Agent"
agent.description = "A mock agent for testing"
@@ -267,10 +268,18 @@ class TestNonStreaming:
async def test_chat_options_forwarded(self) -> None:
agent = _make_agent(
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])])
response=AgentResponse(messages=[Message(role="assistant", contents=[Content.from_text("ok")])]),
raw_agent=True,
)
server = _make_server(agent)
resp = await _post(server, stream=False, temperature=0.5, top_p=0.9, max_output_tokens=1024)
resp = await _post(
server,
stream=False,
temperature=0.5,
top_p=0.9,
max_output_tokens=1024,
parallel_tool_calls=True,
)
assert resp.status_code == 200
agent.run.assert_awaited_once()
@@ -280,6 +289,7 @@ class TestNonStreaming:
assert options["temperature"] == 0.5
assert options["top_p"] == 0.9
assert options["max_tokens"] == 1024
assert options["allow_multiple_tool_calls"] is True
# endregion
@@ -289,6 +299,31 @@ class TestNonStreaming:
class TestStreaming:
async def test_chat_options_forwarded(self) -> None:
agent = _make_agent(
stream_updates=[AgentResponseUpdate(contents=[Content.from_text("ok")], role="assistant")],
raw_agent=True,
)
server = _make_server(agent)
resp = await _post(
server,
stream=True,
temperature=0.5,
top_p=0.9,
max_output_tokens=1024,
parallel_tool_calls=True,
)
assert resp.status_code == 200
agent.run.assert_called_once()
call_kwargs = agent.run.call_args.kwargs
assert call_kwargs["stream"] is True
options = call_kwargs["options"]
assert options["temperature"] == 0.5
assert options["top_p"] == 0.9
assert options["max_tokens"] == 1024
assert options["allow_multiple_tool_calls"] is True
async def test_basic_text_streaming(self) -> None:
agent = _make_agent(
stream_updates=[
@@ -1426,7 +1461,7 @@ class TestMultiTurnMixedContent:
assert body["status"] == "completed"
# Verify agent received text + image
messages = agent.run.call_args.args[0]
messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 1
assert messages[0].role == "user"
assert len(messages[0].contents) == 2
@@ -1464,7 +1499,7 @@ class TestMultiTurnMixedContent:
body = resp.json()
assert body["status"] == "completed"
messages = agent.run.call_args.args[0]
messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 1
assert len(messages[0].contents) == 2
assert messages[0].contents[0].type == "text"
@@ -1501,7 +1536,7 @@ class TestMultiTurnMixedContent:
body = resp.json()
assert body["status"] == "completed"
messages = agent.run.call_args.args[0]
messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 1
assert len(messages[0].contents) == 2
assert messages[0].contents[0].type == "text"
@@ -1542,7 +1577,7 @@ class TestMultiTurnMixedContent:
body = resp.json()
assert body["status"] == "completed"
messages = agent.run.call_args.args[0]
messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 3
assert messages[0].role == "user"
assert messages[0].contents[0].type == "text"
@@ -1591,7 +1626,7 @@ class TestMultiTurnMixedContent:
assert body2["status"] == "completed"
# Verify second call receives history from turn 1 + text+image input
second_call_messages = agent.run.call_args_list[1].args[0]
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
# History: output message from turn 1 ("Send me an image")
# Input: message with text + image
assert len(second_call_messages) >= 2
@@ -1652,7 +1687,7 @@ class TestMultiTurnMixedContent:
assert resp2.json()["status"] == "completed"
# Verify turn 2 received history including function call/result
second_call_messages = agent.run.call_args_list[1].args[0]
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
roles = [m.role for m in second_call_messages]
assert "assistant" in roles
assert "tool" in roles
@@ -1703,7 +1738,7 @@ class TestMultiTurnMixedContent:
assert resp2.json()["status"] == "completed"
# Verify history includes the reasoning and text from turn 1
second_call_messages = agent.run.call_args_list[1].args[0]
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
assert len(second_call_messages) >= 2 # history + new input
async def test_multi_turn_with_mixed_content_and_streaming(self) -> None:
@@ -1795,7 +1830,7 @@ class TestMultiTurnMixedContent:
body = resp.json()
assert body["status"] == "completed"
messages = agent.run.call_args.args[0]
messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 2
assert messages[0].role == "user"
assert messages[0].contents[0].type == "text"
@@ -1867,7 +1902,7 @@ class TestMultiTurnMixedContent:
assert resp3.json()["status"] == "completed"
# Verify turn 3 received full history from turns 1+2 plus new image input
third_call_messages = agent.run.call_args_list[2].args[0]
third_call_messages = agent.run.call_args_list[2].kwargs["messages"]
# Should have: history from turn 1 (assistant text) + history from turn 2
# (function_call, function_call_output, text) + new input (text + image)
assert len(third_call_messages) >= 5
@@ -1918,7 +1953,7 @@ class TestMultiTurnMixedContent:
body = resp.json()
assert body["status"] == "completed"
messages = agent.run.call_args.args[0]
messages = agent.run.call_args.kwargs["messages"]
assert len(messages) == 1
assert len(messages[0].contents) == 2
assert messages[0].contents[0].type == "text"
@@ -1982,7 +2017,7 @@ class TestMultiTurnMixedContent:
assert resp2.json()["status"] == "completed"
# Verify turn 2 received history from turn 1 + new text+file input
second_call_messages = agent.run.call_args_list[1].args[0]
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
assert len(second_call_messages) >= 2
# History should include the assistant response from turn 1
@@ -2050,7 +2085,7 @@ class TestMultiTurnMixedContent:
assert resp2.json()["status"] == "completed"
# Verify turn 2 received history with function call + new text+image
second_call_messages = agent.run.call_args_list[1].args[0]
second_call_messages = agent.run.call_args_list[1].kwargs["messages"]
# History should contain function_call and function_result from turn 1
fc_contents = [
c for m in second_call_messages if m.role == "assistant" for c in m.contents if c.type == "function_call"
+2 -2
View File
@@ -4,7 +4,7 @@ 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.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"agent-framework-openai>=1.1.0,<2",
"foundry-local-sdk>=0.5.1,<0.5.2",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Google Gemini integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0a260423"
version = "1.0.0a260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -24,7 +24,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2.0",
"agent-framework-core>=1.2.0,<2.0",
"google-genai>=1.0.0,<2.0.0",
]
@@ -285,8 +285,10 @@ def test_vertex_ai_requires_project_and_location_together(monkeypatch: pytest.Mo
GeminiChatClient(model="gemini-2.5-flash")
async def test_missing_model_raises_on_get_response() -> None:
async def test_missing_model_raises_on_get_response(monkeypatch: pytest.MonkeyPatch) -> None:
"""Raises ValueError at call time when no model is set on the client or in options."""
monkeypatch.delenv("GEMINI_MODEL", raising=False)
monkeypatch.delenv("GOOGLE_MODEL", raising=False)
client, mock = _make_gemini_client(model=None) # type: ignore[arg-type]
mock.aio.models.generate_content = AsyncMock()
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"github-copilot-sdk>=0.2.1,<=0.2.1; python_version >= '3.11'",
]
@@ -207,9 +207,7 @@ class TestGitHubCopilotAgentInit:
def test_default_options_returns_independent_copy(self) -> None:
"""Test that mutating the returned dict does not affect internal state."""
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(
default_options={"model": "gpt-5.1-mini"}
)
agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent(default_options={"model": "gpt-5.1-mini"})
opts = agent.default_options
opts["model"] = "mutated"
assert agent._settings.get("model") == "gpt-5.1-mini"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Hyperlight CodeAct integrations for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0a260423"
version = "1.0.0a260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"hyperlight-sandbox>=0.3.0,<0.4",
"hyperlight-sandbox-backend-wasm>=0.3.0,<0.4 ; ((sys_platform == 'linux' and platform_machine == 'x86_64') or (sys_platform == 'win32' and platform_machine == 'AMD64')) and python_version < '3.14'",
"hyperlight-sandbox-python-guest>=0.3.0,<0.4",
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Experimental modules for Microsoft Agent Framework"
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -22,7 +22,7 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
]
[project.optional-dependencies]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Mem0 integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"mem0ai>=1.0.0,<2",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Ollama integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://learn.microsoft.com/en-us/agent-framework/"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"ollama>=0.5.3,<0.5.4",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "OpenAI integrations for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.1.1"
version = "1.2.0"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"openai>=1.99.0,<3",
]
@@ -355,6 +355,7 @@ async def test_integration_web_search() -> None:
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
@pytest.mark.skip(reason="Azure OpenAI with files raises 500 error. Needs investigation.")
async def test_integration_client_file_search() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=credential)
@@ -380,6 +381,7 @@ async def test_integration_client_file_search() -> None:
@pytest.mark.integration
@skip_if_azure_openai_integration_tests_disabled
@_with_azure_openai_debug()
@pytest.mark.skip(reason="Azure OpenAI with files raises 500 error. Needs investigation.")
async def test_integration_client_file_search_streaming() -> None:
async with AzureCliCredential() as credential:
client = OpenAIChatClient(credential=credential)
@@ -4,7 +4,7 @@ description = "Orchestration patterns for Microsoft Agent Framework. Includes Se
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
]
[tool.uv]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Purview (Graph dataSecurityAndGovernance) integration f
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://github.com/microsoft/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -24,7 +24,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"azure-core>=1.30.0,<2",
"httpx>=0.27.0,<0.29",
]
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Redis integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0b260423"
version = "1.0.0b260424"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.1.1,<2",
"agent-framework-core>=1.2.0,<2",
"redis>=6.4.0,<7.2.1",
"redisvl>=0.11.0,<0.16",
"numpy>=2.2.6,<3"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.1.1"
version = "1.2.0"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,7 +23,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core[all]==1.1.1",
"agent-framework-core[all]==1.2.0",
]
[dependency-groups]
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Functional Workflow with Agents — Call agents inside @workflow
This sample shows how to call agents inside a functional workflow.
Agent calls are just regular async function calls — no special wrappers needed.
"""
import asyncio
from agent_framework import Agent, workflow
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
# <create_agents>
client = FoundryChatClient(credential=AzureCliCredential())
writer = Agent(
name="WriterAgent",
instructions="Write a short poem (4 lines max) about the given topic.",
client=client,
)
reviewer = Agent(
name="ReviewerAgent",
instructions="Review the given poem in one sentence. Is it good?",
client=client,
)
# </create_agents>
# <create_workflow>
@workflow
async def poem_workflow(topic: str) -> str:
"""Write a poem, then review it."""
poem = (await writer.run(f"Write a poem about: {topic}")).text
review = (await reviewer.run(f"Review this poem: {poem}")).text
return f"Poem:\n{poem}\n\nReview: {review}"
# </create_workflow>
async def main() -> None:
result = await poem_workflow.run("a cat learning to code")
print(result.get_outputs()[0])
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft. All rights reserved.
"""
Functional Workflow Basics — Orchestrate async functions with @workflow
The functional API lets you write workflows as plain Python async functions.
No graph concepts, no edges, no executor classes — just call functions
and use native control flow (if/else, loops, asyncio.gather).
This sample builds a minimal pipeline with two steps:
1. Convert text to uppercase
2. Reverse the text
No external services are required.
"""
import asyncio
from agent_framework import workflow
# Plain async functions — no decorators needed
async def to_upper_case(text: str) -> str:
"""Convert input to uppercase."""
return text.upper()
async def reverse_text(text: str) -> str:
"""Reverse the string."""
return text[::-1]
# <create_workflow>
@workflow
async def text_workflow(text: str) -> str:
"""Uppercase the text, then reverse it."""
upper = await to_upper_case(text)
return await reverse_text(upper)
# </create_workflow>
async def main() -> None:
# <run_workflow>
result = await text_workflow.run("hello world")
print(f"Output: {result.get_outputs()}")
print(f"Final state: {result.get_final_state()}")
# </run_workflow>
"""
Expected output:
Output: ['DLROW OLLEH']
Final state: WorkflowRunState.IDLE
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -12,9 +12,12 @@ from agent_framework import (
from typing_extensions import Never
"""
First Workflow — Chain executors with edges
First Graph Workflow — Chain executors with edges
This sample builds a minimal workflow with two steps:
The graph API gives you full control over execution topology: edges,
fan-out/fan-in, switch/case, and superstep-based checkpointing.
This sample builds a minimal graph workflow with two steps:
1. Convert text to uppercase (class-based executor)
2. Reverse the text (function-based executor)
+4 -2
View File
@@ -24,8 +24,10 @@ export FOUNDRY_MODEL="gpt-4o" # optional, defaults to gpt-4o
| 2 | [02_add_tools.py](02_add_tools.py) | Define a function tool with `@tool` and attach it to an agent. |
| 3 | [03_multi_turn.py](03_multi_turn.py) | Keep conversation history across turns with `AgentSession`. |
| 4 | [04_memory.py](04_memory.py) | Add dynamic context with a custom `ContextProvider`. |
| 5 | [05_first_workflow.py](05_first_workflow.py) | Chain executors into a workflow with edges. |
| 6 | [06_host_your_agent.py](06_host_your_agent.py) | Host a single agent with Azure Functions. |
| 5 | [05_functional_workflow_with_agents.py](05_functional_workflow_with_agents.py) | Call agents inside a functional workflow. |
| 6 | [06_functional_workflow_basics.py](06_functional_workflow_basics.py) | Write a workflow as a plain async function. |
| 7 | [07_first_graph_workflow.py](07_first_graph_workflow.py) | Chain executors into a graph workflow with edges. |
| 8 | [08_host_your_agent.py](08_host_your_agent.py) | Host a single agent with Azure Functions. |
Run any sample with:
@@ -75,11 +75,7 @@ def get_client(client_name: ClientName) -> SupportsChatGetResponse[Any]:
if client_name == "azure_openai_chat_completion":
return OpenAIChatCompletionClient(credential=AzureCliCredential())
if client_name == "foundry_chat":
return FoundryChatClient(
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
credential=AzureCliCredential(),
)
return FoundryChatClient(credential=AzureCliCredential())
raise ValueError(f"Unsupported client name: {client_name}")
@@ -93,21 +89,6 @@ async def main(client_name: ClientName = "openai_chat") -> None:
print(f"Client: {client_name}")
print(f"User: {message.text}")
if isinstance(client, FoundryChatClient):
async with client:
if stream:
response_stream = client.get_response([message], stream=True, options={"tools": get_weather})
print("Assistant: ", end="")
async for chunk in response_stream:
if chunk.text:
print(chunk.text, end="")
print("")
else:
print(
f"Assistant: {await client.get_response([message], stream=False, options={'tools': get_weather})}"
)
return
if stream:
response_stream = client.get_response([message], stream=True, options={"tools": get_weather})
print("Assistant: ", end="")
+14
View File
@@ -30,6 +30,20 @@ Once comfortable with these, explore the rest of the samples below.
## Samples Overview (by directory)
### functional
Write workflows as plain Python async functions — no graph concepts, no executor classes, no edges. Use native control flow (`if`/`else`, loops, `asyncio.gather`) for branching and parallelism.
| Sample | File | Concepts |
|---|---|---|
| Basic Pipeline | [functional/basic_pipeline.py](./functional/basic_pipeline.py) | Sequential steps as plain async functions |
| Basic Streaming Pipeline | [functional/basic_streaming_pipeline.py](./functional/basic_streaming_pipeline.py) | Stream workflow events in real time with `run(stream=True)` |
| Parallel Pipeline | [functional/parallel_pipeline.py](./functional/parallel_pipeline.py) | Fan-out/fan-in with `asyncio.gather` |
| Steps and Checkpointing | [functional/steps_and_checkpointing.py](./functional/steps_and_checkpointing.py) | `@step` decorator for per-step checkpointing and observability |
| Human-in-the-Loop Review | [functional/hitl_review.py](./functional/hitl_review.py) | HITL with `ctx.request_info()` and replay |
| Agent Integration | [functional/agent_integration.py](./functional/agent_integration.py) | Calling agents inside workflow steps |
| Naive Group Chat | [functional/naive_group_chat.py](./functional/naive_group_chat.py) | Simple round-robin group chat as a plain loop |
### agents
| Sample | File | Concepts |
@@ -0,0 +1,107 @@
# Copyright (c) Microsoft. All rights reserved.
"""Calling agents inside functional workflows.
Agent calls work inside @workflow as plain function calls — no decorator needed.
Just call the agent and use the result.
If you want per-step caching (so agent calls don't re-execute on HITL resume
or crash recovery), add @step. Since each agent call hits an LLM API (time +
money), @step is often worth it. But it's always opt-in.
This sample shows both approaches side-by-side so you can see the difference.
"""
import asyncio
from agent_framework import Agent, step, workflow
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
# ---------------------------------------------------------------------------
# Create agents
# ---------------------------------------------------------------------------
client = FoundryChatClient(credential=AzureCliCredential())
classifier_agent = Agent(
name="ClassifierAgent",
instructions=(
"Classify documents into one category: Technical, Legal, Marketing, or Scientific. "
"Reply with only the category name."
),
client=client,
)
writer_agent = Agent(
name="WriterAgent",
instructions="Summarize the given content in one sentence.",
client=client,
)
reviewer_agent = Agent(
name="ReviewerAgent",
instructions="Review the given summary in one sentence. Is it accurate and complete?",
client=client,
)
# ---------------------------------------------------------------------------
# Simplest approach: call agents directly inside the workflow.
# No @step, no wrappers — just plain function calls.
# ---------------------------------------------------------------------------
@workflow
async def simple_pipeline(document: str) -> str:
"""Process a document — agents called inline, no @step."""
classification = (await classifier_agent.run(f"Classify this document: {document}")).text
summary = (await writer_agent.run(f"Summarize: {document}")).text
review = (await reviewer_agent.run(f"Review this summary: {summary}")).text
return f"Classification: {classification}\nSummary: {summary}\nReview: {review}"
# ---------------------------------------------------------------------------
# With @step: agent results are cached. On HITL resume or checkpoint
# recovery, completed steps return their saved result instead of calling
# the LLM again. Worth it for expensive operations.
# ---------------------------------------------------------------------------
@step
async def classify_document(doc: str) -> str:
return (await classifier_agent.run(f"Classify this document: {doc}")).text
@step
async def generate_summary(doc: str) -> str:
return (await writer_agent.run(f"Summarize: {doc}")).text
@step
async def review_summary(summary: str) -> str:
return (await reviewer_agent.run(f"Review this summary: {summary}")).text
@workflow
async def cached_pipeline(document: str) -> str:
"""Same pipeline, but @step caches each agent call."""
classification = await classify_document(document)
summary = await generate_summary(document)
review = await review_summary(summary)
return f"Classification: {classification}\nSummary: {summary}\nReview: {review}"
async def main():
# Simple version — agents called inline
result = await simple_pipeline.run("This is a technical document about machine learning...")
print(result.get_outputs()[0])
# Cached version — same result, but steps won't re-execute on resume
result = await cached_pipeline.run("This is a technical document about machine learning...")
print(f"\nCached: {result.get_outputs()[0]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,58 @@
# Copyright (c) Microsoft. All rights reserved.
"""Basic sequential pipeline using the functional workflow API.
The simplest possible workflow: plain async functions orchestrated by @workflow.
No @step decorator needed — just write Python.
"""
import asyncio
from agent_framework import workflow
# These are plain async functions — no decorators needed.
# They run normally inside the workflow, just like any other Python function.
async def fetch_data(url: str) -> dict[str, str | int]:
"""Simulate fetching data from a URL."""
return {"url": url, "content": f"Data from {url}", "status": 200}
async def transform_data(data: dict[str, str | int]) -> str:
"""Transform raw data into a summary string."""
return f"[{data['status']}] {data['content']}"
# @workflow turns this async function into a FunctionalWorkflow object.
# Without it, this is just a normal async function. With it, you get:
# - .run() that returns a WorkflowRunResult with events and outputs
# - .run(stream=True) for streaming events in real time
# - .as_agent() to use this workflow anywhere an agent is expected
#
# The function's first parameter receives the input from .run("...").
# Add a `ctx: RunContext` parameter only if you need HITL, state, or custom events.
@workflow
async def data_pipeline(url: str) -> str:
"""A simple sequential data pipeline."""
raw = await fetch_data(url)
summary = await transform_data(raw)
# This is just a function — plain Python works between calls.
# No need to wrap every operation in a separate async function.
is_valid = len(summary) > 0 and "[200]" in summary
tag = "VALID" if is_valid else "INVALID"
# Returning a value automatically emits it as an output.
# Callers retrieve it via result.get_outputs().
return f"[{tag}] {summary}"
async def main():
# .run() is provided by @workflow — a plain async function wouldn't have it
result = await data_pipeline.run("https://example.com/api/data")
print("Output:", result.get_outputs()[0])
print("State:", result.get_final_state())
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,63 @@
# Copyright (c) Microsoft. All rights reserved.
"""Basic streaming pipeline using the functional workflow API.
Stream workflow events in real time with run(stream=True).
"""
import asyncio
from agent_framework import workflow
# Plain async functions — no decorators needed for simple helpers.
async def fetch_data(url: str) -> dict[str, str | int]:
"""Simulate fetching data from a URL."""
return {"url": url, "content": f"Data from {url}", "status": 200}
async def transform_data(data: dict[str, str | int]) -> str:
"""Transform raw data into a summary string."""
return f"[{data['status']}] {data['content']}"
async def validate_result(summary: str) -> bool:
"""Validate the transformed result."""
return len(summary) > 0 and "[200]" in summary
# @workflow enables .run(stream=True), which returns a ResponseStream
# you can iterate over with `async for`. Without @workflow, you'd just
# have a normal async function with no streaming capability.
@workflow
async def data_pipeline(url: str) -> str:
"""A simple sequential data pipeline."""
raw = await fetch_data(url)
summary = await transform_data(raw)
is_valid = await validate_result(summary)
return f"{summary} (valid={is_valid})"
async def main():
# run(stream=True) returns a ResponseStream that yields events as they
# are produced. The raw stream includes lifecycle events (started, status)
# alongside application events — filter by event.type to find what you need.
stream = data_pipeline.run("https://example.com/api/data", stream=True)
async for event in stream:
if event.type == "output":
print(f"Output: {event.data}")
# After iteration, get_final_response() returns the WorkflowRunResult
result = await stream.get_final_response()
print(f"Final state: {result.get_final_state()}")
"""
Expected output:
Output: [200] Data from https://example.com/api/data (valid=True)
Final state: WorkflowRunState.IDLE
"""
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,84 @@
# Copyright (c) Microsoft. All rights reserved.
"""Human-in-the-loop review pipeline using functional workflows.
Demonstrates ctx.request_info() for pausing the workflow to wait for
external input and resuming with run(responses={...}).
HITL works with or without @step. The difference is what happens on resume:
- Without @step: every function re-executes from the top (fine for cheap calls).
- With @step: completed functions return their saved result instantly.
This sample uses @step on write_draft() because it simulates an expensive
operation that shouldn't re-run just because the workflow was paused.
"""
import asyncio
from agent_framework import RunContext, WorkflowRunState, step, workflow
# @step saves the result. When the workflow resumes after the HITL pause,
# this returns its saved result instead of running the expensive operation again.
#
# In a real workflow you might call an agent here instead:
# @step
# async def write_draft(topic: str) -> str:
# return (await writer_agent.run(f"Write a draft about: {topic}")).text
@step
async def write_draft(topic: str) -> str:
"""Simulate writing a draft — expensive, shouldn't re-run on resume."""
print(f" write_draft executing for '{topic}'")
return f"Draft document about '{topic}': Lorem ipsum dolor sit amet..."
@step
async def revise_draft(draft: str, feedback: str) -> str:
"""Revise the draft based on feedback."""
return f"Revised: {draft[:50]}... [Applied feedback: {feedback}]"
@workflow
async def review_pipeline(topic: str, ctx: RunContext) -> str:
"""Write a draft, get human review, then revise."""
draft = await write_draft(topic)
# ctx.request_info() suspends the workflow here. The caller gets back
# a WorkflowRunResult with state IDLE_WITH_PENDING_REQUESTS and can
# inspect the pending request via result.get_request_info_events().
feedback = await ctx.request_info(
{"draft": draft, "instructions": "Please review this draft"},
response_type=str,
request_id="review_request",
)
# This only executes after the caller resumes with run(responses={...}).
# write_draft above returns its saved result (thanks to @step),
# request_info returns the provided response, and we continue here.
return await revise_draft(draft, feedback)
async def main():
# Phase 1: Run until the workflow pauses for human input
print("=== Phase 1: Initial run ===")
result1 = await review_pipeline.run("AI Safety")
# If request_info() was reached, the state is IDLE_WITH_PENDING_REQUESTS.
# If the workflow completed without hitting request_info(), it would be IDLE.
print(f"State: {(final_state := result1.get_final_state())}")
assert final_state == WorkflowRunState.IDLE_WITH_PENDING_REQUESTS
requests = result1.get_request_info_events()
print(f"Pending request: {requests[0].request_id}")
# Phase 2: Resume with the human's response
print("\n=== Phase 2: Resume with feedback ===")
print("(write_draft should NOT execute again — saved by @step)")
result2 = await review_pipeline.run(responses={"review_request": "Add more details about alignment research"})
print(f"State: {result2.get_final_state()}")
print(f"Output: {result2.get_outputs()[0]}")
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,82 @@
# Copyright (c) Microsoft. All rights reserved.
"""Naive group chat using the functional workflow API.
A simple round-robin group chat where agents take turns responding.
Because it's just a function, you control the loop, the turn order,
and the termination condition with plain Python — no framework abstractions.
Compare this with the graph-based GroupChat orchestration to see how the
functional API lets you start simple and add complexity only when needed.
"""
import asyncio
from agent_framework import Agent, Message, workflow
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
# ---------------------------------------------------------------------------
# Create agents
# ---------------------------------------------------------------------------
client = FoundryChatClient(credential=AzureCliCredential())
expert = Agent(
name="PythonExpert",
instructions=(
"You are a Python expert in a group discussion. "
"Answer questions about Python and refine your answer based on feedback. "
"Keep responses concise (2-3 sentences)."
),
client=client,
)
critic = Agent(
name="Critic",
instructions=(
"You are a constructive critic in a group discussion. "
"Point out edge cases, gotchas, or missing nuances in the previous answer. "
"If the answer is solid, say so briefly."
),
client=client,
)
summarizer = Agent(
name="Summarizer",
instructions=(
"You are a summarizer in a group discussion. "
"After the discussion, provide a final concise summary that incorporates "
"the expert's answer and the critic's feedback. Keep it to 2-3 sentences."
),
client=client,
)
# ---------------------------------------------------------------------------
# A naive group chat is just a loop — no special framework needed
# ---------------------------------------------------------------------------
@workflow
async def group_chat(question: str) -> str:
"""Round-robin group chat: expert answers, critic reviews, summarizer wraps up."""
participants = [expert, critic, summarizer]
# Passing list[Message] keeps roles/authorship intact between turns,
# instead of stringifying everything into a single prompt.
conversation: list[Message] = [Message("user", [question])]
# Simple round-robin: each agent sees the full conversation so far
for agent in participants:
response = await agent.run(conversation)
conversation.extend(response.messages)
return "\n\n".join(f"{m.author_name or m.role}: {m.text}" for m in conversation)
async def main():
result = await group_chat.run("What's the difference between a list and a tuple in Python?")
print(result.get_outputs()[0])
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,66 @@
# Copyright (c) Microsoft. All rights reserved.
"""Parallel pipeline using asyncio.gather with functional workflows.
Fan-out/fan-in uses native Python concurrency via asyncio.gather.
No @step needed — still just plain async functions.
"""
import asyncio
from agent_framework import workflow
# Plain async functions — asyncio.gather handles the concurrency,
# no framework primitives needed for parallelism.
async def research_web(topic: str) -> str:
"""Simulate web research."""
await asyncio.sleep(0.05)
return f"Web results for '{topic}': 10 articles found"
async def research_papers(topic: str) -> str:
"""Simulate academic paper search."""
await asyncio.sleep(0.05)
return f"Papers on '{topic}': 3 relevant papers"
async def research_news(topic: str) -> str:
"""Simulate news search."""
await asyncio.sleep(0.05)
return f"News about '{topic}': 5 recent articles"
async def synthesize(sources: list[str]) -> str:
"""Combine research results into a summary."""
return "Research Summary:\n" + "\n".join(f" - {s}" for s in sources)
# @workflow wraps the orchestration logic so you get .run(), streaming,
# and events. The functions it calls are plain Python — no decorators
# needed just because they're inside a workflow.
@workflow
async def research_pipeline(topic: str) -> str:
"""Fan-out to three research tasks, then synthesize results."""
# asyncio.gather runs all three concurrently — this is standard Python,
# not a framework concept. Use it the same way you would anywhere else.
#
# Tip: if any of these were wrapped with @step (e.g. an expensive agent call),
# the pattern is identical — @step composes with asyncio.gather, so each
# branch is independently cached on HITL resume or checkpoint restore.
web, papers, news = await asyncio.gather(
research_web(topic),
research_papers(topic),
research_news(topic),
)
return await synthesize([web, papers, news])
async def main():
result = await research_pipeline.run("AI agents")
print(result.get_outputs()[0])
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,97 @@
# Copyright (c) Microsoft. All rights reserved.
"""Introducing @step: per-step checkpointing and observability.
The previous samples used plain functions — and that works. Workflows support
HITL (ctx.request_info) and checkpointing regardless of whether you use @step.
The difference: without @step, a resumed workflow re-executes every function
call from the top. That's fine for cheap functions. But for expensive operations
(API calls, agent runs, etc.) you don't want to pay that cost again.
@step saves each function's result so it skips re-execution on resume:
- On HITL resume, completed steps return their saved result instantly.
- On crash recovery from a checkpoint, earlier step results are restored.
- Each step emits executor_invoked/executor_completed events for observability.
@step is opt-in. Plain functions still work alongside @step in the same workflow.
"""
import asyncio
from agent_framework import InMemoryCheckpointStorage, step, workflow
# Track call counts to show which functions actually execute on resume
fetch_calls = 0
transform_calls = 0
# @step saves this function's result. On resume, it returns the saved
# result instead of re-executing — useful because this is expensive.
@step
async def fetch_data(url: str) -> dict[str, str | int]:
"""Expensive operation — @step prevents re-execution on resume."""
global fetch_calls
fetch_calls += 1
print(f" fetch_data called (call #{fetch_calls})")
return {"url": url, "content": f"Data from {url}", "status": 200}
@step
async def transform_data(data: dict[str, str | int]) -> str:
"""Another expensive operation — @step saves the result."""
global transform_calls
transform_calls += 1
print(f" transform_data called (call #{transform_calls})")
return f"[{data['status']}] {data['content']}"
# No @step — this is cheap, so it just re-runs on resume. That's fine.
async def validate_result(summary: str) -> bool:
"""Cheap validation — no @step needed."""
return len(summary) > 0 and "[200]" in summary
storage = InMemoryCheckpointStorage()
# checkpoint_storage tells @workflow where to persist step results.
# Each @step saves a checkpoint after it completes.
@workflow(checkpoint_storage=storage)
async def data_pipeline(url: str) -> str:
"""Mix of @step functions and plain functions."""
raw = await fetch_data(url)
summary = await transform_data(raw)
is_valid = await validate_result(summary)
return f"{summary} (valid={is_valid})"
async def main():
# --- Run 1: Everything executes normally ---
print("=== Run 1: Fresh execution ===")
result = await data_pipeline.run("https://example.com/api/data")
print(f"Output: {result.get_outputs()[0]}")
print(f"fetch_calls={fetch_calls}, transform_calls={transform_calls}")
# @step functions emit executor events; plain functions don't.
print("\nEvents:")
for event in result:
if event.type in ("executor_invoked", "executor_completed"):
print(f" {event.type}: {event.executor_id}")
# --- Run 2: Restore from checkpoint ---
# The workflow re-executes, but @step functions return saved results.
# Only validate_result() (no @step) actually runs again.
print("\n=== Run 2: Restored from checkpoint ===")
latest = await storage.get_latest(workflow_name="data_pipeline")
assert latest is not None
result2 = await data_pipeline.run(checkpoint_id=latest.checkpoint_id)
print(f"Output: {result2.get_outputs()[0]}")
print(f"fetch_calls={fetch_calls}, transform_calls={transform_calls}")
print("(call counts unchanged — @step results were restored from checkpoint)")
if __name__ == "__main__":
asyncio.run(main())
@@ -8,4 +8,4 @@ This folder contains a list of samples that show how to host agents using the `r
| [02_local_tools](./02_local_tools) | An example of hosting an agent with the `responses` API and local tools including a function tool and a local shell tool. |
| [03_remote_mcp](./03_remote_mcp) | An example of hosting an agent with the `responses` API and remote MCPs, including a GitHub MCP server and a Foundry Toolbox. |
| [04_workflows](./04_workflows) | An example of hosting a workflow with the `responses` API. |
| [using_deployed_agent.py](./using_deployed_agent.py) | An example of how to use the deployed agent in Agent Framework. |
| [using_deployed_agent.py](./using_deployed_agent.py) | Connect to the deployed basic Foundry agent with `FoundryAgent`, `allow_preview=True`, and version `v2`. |
@@ -1,50 +1,146 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import os
from collections.abc import Mapping
from typing import Any, cast
from agent_framework import Agent, AgentResponse, AgentResponseUpdate, ResponseStream
from agent_framework.openai import OpenAIChatClient
from typing_extensions import Any
from agent_framework import AgentSession
from agent_framework.foundry import FoundryAgent
from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import VersionRefIndicator
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
load_dotenv()
"""
This script demonstrates how to talk to a deployed agent using the OpenAIChatClient.
This sample demonstrates how to connect to the deployed basic Foundry agent with
`FoundryAgent`.
The sample uses environment variables for configuration, which can be set in a .env file or in the environment directly:
Environment variables:
FOUNDRY_PROJECT_ENDPOINT: Azure AI Foundry project endpoint.
FOUNDRY_AGENT_NAME: Hosted agent name.
FOUNDRY_AGENT_VERSION: Hosted agent version. Optional, defaults to latest if not specified.
After you deploy one of the agents in this directory, you can run this sample
to connect to it and have a conversation.
Note: The `allow_preview=True` flag is required to connect to the new hosted
agents, as this is a preview feature in Foundry.
Depending on where you have deployed your agent (local or Foundry Hosting), you may
need to change the base_url when initializing the OpenAIChatClient.
"""
async def print_streaming_response(streaming_response: ResponseStream[AgentResponseUpdate, AgentResponse[Any]]) -> None:
async for chunk in streaming_response:
if chunk.text:
print(chunk.text, end="", flush=True)
async def create_hosted_agent_session(
*,
agent: FoundryAgent,
project_client: AIProjectClient,
agent_name: str,
agent_version: str | None,
isolation_key: str,
) -> AgentSession:
"""Create a hosted-agent service session and wrap it in an AgentSession."""
create_session_kwargs: dict[str, Any] = {
"agent_name": agent_name,
"isolation_key": isolation_key,
}
resolved_agent_version = agent_version
if resolved_agent_version is None:
agent_details = await cast(Any, project_client.beta.agents).get( # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
agent_name=agent_name
)
versions = getattr(agent_details, "versions", None)
if not isinstance(versions, Mapping):
raise ValueError("Hosted agent details did not include a versions mapping.")
latest_version = getattr(cast(Any, versions.get("latest")), "version", None)
if not isinstance(latest_version, str) or not latest_version:
raise ValueError("Hosted agent details did not include a latest version string.")
resolved_agent_version = latest_version
create_session_kwargs["version_indicator"] = VersionRefIndicator(agent_version=resolved_agent_version)
service_session = await project_client.beta.agents.create_session(**create_session_kwargs)
agent_session_id = getattr(service_session, "agent_session_id", None)
if not isinstance(agent_session_id, str) or not agent_session_id:
raise ValueError("Hosted agent session creation did not return a non-empty agent_session_id.")
return agent.get_session(agent_session_id)
async def main() -> None:
agent = Agent(client=OpenAIChatClient(base_url="http://localhost:8088"))
session = agent.create_session()
credential = AzureCliCredential()
project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
agent_name = os.environ["FOUNDRY_AGENT_NAME"]
agent_version = os.getenv("FOUNDRY_AGENT_VERSION")
isolation_key = "my-isolation-key"
# First turn
query = "Hi!"
print(f"User: {query}")
print("Agent: ", end="", flush=True)
streaming_response = agent.run(query, session=session, stream=True)
await print_streaming_response(streaming_response)
project_client = AIProjectClient(
endpoint=project_endpoint,
credential=credential,
allow_preview=True,
)
async with (
project_client,
FoundryAgent(
project_client=project_client,
agent_name=agent_name,
agent_version=agent_version,
allow_preview=True,
) as agent,
):
session = await create_hosted_agent_session(
agent=agent,
project_client=project_client,
agent_name=agent_name,
agent_version=agent_version,
isolation_key=isolation_key,
)
# Second turn
query = "Your name is Javis. What can you do?"
print(f"\nUser: {query}")
print("Agent: ", end="", flush=True)
streaming_response = agent.run(query, session=session, stream=True)
await print_streaming_response(streaming_response)
try:
# 1. Send the first turn.
query = "Hi!"
print(f"User: {query}")
print("Agent: ", end="", flush=True)
async for chunk in agent.run(query, session=session, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
# Third turn
query = "What is your name?"
print(f"\nUser: {query}")
print("Agent: ", end="", flush=True)
streaming_response = agent.run(query, session=session, stream=True)
await print_streaming_response(streaming_response)
# 2. Continue the conversation with the same deployed agent session.
query = "Your name is Javis. What can you do?"
print(f"\nUser: {query}")
print("Agent: ", end="", flush=True)
async for chunk in agent.run(query, session=session, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
# 3. Ask a follow-up question in the same session.
query = "What is your name?"
print(f"\nUser: {query}")
print("Agent: ", end="", flush=True)
async for chunk in agent.run(query, session=session, stream=True):
if chunk.text:
print(chunk.text, end="", flush=True)
finally:
if session.service_session_id is not None:
await project_client.beta.agents.delete_session(
agent_name=agent_name,
session_id=session.service_session_id,
isolation_key=isolation_key,
)
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output:
User: Hi!
Agent: Hello! How can I help you today?
User: Your name is Javis. What can you do?
Agent: I can answer questions and help with tasks using the instructions configured on the deployed agent.
User: What is your name?
Agent: My name is Javis.
"""
+4 -2
View File
@@ -20,8 +20,10 @@ Start with `01-get-started/` and work through the numbered files:
2. **[02_add_tools.py](./01-get-started/02_add_tools.py)** — Add function tools with `@tool`
3. **[03_multi_turn.py](./01-get-started/03_multi_turn.py)** — Multi-turn conversations with `AgentSession`
4. **[04_memory.py](./01-get-started/04_memory.py)** — Agent memory with `ContextProvider`
5. **[05_first_workflow.py](./01-get-started/05_first_workflow.py)** — Build a workflow with executors and edges
6. **[06_host_your_agent.py](./01-get-started/06_host_your_agent.py)** — Host your agent via Azure Functions
5. **[05_functional_workflow_with_agents.py](./01-get-started/05_functional_workflow_with_agents.py)** — Call agents inside a functional workflow
6. **[06_functional_workflow_basics.py](./01-get-started/06_functional_workflow_basics.py)** — Write a workflow as a plain async function
7. **[07_first_graph_workflow.py](./01-get-started/07_first_graph_workflow.py)** — Build a workflow with executors and edges
8. **[08_host_your_agent.py](./01-get-started/08_host_your_agent.py)** — Host your agent via Azure Functions
## Prerequisites
+27 -27
View File
@@ -96,7 +96,7 @@ wheels = [
[[package]]
name = "agent-framework"
version = "1.1.1"
version = "1.2.0"
source = { virtual = "." }
dependencies = [
{ name = "agent-framework-core", extra = ["all"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -151,7 +151,7 @@ dev = [
[[package]]
name = "agent-framework-a2a"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/a2a" }
dependencies = [
{ name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -166,7 +166,7 @@ requires-dist = [
[[package]]
name = "agent-framework-ag-ui"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/ag-ui" }
dependencies = [
{ name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -194,7 +194,7 @@ provides-extras = ["dev"]
[[package]]
name = "agent-framework-anthropic"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/anthropic" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -209,7 +209,7 @@ requires-dist = [
[[package]]
name = "agent-framework-azure-ai-search"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/azure-ai-search" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -224,7 +224,7 @@ requires-dist = [
[[package]]
name = "agent-framework-azure-cosmos"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/azure-cosmos" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -239,7 +239,7 @@ requires-dist = [
[[package]]
name = "agent-framework-azurefunctions"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/azurefunctions" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -261,7 +261,7 @@ dev = []
[[package]]
name = "agent-framework-bedrock"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/bedrock" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -278,7 +278,7 @@ requires-dist = [
[[package]]
name = "agent-framework-chatkit"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/chatkit" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -293,7 +293,7 @@ requires-dist = [
[[package]]
name = "agent-framework-claude"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/claude" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -308,7 +308,7 @@ requires-dist = [
[[package]]
name = "agent-framework-copilotstudio"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/copilotstudio" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -323,7 +323,7 @@ requires-dist = [
[[package]]
name = "agent-framework-core"
version = "1.1.1"
version = "1.2.0"
source = { editable = "packages/core" }
dependencies = [
{ name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -395,7 +395,7 @@ provides-extras = ["all"]
[[package]]
name = "agent-framework-declarative"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/declarative" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -420,7 +420,7 @@ dev = [{ name = "types-pyyaml", specifier = "==6.0.12.20250915" }]
[[package]]
name = "agent-framework-devui"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/devui" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -458,7 +458,7 @@ provides-extras = ["dev", "all"]
[[package]]
name = "agent-framework-durabletask"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/durabletask" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -485,7 +485,7 @@ dev = [{ name = "types-python-dateutil", specifier = "==2.9.0.20260402" }]
[[package]]
name = "agent-framework-foundry"
version = "1.1.1"
version = "1.2.0"
source = { editable = "packages/foundry" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -523,7 +523,7 @@ requires-dist = [
[[package]]
name = "agent-framework-foundry-local"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/foundry_local" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -540,7 +540,7 @@ requires-dist = [
[[package]]
name = "agent-framework-gemini"
version = "1.0.0a260423"
version = "1.0.0a260424"
source = { editable = "packages/gemini" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -555,7 +555,7 @@ requires-dist = [
[[package]]
name = "agent-framework-github-copilot"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/github_copilot" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -570,7 +570,7 @@ requires-dist = [
[[package]]
name = "agent-framework-hyperlight"
version = "1.0.0a260423"
version = "1.0.0a260424"
source = { editable = "packages/hyperlight" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -589,7 +589,7 @@ requires-dist = [
[[package]]
name = "agent-framework-lab"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/lab" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -670,7 +670,7 @@ dev = [
[[package]]
name = "agent-framework-mem0"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/mem0" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -685,7 +685,7 @@ requires-dist = [
[[package]]
name = "agent-framework-ollama"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/ollama" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -700,7 +700,7 @@ requires-dist = [
[[package]]
name = "agent-framework-openai"
version = "1.1.1"
version = "1.2.0"
source = { editable = "packages/openai" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -715,7 +715,7 @@ requires-dist = [
[[package]]
name = "agent-framework-orchestrations"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/orchestrations" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -726,7 +726,7 @@ requires-dist = [{ name = "agent-framework-core", editable = "packages/core" }]
[[package]]
name = "agent-framework-purview"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/purview" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -743,7 +743,7 @@ requires-dist = [
[[package]]
name = "agent-framework-redis"
version = "1.0.0b260423"
version = "1.0.0b260424"
source = { editable = "packages/redis" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },