Compare commits

..
Author SHA1 Message Date
Evan MattsonandGitHub f5419b9f38 Python: bump package versions for 1.2.2 release (#5561)
* Python: bump package versions for 1.2.2 release

PATCH bump (1.2.1 -> 1.2.2) for the released cohort. Five PRs land in this
window:

- agent-framework-openai: fix file_search citations breaking the assistant-
  message history roundtrip (#5557) — drives the released-tier PATCH
- agent-framework-orchestrations: [BREAKING] standardize orchestration
  terminal outputs as AgentResponse (#5301)
- agent-framework-core, agent-framework-declarative: preserve Workflow.run()
  shared state across calls, accept list[Message] in declarative start
  executor, and coerce Enum values when serializing PowerFx symbols (#5531)
- agent-framework-foundry-hosting: add hosted Durable Workflow support
  (#5531)
- agent-framework-azure-contentunderstanding: new alpha package — Azure AI
  Content Understanding context provider (#4829)
- dependencies: workspace package dependency refresh (#5555)

Per lockstep convention, all 21 beta packages stamp 1.0.0b260429 and all 4
alpha packages (now including the new contentunderstanding) stamp
1.0.0a260429. Date stamp reflects 2026-04-29 Pacific. Every non-core package
floor on agent-framework-core is raised to >=1.2.2; the new
contentunderstanding package's stale >=1.0.0 floor is brought into line.

Two follow-on fixes bundled to keep validate-dependency-bounds-test green
at lowest-direct resolution:
- Bump agent-framework-azure-contentunderstanding's azure-ai-content
  understanding lower bound from >=1.0.0 to >=1.0.1 (1.0.0 ships without
  proper typing — pyright reports 65 unknown-type errors)
- Add pyright ignore comments to core/foundry/__init__.pyi for the new
  alpha package's type-stub imports, since alpha packages are not in
  core's [all] extra and therefore aren't installed at lowest-direct

* Python: add #5552 to 1.2.2 CHANGELOG

Add the streaming-span observability fix to the Fixed section. PR is on
upstream/main but not yet pulled into origin/main; the code itself will
land via the PR merge.

* Python: address PR #5561 review feedback on dependency bounds

Two packaging fixes flagged in review:

1. agent-framework-azure-contentunderstanding: add agent-framework-foundry
   as a runtime dependency. The package's README directs users to
   `pip install agent-framework-azure-contentunderstanding --pre` and the
   basic example imports `FoundryChatClient` from `agent_framework.foundry`,
   so the documented install path was failing with ImportError. Pulling
   agent-framework-foundry into deps makes the advertised entry path
   self-contained.

2. agent-framework-foundry: bump agent-framework-openai lower bound from
   >=1.1.0 to >=1.2.2,<2. Foundry imports private modules from
   agent_framework_openai (`_chat_client.py:22`, `_agent.py:34`), so
   resolvers were free to pair foundry==1.2.2 with older OpenAI versions
   that lack this release's coordinated Responses/history fix. Lockstep the
   floor with the released cohort to prevent mismatched installs.

Both changes pass `validate-dependency-bounds-test` lower + upper at
their respective packages.
2026-04-29 17:51:48 +09:00
Tao ChenandGitHub 03e47b5232 Python: Fix spans not correctly nested when using streaming (#5552)
* Fix spans not correctly nested when using streaming

* fix pre commit

* Address comments
2026-04-29 08:21:28 +00:00
Evan MattsonandGitHub 46ab47b9e1 Python: Fix file_search citations breaking assistant history roundtrip (#5557)
* Python: Fix file_search citations breaking assistant history roundtrip

The Responses API rejects 'input_file' inside an assistant message, but the
SDK was emitting it whenever an assistant Message contained a hosted_file
content (which is what file_search citations become). Three coordinated fixes:

1. _prepare_content_for_openai now skips hosted_file for the assistant role
   instead of mapping to input_file (which the API rejects there).

2. The streaming response.output_text.annotation.added handler attaches
   file_citation, container_file_citation, and file_path as annotations on
   text content, matching the non-streaming path. Previously streaming
   produced standalone HostedFileContent items that always tripped (1).

3. output_text serialization preserves Annotation objects on roundtrip via a
   new _annotations_to_output_text helper instead of hardcoding 'annotations'
   to []. file_search citations now survive multi-agent forwarding.

Closes #5556.

* Address PR review

- _annotations_to_output_text: fan out one entry per annotated_region for
  url_citation/container_file_citation (Annotation.annotated_regions is a
  Sequence; the API form carries one start/end per entry).
- Validate region span bounds are ints before emitting; skip otherwise.
- Add test for the file_path branch (annotation with file_id only).
- Add test verifying streamed citation events coalesce onto surrounding
  text via _finalize_response so span indices reference the merged text,
  not the empty-text streaming carrier.
2026-04-29 07:38:19 +00:00
Evan MattsonandGitHub 094f9903b3 Python: Update package dependencies (#5555)
* Update dependencies

* Preserve mcp[ws] and uvicorn[standard] extras in override-dependencies

Bare-package overrides on mcp and uvicorn dropped the [ws] and [standard]
extras (and their transitive deps like httptools, watchfiles) from the
generated lock. Re-add the extras to the overrides so the lock matches
what workspace packages actually request.
2026-04-29 06:18:03 +00:00
8b71f9459a Python: Feature/hosted dwf (#5531)
* Fix declarative Workflow.as_agent() by accepting list[Message] in start executor

The declarative start executor (JoinExecutor) only advertised dict and str
in its input_types, so WorkflowAgent.__init__ rejected it with
'Workflow's start executor cannot handle list[Message]'.

Add list[Message] to the JoinExecutor handler annotation and add a
matching branch in DeclarativeActionExecutor._ensure_state_initialized
that extracts the last user-message text and falls through to the
string-input initialization path, so =System.LastMessageText works
end-to-end via as_agent().

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

* Populate Conversation.messages from list[Message] trigger

When Workflow.as_agent() is invoked with a list[Message], the start executor now populates Conversation.messages / Conversation.history / System.conversations.{id}.messages with prior turns only (excluding the latest user message), and surfaces the latest user message via Inputs.input and System.LastMessage*. This matches InvokeAzureAgent's contract that the messages binding holds prior turns and the executor itself appends the new user input before invoking, avoiding double-append of the trailing user turn while preserving full history (incl. assistant/system/tool roles and multi-modal content) for downstream actions.

* Coerce Enum values when serializing PowerFx symbols

MessageRole and other str-subclass Enums passed isinstance(v, str) and were forwarded to pythonnet unchanged. pythonnet then raised 'MessageRole value cannot be converted to System.String' for every PowerFx primitive when ConditionGroup/Expr eval walked the symbol table containing Conversation.messages. Reduce Enum members to their underlying value before the primitive check so eval sees plain strings/ints.

* Foundry hosting: pass full conversation history to workflow agents

_handle_inner_workflow only forwarded the latest user turn to WorkflowAgent.run, even though _handle_inner_agent already prepends history fetched from Foundry storage to the messages it sends a regular agent. Declarative workflows reset Conversation.messages on every run (state.initialize), so checkpoint replay alone does not give them prior turns - the host has to pass them in, the same way it does for non-workflow agents. Mirror that contract: fetch context.get_history() and pass [*history, *input_messages] to the workflow agent.

* feat(workflows): support combined message + checkpoint_id for multi-turn continuation

Allow Workflow.run(message=..., checkpoint_id=...) so callers can restore
prior workflow state from a checkpoint AND deliver a new message to the
start executor in a single call. The existing reset_context logic
already preserves shared state when checkpoint_id is set, so this gives
us 'fresh start executor invocation with prior state intact' - exactly
what hosted multi-turn declarative workflows need.

- _workflow.py: drop the message+checkpoint_id mutual exclusion and
  update _execute_with_message_or_checkpoint to do both (restore then
  execute) when both are provided.
- _agent.py: in _run_core's checkpoint branch, also forward
  input_messages so WorkflowAgent.run(messages, checkpoint_id=...) works
  end-to-end. Falls back to the legacy 'restore only' behavior when
  messages are absent.
- _declarative_base.py: detect continuation in _ensure_state_initialized
  by checking whether DECLARATIVE_STATE_KEY already exists in shared
  state; if so, refresh inputs/LastMessage* and append non-user trigger
  messages instead of calling state.initialize() (which would wipe
  Conversation/Local/System).
- foundry_hosting/_responses.py: collapse the host's two-call pattern
  (restore-only, then fresh run) into a single combined call now that
  the underlying APIs support it.
- tests: drop the assertion that combined message+checkpoint_id raises.

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

* Pivot: preserve workflow state across run() calls

Replace the prior 'combined message + checkpoint_id in one run()' approach
with a cleaner default: Workflow.run no longer wipes shared state or runner-
context messages between calls. Iteration counting and per-run kwargs still
reset on a fresh-message run; checkpoint and responses runs are continuations
that preserve everything.

This lets a WorkflowAgent be invoked repeatedly on the same instance and
maintain multi-turn context (e.g. accumulated Conversation.messages) without
asking developers to opt in. Hosted-agent multi-turn pattern becomes two
explicit calls: restore-from-checkpoint (drive to idle), then run-with-message.

Key changes:
- _workflow.py: drop _state.clear() and reset_for_new_run() from run().
  Reset iteration count and run kwargs on fresh-message runs only.
  Restore 'Cannot provide both message and checkpoint_id' validation.
  Add async guard: fresh-message run with un-drained pending executor
  messages from a prior run is invalid.
- _runner.py: clear _state before import_state in restore_from_checkpoint
  so restore is authoritative (import_state merges, not replaces).
- _agent.py: revert checkpoint branch to restore-only (no message forward).
- _responses.py (foundry_hosting): two-call host pattern - restore checkpoint
  silently, then run with new user input.
- tests: state-preservation is the new default; rebuild Workflow for clean slate.

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

* Fix CI lint and mypy issues from prior pivot commit

- _workflow.py: collapse nested if (SIM102), drop redundant assignment (RET504)
- _declarative_base.py: remove unused last_user_msg = tail assignment
  whose Message | None type clashed with the prior Message-typed branch

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

* Address PR review: fix Inputs.input update and checkpoint storage path

- _declarative_base.py: continuation branch was writing 'Inputs.input' via
  state.set, which routes to the Custom namespace and never updates the
  PowerFx-visible Workflow.Inputs.input. Update state_data['Inputs'] in
  place via get_state_data / set_state_data so =Workflow.Inputs.input and
  =inputs.input see the new turn's user text on continuation.
- _declarative_base.py: refresh docstring to clarify that on a list[Message]
  trigger, Conversation.messages excludes the current user message at the
  start of the turn (agent executors append it before invoking the inner
  agent).
- _responses.py: when previous_response_id is supplied (no conversation_id),
  the prior checkpoint lives under <storage>/<previous_response_id> but new
  checkpoints must land under <storage>/<current_response_id> for the next
  turn to find them. Hold onto restore_storage from the get_latest lookup
  and pass it to the restore-only run; pass write_storage (current id) to
  the message-delivery run and to checkpoint cleanup.

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

* Fix pyright errors in _declarative_base.py for CI

- Replace state._state.get(...) protected access with new public
  is_initialized() method on DeclarativeWorkflowState (also clearer intent
  for the continuation detection use case).
- Add narrow pyright ignores for the Any-typed trigger paths that pyright
  cannot fully narrow (the list[Message] isinstance loop and the
  fallback-DefaultTransform branch).

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

* Address Copilot review batch: tests + Workflow.reset escape hatch

* Add Workflow.reset() public method as recovery escape hatch when an
  in-flight run aborted (e.g. WorkflowConvergenceException) and the
  workflow is not checkpointed. Update the in-flight messages guard's
  error message to point callers at it.

* Add test_workflow_run_inflight_messages_guard exercising both the
  guard (sync + streaming) and the reset() recovery path.
* Add test_workflow_reset_rejects_concurrent_runs to lock down the
  in-progress guard on reset.

* Add test_as_agent_continuation_preserves_prior_state covering the
  is_continuation branch in _ensure_state_initialized: stamps a marker
  between calls and asserts it survives, while Inputs.input and
  System.LastMessageText refresh to the new turn.

* Add test_powerfx_safe.py regression tests for the Enum branch in
  _make_powerfx_safe (str-subclass, int-subclass, plain Enum, and
  Enums nested in dict/list).

* Drop redundant @pytest.mark.asyncio on
  test_as_agent_round_trip_with_last_message_text (asyncio_mode='auto').

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

* Skip restore-only pre-pass when checkpoint has pending request_info

Address Copilot review on _responses.py: the restore-only checkpoint
replay populates self._agent.pending_requests for any request_info
events captured in the checkpoint. The follow-up run(input_messages)
call would then route through WorkflowAgent._process_pending_requests,
which expects function-response content and rejects plain text input
as 'unexpected content while awaiting request info responses'.

Workflows resumed from a checkpoint that was idle-with-pending-requests
would therefore fail every subsequent plain-text user turn. Inspect the
loaded checkpoint and skip the pre-pass when its
pending_request_info_events dict is non-empty. Workflows that don't use
request_info (the current sample set) are unaffected; workflows that do
will fall through to a fresh-message run rather than silently corrupting
the routing state.

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

* Loosen azure-ai-agentserver-* pins to major version

The exact-version pins on azure-ai-agentserver-{core,responses,invocations}
forced foundry-hosting consumers to upgrade in lockstep with every beta
bump from upstream. Switch to '>=current,<next-major' so we pick up patch
and feature updates within the same major series without a coordinated
release.

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

* Drop Workflow.reset(); checkpointing is the recovery path

The in-flight-messages guard prevented silent misbehavior, but the
companion Workflow.reset() escape hatch only cleared _messages while
leaving iteration count, executor-local state, and shared State
mutations in an indeterminate condition after a mid-run failure. That
gave a false sense of recovery.

Recovery from a mid-run failure is supported only via checkpoint
restoration. Keep the guard and reframe its error message accordingly;
remove reset() and its tests.

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

* Address Tao's review on PR 5531

- Rename Workflow._run_workflow_with_tracing parameter
  is_fresh_message_run -> is_continuation (default False, inverted).
  Fresh-message turns reset per-run accounting; continuations
  (checkpoint restores, responses replays) preserve it.
- Simplify the in-flight-messages guard: _validate_run_params already
  enforces that 'message' is mutually exclusive with 'checkpoint_id'
  and 'responses', so the additional checks were dead code.
- foundry_hosting _responses: move the restore-only pre-pass above
  emit_created/emit_in_progress; restore is preparation, not run
  progress. Drop the skip-restore gate (state preservation requires
  unconditional restore) and instead clear agent.pending_requests
  after the restore-only call. Collapse over-conditioned check.

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

* Don't clear pending_requests after restore-only pre-pass

Pending requests in the restored checkpoint represent genuinely
outstanding HITL requests. The next user input may carry function
responses (Responses API `function_call_output` items become
FunctionResultContent / FunctionApprovalResponseContent), which
`WorkflowAgent._process_pending_requests` correctly extracts and
matches against the populated `pending_requests`. Clearing them
after restore would silently drop that state and force the next turn
to be treated as a fresh input even when the caller is responding to
the outstanding requests.

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

---------

Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
2026-04-29 00:51:49 +00:00
48 changed files with 1523 additions and 578 deletions
+17 -1
View File
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.2.2] - 2026-04-29
### Added
- **agent-framework-azure-contentunderstanding**: New alpha package — Azure AI Content Understanding context provider that auto-analyzes file attachments (documents, images, audio, video) and injects structured results into the LLM context, with multi-document session state, configurable timeout, output filtering via `AnalysisSection`, and auto-registered `list_documents` / `get_analyzed_document` tools ([#4829](https://github.com/microsoft/agent-framework/pull/4829))
- **agent-framework-foundry-hosting**: Add hosted Durable Workflow support — propagate full conversation history to workflow agents and wire `Workflow.as_agent()` end-to-end via the foundry hosting layer ([#5531](https://github.com/microsoft/agent-framework/pull/5531))
### Changed
- **agent-framework-orchestrations**: [BREAKING] Standardize orchestration terminal outputs as `AgentResponse` so `Workflow.as_agent()` returns the final answer only; aligns sequential-approval (`with_request_info`) and concurrent (`intermediate_outputs=True`) flows on the same output contract ([#5301](https://github.com/microsoft/agent-framework/pull/5301))
- **agent-framework-core**, **agent-framework-declarative**: Preserve `Workflow.run()` shared state across calls so multi-turn `WorkflowAgent` invocations retain context, accept `list[Message]` input in the declarative start executor, and coerce `Enum` values when serializing PowerFx symbols ([#5531](https://github.com/microsoft/agent-framework/pull/5531))
- **dependencies**: Update workspace package dependencies and preserve `mcp[ws]` / `uvicorn[standard]` extras through override-dependencies in `/python` ([#5555](https://github.com/microsoft/agent-framework/pull/5555))
### Fixed
- **agent-framework-core**: Fix observability spans not being correctly nested when using streaming ([#5552](https://github.com/microsoft/agent-framework/pull/5552))
- **agent-framework-openai**: Fix `file_search` citations breaking the assistant-message history roundtrip — skip `hosted_file` content in the assistant role so the Responses API no longer rejects `input_file` ([#5557](https://github.com/microsoft/agent-framework/pull/5557))
## [1.2.1] - 2026-04-28
### Added
@@ -1003,7 +1018,8 @@ 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.2.1...HEAD
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.2.2...HEAD
[1.2.2]: https://github.com/microsoft/agent-framework/compare/python-1.2.1...python-1.2.2
[1.2.1]: https://github.com/microsoft/agent-framework/compare/python-1.2.0...python-1.2.1
[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
+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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"azure-search-documents>=11.7.0b2,<11.7.0b3",
]
@@ -4,7 +4,7 @@ description = "Azure Content Understanding integration for Microsoft Agent Frame
authors = [{ name = "Microsoft", email = "af-support@microsoft.com" }]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0a260401"
version = "1.0.0a260429"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,9 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.0.0,<2",
"azure-ai-contentunderstanding>=1.0.0,<1.1",
"agent-framework-core>=1.2.2,<2",
"agent-framework-foundry>=1.2.2,<2",
"azure-ai-contentunderstanding>=1.0.1,<1.1",
"aiohttp>=3.9,<4",
"filetype>=1.2,<2",
]
@@ -15,12 +15,10 @@ import os
from pathlib import Path
from agent_framework import Agent, Content, Message
from agent_framework.foundry import FoundryChatClient
from agent_framework.foundry import ContentUnderstandingContextProvider, FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from agent_framework.foundry import ContentUnderstandingContextProvider
load_dotenv()
"""
@@ -15,12 +15,10 @@ import os
from pathlib import Path
from agent_framework import Agent, AgentSession, Content, Message
from agent_framework.foundry import FoundryChatClient
from agent_framework.foundry import ContentUnderstandingContextProvider, FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from agent_framework.foundry import ContentUnderstandingContextProvider
load_dotenv()
"""
@@ -16,12 +16,10 @@ import time
from pathlib import Path
from agent_framework import Agent, AgentSession, Content, Message
from agent_framework.foundry import FoundryChatClient
from agent_framework.foundry import ContentUnderstandingContextProvider, FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from agent_framework.foundry import ContentUnderstandingContextProvider
load_dotenv()
"""
@@ -16,13 +16,11 @@ import os
from pathlib import Path
from agent_framework import Agent, AgentSession, Content, Message
from agent_framework.foundry import FoundryChatClient
from agent_framework.foundry import ContentUnderstandingContextProvider, FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from agent_framework.foundry import ContentUnderstandingContextProvider
load_dotenv()
"""
@@ -21,13 +21,11 @@ Run with DevUI:
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.foundry import ContentUnderstandingContextProvider, FoundryChatClient
from azure.core.credentials import AzureKeyCredential
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from agent_framework.foundry import ContentUnderstandingContextProvider
load_dotenv()
# --- Auth ---
@@ -32,17 +32,16 @@ Run with DevUI:
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from agent_framework.foundry import (
ContentUnderstandingContextProvider,
FileSearchConfig,
FoundryChatClient,
)
from azure.core.credentials import AzureKeyCredential
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from openai import AzureOpenAI
from agent_framework.foundry import (
ContentUnderstandingContextProvider,
FileSearchConfig,
)
load_dotenv()
# --- Auth ---
+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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"microsoft-agents-copilotstudio-client>=0.3.1,<0.3.2",
]
+53 -7
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import base64
import contextlib
import json
import logging
import re
@@ -2890,6 +2891,7 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
self._inner_stream_source: ResponseStream[Any, Any] | Awaitable[ResponseStream[Any, Any]] | None = None
self._wrap_inner: bool = False
self._map_update: Callable[[Any], UpdateT | Awaitable[UpdateT]] | None = None
self._pull_context_manager_factories: list[Callable[[], contextlib.AbstractContextManager[Any]]] = []
def map(
self,
@@ -3008,11 +3010,18 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
return self
async def __anext__(self) -> UpdateT:
if self._iterator is None:
stream = await self._get_stream()
self._iterator = stream.__aiter__()
try:
update: UpdateT = await self._iterator.__anext__()
with contextlib.ExitStack() as stack:
for factory in self._pull_context_manager_factories:
stack.enter_context(factory())
# Resolve the underlying stream inside the pull contexts so that any
# spans/contexts created during stream resolution (e.g. inner chat
# completion spans created on the first pull of a wrapped agent stream)
# inherit the active context (e.g. an outer agent invoke span).
if self._iterator is None:
stream = await self._get_stream()
self._iterator = stream.__aiter__()
update: UpdateT = await self._iterator.__anext__()
except StopAsyncIteration:
self._consumed = True
await self._run_cleanup_hooks()
@@ -3038,9 +3047,25 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
update = hooked
return update
async def _resolve_stream_with_pull_contexts(self) -> AsyncIterable[UpdateT]:
"""Resolve the underlying stream while activating any registered pull context managers.
Used by ``__await__`` and ``get_final_response`` so that any spans/contexts created
during stream resolution (e.g. when the source is an Awaitable that internally
creates child telemetry spans) inherit the same active context as iterator pulls.
``__anext__`` resolves the stream inside its own ExitStack and so calls ``_get_stream``
directly.
"""
if self._stream is not None:
return await self._get_stream()
with contextlib.ExitStack() as stack:
for factory in self._pull_context_manager_factories:
stack.enter_context(factory())
return await self._get_stream()
def __await__(self) -> Any:
async def _wrap() -> ResponseStream[UpdateT, FinalT]:
await self._get_stream()
await self._resolve_stream_with_pull_contexts()
return self
return _wrap().__await__()
@@ -3064,10 +3089,12 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
"""
if self._wrap_inner:
if self._inner_stream is None:
# Use _get_stream() to resolve the awaitable - this properly handles
# Use _resolve_stream_with_pull_contexts() so that any spans/contexts
# created while resolving the awaitable (e.g. inner telemetry spans)
# inherit the same active context as iterator pulls. This also handles
# the case where _stream_source and _inner_stream_source are the same
# coroutine (e.g., from from_awaitable), avoiding double-await errors.
await self._get_stream()
await self._resolve_stream_with_pull_contexts()
if self._inner_stream is None:
raise RuntimeError("Inner stream not available")
if not self._finalized and not self._consumed:
@@ -3177,6 +3204,25 @@ class ResponseStream(AsyncIterable[UpdateT], Generic[UpdateT, FinalT]):
self._cleanup_hooks.append(hook)
return self
def with_pull_context_manager(
self,
cm_factory: Callable[[], contextlib.AbstractContextManager[Any]],
) -> ResponseStream[UpdateT, FinalT]:
"""Register a context manager factory invoked around each underlying iterator pull.
The factory is called once per ``__anext__`` and the returned context manager wraps
the await of the underlying iterator. This is useful for state that needs to be
active while the inner async work runs - for example, attaching an OpenTelemetry
span to the current context so child spans created by inner code (HTTP clients,
tool execution) are correctly parented.
Because the context manager is entered and exited within the same ``__anext__``
invocation, attach/detach style operations remain symmetric in the same async
context regardless of where the stream is iterated.
"""
self._pull_context_manager_factories.append(cm_factory)
return self
async def _run_cleanup_hooks(self) -> None:
if self._cleanup_run:
return
@@ -622,9 +622,7 @@ class Workflow(DictConvertible):
"checkpointing; there is no in-process recovery path."
)
initial_executor_fn = self._resolve_execution_mode(
message, responses, checkpoint_id, checkpoint_storage
)
initial_executor_fn = self._resolve_execution_mode(message, responses, checkpoint_id, checkpoint_storage)
async for event in self._run_workflow_with_tracing(
initial_executor_fn=initial_executor_fn,
@@ -724,9 +722,7 @@ class Workflow(DictConvertible):
initial_executor_fn = functools.partial(self._send_responses_internal, responses)
return initial_executor_fn
# Regular run or checkpoint restoration
return functools.partial(
self._execute_with_message_or_checkpoint, message, checkpoint_id, checkpoint_storage
)
return functools.partial(self._execute_with_message_or_checkpoint, message, checkpoint_id, checkpoint_storage)
async def _restore_and_send_responses(
self,
@@ -15,7 +15,10 @@ from typing import Any
_IMPORTS: dict[str, tuple[str, str]] = {
"AnalysisSection": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
"AnthropicFoundryClient": ("agent_framework_anthropic", "agent-framework-anthropic"),
"ContentUnderstandingContextProvider": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
"ContentUnderstandingContextProvider": (
"agent_framework_azure_contentunderstanding",
"agent-framework-azure-contentunderstanding",
),
"DocumentStatus": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
"FileSearchBackend": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
"FileSearchConfig": ("agent_framework_azure_contentunderstanding", "agent-framework-azure-contentunderstanding"),
@@ -4,12 +4,12 @@
# Install the relevant packages for full type support.
from agent_framework_anthropic import AnthropicFoundryClient, RawAnthropicFoundryClient
from agent_framework_azure_contentunderstanding import (
AnalysisSection,
ContentUnderstandingContextProvider,
DocumentStatus,
FileSearchBackend,
FileSearchConfig,
from agent_framework_azure_contentunderstanding import ( # pyright: ignore[reportMissingImports]
AnalysisSection, # pyright: ignore[reportUnknownVariableType]
ContentUnderstandingContextProvider, # pyright: ignore[reportUnknownVariableType]
DocumentStatus, # pyright: ignore[reportUnknownVariableType]
FileSearchBackend, # pyright: ignore[reportUnknownVariableType]
FileSearchConfig, # pyright: ignore[reportUnknownVariableType]
)
from agent_framework_foundry import (
FoundryAgent,
@@ -26,6 +26,7 @@ from time import perf_counter, time_ns
from typing import TYPE_CHECKING, Any, ClassVar, Final, Generic, Literal, TypedDict, cast, overload
from dotenv import load_dotenv
from opentelemetry import context as otel_context
from opentelemetry import metrics, trace
from . import __version__ as version_info
@@ -1277,27 +1278,8 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
)
if stream:
result_stream = cast(
ResponseStream[ChatResponseUpdate, ChatResponse[Any]],
super_get_response(
messages=messages,
stream=True,
options=opts,
compaction_strategy=compaction_strategy,
tokenizer=tokenizer,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=merged_client_kwargs,
),
)
span = _start_streaming_span(attributes, OtelAttr.REQUEST_MODEL)
# Create span directly without trace.use_span() context attachment.
# Streaming spans are closed asynchronously in cleanup hooks, which run
# in a different async context than creation — using use_span() would
# cause "Failed to detach context" errors from OpenTelemetry.
operation = attributes.get(OtelAttr.OPERATION, "operation")
span_name = attributes.get(OtelAttr.REQUEST_MODEL, "unknown")
span = get_tracer().start_span(f"{operation} {span_name}")
span.set_attributes(attributes)
if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages:
_capture_messages(
span=span,
@@ -1319,6 +1301,24 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
def _record_duration() -> None:
duration_state["duration"] = perf_counter() - start_time
try:
result_stream = cast(
ResponseStream[ChatResponseUpdate, ChatResponse[Any]],
super_get_response(
messages=messages,
stream=True,
options=opts,
compaction_strategy=compaction_strategy,
tokenizer=tokenizer,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=merged_client_kwargs,
),
)
except Exception as exception:
capture_exception(span=span, exception=exception, timestamp=time_ns())
_close_span()
raise
async def _finalize_stream() -> None:
from ._types import ChatResponse
@@ -1357,11 +1357,18 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
finally:
_close_span()
# Register a weak reference callback to close the span if stream is garbage collected
# without being consumed. This ensures spans don't leak if users don't consume streams.
wrapped_stream: ResponseStream[ChatResponseUpdate, ChatResponse[Any]] = result_stream.with_cleanup_hook(
_record_duration
).with_cleanup_hook(_finalize_stream)
# The pull context manager attaches the span around each underlying iterator pull so
# that child spans created during the pull (e.g. HTTP requests, inner tool execution)
# are parented under this chat span. Attach and detach happen in the same async
# context as the pull, avoiding cross-context cleanup issues. The weakref finalizer
# ensures the span is closed even if the stream is garbage collected without being
# consumed.
wrapped_stream: ResponseStream[ChatResponseUpdate, ChatResponse[Any]] = (
result_stream
.with_cleanup_hook(_record_duration)
.with_cleanup_hook(_finalize_stream)
.with_pull_context_manager(lambda: _activate_span(span))
)
weakref.finalize(wrapped_stream, _close_span)
return wrapped_stream
@@ -1543,23 +1550,8 @@ class AgentTelemetryLayer:
inner_accumulated_usage_token = INNER_ACCUMULATED_USAGE.set({})
if stream:
try:
run_result: object = execute()
if isinstance(run_result, ResponseStream):
result_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = run_result # pyright: ignore[reportUnknownVariableType]
elif isinstance(run_result, Awaitable):
result_stream = ResponseStream.from_awaitable(run_result) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
else:
raise RuntimeError("Streaming telemetry requires a ResponseStream result.")
except Exception:
INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS.reset(inner_response_telemetry_captured_fields_token)
INNER_ACCUMULATED_USAGE.reset(inner_accumulated_usage_token)
raise
span = _start_streaming_span(attributes, OtelAttr.AGENT_NAME)
operation = attributes.get(OtelAttr.OPERATION, "operation")
span_name = attributes.get(OtelAttr.AGENT_NAME, "unknown")
span = get_tracer().start_span(f"{operation} {span_name}")
span.set_attributes(attributes)
if OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED and messages:
_capture_messages(
span=span,
@@ -1581,6 +1573,21 @@ class AgentTelemetryLayer:
def _record_duration() -> None:
duration_state["duration"] = perf_counter() - start_time
try:
run_result: object = execute()
if isinstance(run_result, ResponseStream):
result_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = run_result # pyright: ignore[reportUnknownVariableType]
elif isinstance(run_result, Awaitable):
result_stream = ResponseStream.from_awaitable(run_result) # type: ignore[arg-type] # pyright: ignore[reportArgumentType]
else:
raise RuntimeError("Streaming telemetry requires a ResponseStream result.")
except Exception as exception:
capture_exception(span=span, exception=exception, timestamp=time_ns())
INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS.reset(inner_response_telemetry_captured_fields_token)
INNER_ACCUMULATED_USAGE.reset(inner_accumulated_usage_token)
_close_span()
raise
async def _finalize_stream() -> None:
from ._types import AgentResponse
@@ -1620,9 +1627,18 @@ class AgentTelemetryLayer:
INNER_ACCUMULATED_USAGE.reset(inner_accumulated_usage_token)
_close_span()
wrapped_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = result_stream.with_cleanup_hook(
_record_duration
).with_cleanup_hook(_finalize_stream)
# The pull context manager attaches the span around each underlying iterator pull so
# that child spans created during the pull (e.g. inner chat completion spans from the
# underlying ChatTelemetryLayer) are parented under this agent invoke span. Attach and
# detach happen in the same async context as the pull, avoiding cross-context cleanup
# issues. The weakref finalizer ensures the span is closed even if the stream is
# garbage collected without being consumed.
wrapped_stream: ResponseStream[AgentResponseUpdate, AgentResponse[Any]] = (
result_stream
.with_cleanup_hook(_record_duration)
.with_cleanup_hook(_finalize_stream)
.with_pull_context_manager(lambda: _activate_span(span))
)
weakref.finalize(wrapped_stream, _close_span)
return wrapped_stream
@@ -1809,6 +1825,27 @@ def get_function_span(
)
@contextlib.contextmanager
def _activate_span(span: trace.Span) -> Generator[None]:
"""Attach ``span`` as the current span in the OpenTelemetry context.
Designed to be used as a per-pull context manager registered on a
``ResponseStream`` via ``with_pull_context_manager``: it attaches the span
before each underlying iterator pull and detaches immediately after, so
child spans created during the pull (HTTP clients, inner chat completions,
tool execution) are correctly parented under ``span``.
Because attach and detach happen within the same ``__anext__`` invocation
(and therefore the same async task / contextvars context), there is no risk
of "Failed to detach context" warnings from cross-context cleanup.
"""
token = otel_context.attach(trace.set_span_in_context(span))
try:
yield
finally:
otel_context.detach(token)
@contextlib.contextmanager
def _get_span(
attributes: dict[str, Any],
@@ -1831,6 +1868,29 @@ def _get_span(
yield current_span
def _start_streaming_span(attributes: dict[str, Any], span_name_attribute: str) -> trace.Span:
"""Start a non-current span for a streaming operation.
Unlike :func:`_get_span`, the returned span is not attached to the current
OpenTelemetry context. The caller is responsible for:
- Ending the span via cleanup hooks on the wrapped
:class:`~agent_framework._types.ResponseStream`.
- Activating the span around each iterator pull via
:func:`_activate_span` registered with ``with_pull_context_manager`` so
that child spans created during stream production inherit it as parent.
Streaming spans are closed asynchronously in cleanup hooks that run in a
different async context than creation, so attaching the span at creation
time would cause "Failed to detach context" errors from OpenTelemetry.
"""
operation = attributes.get(OtelAttr.OPERATION, "operation")
span_name = attributes.get(span_name_attribute, "unknown")
span = get_tracer().start_span(f"{operation} {span_name}")
span.set_attributes(attributes)
return span
def _get_instructions_from_options(options: Any) -> str | list[str] | None:
"""Extract instructions from options dict."""
if options is None:
+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.2.1"
version = "1.2.2"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -3313,3 +3313,487 @@ async def test_agent_invoke_span_aggregates_usage_on_max_iterations_exhaustion(s
# The invoke_agent span must aggregate usage from the in-loop call and the final exhaustion call
assert agent_span.attributes.get(OtelAttr.INPUT_TOKENS) == 500
assert agent_span.attributes.get(OtelAttr.OUTPUT_TOKENS) == 100
# region Test span nesting (parent-child relationships)
@pytest.mark.parametrize("stream", [False, True])
async def test_chat_span_nested_under_agent_span(span_exporter: InMemorySpanExporter, stream: bool):
"""The inner chat span must be a child of the outer agent invoke span."""
class NestedChatClient(ChatTelemetryLayer, BaseChatClient[Any]):
def service_url(self):
return "https://test.example.com"
def _inner_get_response(
self, *, messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
if stream:
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
yield ChatResponseUpdate(contents=[Content.from_text("Hello")], role="assistant")
yield ChatResponseUpdate(
contents=[Content.from_text(" world")], role="assistant", finish_reason="stop"
)
def _finalize(updates: Sequence[ChatResponseUpdate]) -> ChatResponse:
return ChatResponse(
messages=[Message(role="assistant", contents=["Hello world"])],
response_id="resp_1",
usage_details=UsageDetails(input_token_count=3, output_token_count=4),
finish_reason="stop",
)
return ResponseStream(_stream(), finalizer=_finalize)
async def _get() -> ChatResponse:
return ChatResponse(
messages=[Message(role="assistant", contents=["Hello world"])],
response_id="resp_1",
usage_details=UsageDetails(input_token_count=3, output_token_count=4),
finish_reason="stop",
)
return _get()
agent = Agent(
client=NestedChatClient(),
id="nested_agent_id",
name="nested_agent",
default_options={"model": "NestedModel"},
)
span_exporter.clear()
if stream:
result_stream = agent.run("Test message", stream=True)
async for _ in result_stream:
pass
await result_stream.get_final_response()
else:
await agent.run("Test message")
spans = span_exporter.get_finished_spans()
assert len(spans) == 2
span_by_op = {s.attributes[OtelAttr.OPERATION.value]: s for s in spans}
agent_span = span_by_op[OtelAttr.AGENT_INVOKE_OPERATION]
chat_span = span_by_op[OtelAttr.CHAT_COMPLETION_OPERATION]
# Agent span has no parent (it is the root)
assert agent_span.parent is None
# Chat span's parent must be the agent span
assert chat_span.parent is not None
assert chat_span.parent.span_id == agent_span.context.span_id
assert chat_span.parent.trace_id == agent_span.context.trace_id
# Both spans must share the same trace
assert chat_span.context.trace_id == agent_span.context.trace_id
@pytest.mark.parametrize("stream", [False, True])
async def test_function_call_spans_nested_under_agent_span(span_exporter: InMemorySpanExporter, stream: bool):
"""All inner spans (chat completions and execute_tool) must be children of the agent span."""
from agent_framework import Content
from agent_framework._tools import FunctionInvocationLayer
@tool(name="get_weather", description="Get the weather for a location")
def get_weather(location: str) -> str:
return f"The weather in {location} is sunny."
class NestedToolChatClient(FunctionInvocationLayer, ChatTelemetryLayer, BaseChatClient[Any]):
def __init__(self) -> None:
super().__init__()
self.call_count = 0
def service_url(self):
return "https://test.example.com"
def _inner_get_response(
self, *, messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
self.call_count += 1
is_first = self.call_count == 1
if stream:
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
if is_first:
yield ChatResponseUpdate(
contents=[
Content.from_function_call(
call_id="call_123",
name="get_weather",
arguments='{"location": "Seattle"}',
)
],
role="assistant",
)
else:
yield ChatResponseUpdate(
contents=[Content.from_text("The weather in Seattle is sunny!")],
role="assistant",
finish_reason="stop",
)
def _finalize(updates: Sequence[ChatResponseUpdate]) -> ChatResponse:
return ChatResponse.from_updates(updates)
return ResponseStream(_stream(), finalizer=_finalize)
async def _get() -> ChatResponse:
if is_first:
return ChatResponse(
messages=[
Message(
role="assistant",
contents=[
Content.from_function_call(
call_id="call_123",
name="get_weather",
arguments='{"location": "Seattle"}',
)
],
)
],
)
return ChatResponse(
messages=[Message(role="assistant", contents=["The weather in Seattle is sunny!"])],
finish_reason="stop",
)
return _get()
agent = Agent(
client=NestedToolChatClient(),
id="tool_agent_id",
name="tool_agent",
default_options={"model": "ToolModel", "tools": [get_weather], "tool_choice": "auto"},
)
span_exporter.clear()
if stream:
result_stream = agent.run("What's the weather in Seattle?", stream=True)
async for _ in result_stream:
pass
await result_stream.get_final_response()
else:
await agent.run("What's the weather in Seattle?")
spans = span_exporter.get_finished_spans()
invoke_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.AGENT_INVOKE_OPERATION]
chat_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.CHAT_COMPLETION_OPERATION]
tool_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.TOOL_EXECUTION_OPERATION]
assert len(invoke_spans) == 1, f"Expected 1 invoke_agent span, got {len(invoke_spans)}"
assert len(chat_spans) == 2, f"Expected 2 chat spans, got {len(chat_spans)}"
assert len(tool_spans) == 1, f"Expected 1 execute_tool span, got {len(tool_spans)}"
agent_span = invoke_spans[0]
assert agent_span.parent is None
# All inner spans must be parented under the agent invoke span
for inner in (*chat_spans, *tool_spans):
assert inner.parent is not None, f"Span {inner.name} has no parent"
assert inner.parent.span_id == agent_span.context.span_id, (
f"Span {inner.name} parent={inner.parent.span_id} != agent={agent_span.context.span_id}"
)
assert inner.context.trace_id == agent_span.context.trace_id
@pytest.mark.parametrize("stream", [False, True])
async def test_chat_span_nested_under_explicit_outer_span(
span_exporter: InMemorySpanExporter, mock_chat_client, stream: bool
):
"""Chat telemetry spans (including streaming) must inherit a user-provided outer span as parent."""
from agent_framework.observability import get_tracer
client = mock_chat_client()
span_exporter.clear()
tracer = get_tracer()
with tracer.start_as_current_span("outer") as outer_span:
outer_ctx = outer_span.get_span_context()
if stream:
stream_obj = client.get_response(
stream=True, messages=[Message(role="user", contents=["Test"])], options={"model": "Test"}
)
async for _ in stream_obj:
pass
await stream_obj.get_final_response()
else:
await client.get_response(messages=[Message(role="user", contents=["Test"])], options={"model": "Test"})
spans = span_exporter.get_finished_spans()
chat_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.CHAT_COMPLETION_OPERATION]
assert len(chat_spans) == 1
chat_span = chat_spans[0]
assert chat_span.parent is not None
assert chat_span.parent.span_id == outer_ctx.span_id
assert chat_span.context.trace_id == outer_ctx.trace_id
@pytest.mark.parametrize("stream", [False, True])
async def test_http_span_nested_under_chat_span(span_exporter: InMemorySpanExporter, stream: bool):
"""A span created inside ``_inner_get_response`` (e.g. an HTTP client call to the LLM provider)
must be parented under the chat completion span.
This validates that the chat span context is active while the inner client implementation
runs, both for non-streaming responses and while streaming updates are being pulled.
"""
from agent_framework.observability import get_tracer
tracer = get_tracer()
class HttpEmittingClient(ChatTelemetryLayer, BaseChatClient[Any]):
def service_url(self):
return "https://test.example.com"
def _inner_get_response(
self, *, messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
if stream:
async def _stream() -> AsyncIterable[ChatResponseUpdate]:
# Simulate an HTTP request to the model provider while producing the stream.
with tracer.start_as_current_span("HTTP POST"):
pass
yield ChatResponseUpdate(contents=[Content.from_text("hi")], role="assistant", finish_reason="stop")
def _finalize(updates: Sequence[ChatResponseUpdate]) -> ChatResponse:
return ChatResponse.from_updates(updates)
return ResponseStream(_stream(), finalizer=_finalize)
async def _get() -> ChatResponse:
# Simulate an HTTP request to the model provider during the call.
with tracer.start_as_current_span("HTTP POST"):
pass
return ChatResponse(
messages=[Message(role="assistant", contents=["done"])],
usage_details=UsageDetails(input_token_count=1, output_token_count=1),
)
return _get()
span_exporter.clear()
client = HttpEmittingClient()
if stream:
result_stream = client.get_response(
stream=True, messages=[Message(role="user", contents=["Test"])], options={"model": "Test"}
)
async for _ in result_stream:
pass
await result_stream.get_final_response()
else:
await client.get_response(messages=[Message(role="user", contents=["Test"])], options={"model": "Test"})
spans = span_exporter.get_finished_spans()
chat_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.CHAT_COMPLETION_OPERATION]
http_spans = [s for s in spans if s.name == "HTTP POST"]
assert len(chat_spans) == 1
assert len(http_spans) == 1
chat_span = chat_spans[0]
http_span = http_spans[0]
assert http_span.parent is not None
assert http_span.parent.span_id == chat_span.context.span_id
assert http_span.context.trace_id == chat_span.context.trace_id
# region Test ResponseStream.with_pull_context_manager
async def test_with_pull_context_manager_enters_and_exits_per_pull():
"""The registered factory is entered and exited symmetrically around each iterator pull."""
import contextlib
events: list[str] = []
@contextlib.contextmanager
def cm():
events.append("enter")
try:
yield
finally:
events.append("exit")
async def src() -> AsyncIterable[int]:
yield 1
yield 2
stream: ResponseStream[int, list[int]] = ResponseStream(src(), finalizer=lambda updates: list(updates))
stream.with_pull_context_manager(cm)
pulled = [u async for u in stream]
assert pulled == [1, 2]
# Enter/exit must be balanced and there must be at least one pair per yielded update.
assert events.count("enter") == events.count("exit")
assert events.count("enter") >= 2
# Verify symmetric ordering (no overlapping pairs).
for i in range(0, len(events), 2):
assert events[i] == "enter"
assert events[i + 1] == "exit"
async def test_with_pull_context_manager_exits_on_iteration_error():
"""The pull context is exited even when the underlying stream raises mid-iteration."""
import contextlib
events: list[str] = []
@contextlib.contextmanager
def cm():
events.append("enter")
try:
yield
finally:
events.append("exit")
async def src() -> AsyncIterable[int]:
yield 1
raise RuntimeError("boom")
stream: ResponseStream[int, list[int]] = ResponseStream(src(), finalizer=lambda updates: list(updates))
stream.with_pull_context_manager(cm)
with pytest.raises(RuntimeError, match="boom"):
async for _ in stream:
pass
# Enter/exit balanced even on the failing pull.
assert events.count("enter") == events.count("exit")
assert events.count("enter") >= 2
async def test_with_pull_context_manager_wraps_stream_resolution_via_await():
"""Awaiting a ``from_awaitable`` stream resolves the inner stream under the pull contexts."""
import contextlib
events: list[str] = []
@contextlib.contextmanager
def cm():
events.append("enter")
try:
yield
finally:
events.append("exit")
async def inner() -> AsyncIterable[int]:
yield 1
async def make_stream() -> ResponseStream[int, list[int]]:
# Record that we resolve while a pull context is active.
events.append("resolving")
return ResponseStream(inner(), finalizer=lambda updates: list(updates))
stream: ResponseStream[int, list[int]] = ResponseStream.from_awaitable(make_stream())
stream.with_pull_context_manager(cm)
await stream # Triggers _resolve_stream_with_pull_contexts via __await__
assert "resolving" in events
resolve_index = events.index("resolving")
assert events[resolve_index - 1] == "enter" # Pull context active during resolution
# region Test streaming telemetry error paths
@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True)
async def test_chat_streaming_super_failure_closes_span(span_exporter: InMemorySpanExporter, enable_sensitive_data):
"""If the underlying client raises synchronously when constructing the stream, the chat
span is ended and the exception is recorded (no span leak)."""
class FailingClient(ChatTelemetryLayer, BaseChatClient[Any]):
def service_url(self):
return "https://test.example.com"
def _inner_get_response(
self, *, messages: MutableSequence[Message], stream: bool, options: dict[str, Any], **kwargs: Any
) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]:
raise RuntimeError("inner failed")
span_exporter.clear()
client = FailingClient()
with pytest.raises(RuntimeError, match="inner failed"):
client.get_response(stream=True, messages=[Message(role="user", contents=["Test"])], options={"model": "Test"})
spans = span_exporter.get_finished_spans()
chat_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.CHAT_COMPLETION_OPERATION]
assert len(chat_spans) == 1
assert chat_spans[0].status.status_code == StatusCode.ERROR
@pytest.mark.parametrize("enable_sensitive_data", [True], indirect=True)
async def test_agent_streaming_execute_failure_closes_span_and_resets_contextvars(
span_exporter: InMemorySpanExporter, enable_sensitive_data
):
"""If ``execute()`` raises synchronously during streaming agent invocation, the agent span is
ended, the exception is recorded, and the telemetry contextvars are reset."""
from agent_framework.observability import (
INNER_ACCUMULATED_USAGE,
INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS,
)
class _FailingExecuteAgent:
AGENT_PROVIDER_NAME = "test_provider"
def __init__(self):
self._id = "failing_execute"
self._name = "Failing Execute"
self._description = "Agent whose stream call raises synchronously"
self._default_options: dict[str, Any] = {}
@property
def id(self):
return self._id
@property
def name(self):
return self._name
@property
def description(self):
return self._description
@property
def default_options(self):
return self._default_options
def run(self, messages=None, *, stream: bool = False, session=None, **kwargs):
if stream:
raise RuntimeError("execute failed")
raise NotImplementedError
class FailingExecuteAgent(AgentTelemetryLayer, _FailingExecuteAgent):
pass
# Sentinel values to detect that contextvars were reset to their pre-call state.
sentinel_fields: set[str] = set()
sentinel_usage: dict[str, Any] = {}
fields_token = INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS.set(sentinel_fields)
usage_token = INNER_ACCUMULATED_USAGE.set(sentinel_usage)
try:
agent = FailingExecuteAgent()
span_exporter.clear()
with pytest.raises(RuntimeError, match="execute failed"):
agent.run(messages="Hello", stream=True)
# Contextvars must be back to the sentinel values registered before the call.
assert INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS.get() is sentinel_fields
assert INNER_ACCUMULATED_USAGE.get() is sentinel_usage
finally:
INNER_ACCUMULATED_USAGE.reset(usage_token)
INNER_RESPONSE_TELEMETRY_CAPTURED_FIELDS.reset(fields_token)
spans = span_exporter.get_finished_spans()
agent_spans = [s for s in spans if s.attributes.get(OtelAttr.OPERATION.value) == OtelAttr.AGENT_INVOKE_OPERATION]
assert len(agent_spans) == 1
assert agent_spans[0].status.status_code == StatusCode.ERROR
@@ -959,16 +959,12 @@ class DeclarativeActionExecutor(Executor):
last_user_msg = messages_list[last_user_index]
last_user_text = last_user_msg.text or ""
last_user_id = getattr(last_user_msg, "message_id", "") or ""
history_messages = (
messages_list[:last_user_index] + messages_list[last_user_index + 1:]
)
history_messages = messages_list[:last_user_index] + messages_list[last_user_index + 1 :]
else:
history_messages = list(messages_list)
tail = messages_list[-1] if messages_list else None
last_user_text = (tail.text or "") if tail is not None else ""
last_user_id = (
getattr(tail, "message_id", "") or "" if tail is not None else ""
)
last_user_id = getattr(tail, "message_id", "") or "" if tail is not None else ""
if is_continuation:
# Continuation turn: keep prior Conversation.messages intact.
+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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"powerfx>=0.0.32,<0.0.35; python_version < '3.14'",
"pyyaml>=6.0,<7.0",
]
@@ -284,17 +284,13 @@ actions:
agent = workflow.as_agent(name="continuation-agent")
first = await agent.run("turn-1-msg")
assert first.text == "turn-1-msg", (
f"Expected turn-1 echo 'turn-1-msg', got: {first.text!r}"
)
assert first.text == "turn-1-msg", f"Expected turn-1 echo 'turn-1-msg', got: {first.text!r}"
# Stamp a marker into the declarative state between turns. The
# continuation branch must preserve it; a state-clearing run would
# wipe ``DECLARATIVE_STATE_KEY`` and force re-initialization.
state_data = workflow._state.get(DECLARATIVE_STATE_KEY)
assert isinstance(state_data, dict), (
"Expected declarative state to be initialized after turn 1"
)
assert isinstance(state_data, dict), "Expected declarative state to be initialized after turn 1"
state_data["Local"] = {"persisted_marker": "kept-from-turn-1"}
workflow._state.set(DECLARATIVE_STATE_KEY, state_data)
workflow._state.commit()
+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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"openai>=1.99.0,<3",
"opentelemetry-sdk>=1.39.0,<2",
"fastapi>=0.115.0,<0.133.1",
+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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"durabletask>=1.3.0,<2",
"durabletask-azuremanaged>=1.3.0,<2",
"python-dateutil>=2.8.0,<3",
+3 -3
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.2.1"
version = "1.2.2"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,8 +23,8 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.2.1,<2",
"agent-framework-openai>=1.1.0,<2",
"agent-framework-core>=1.2.2,<2",
"agent-framework-openai>=1.2.2,<2",
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
"azure-ai-projects>=2.1.0,<3.0",
]
@@ -4,7 +4,7 @@ description = "Foundry Hosting integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0a260428"
version = "1.0.0a260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"azure-ai-agentserver-core>=2.0.0b3,<3",
"azure-ai-agentserver-responses>=1.0.0b5,<2",
"azure-ai-agentserver-invocations>=1.0.0b3,<2",
+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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0a260428"
version = "1.0.0a260429"
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.2.1,<2.0",
"agent-framework-core>=1.2.2,<2.0",
"google-genai>=1.65.0,<2.0.0",
]
@@ -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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"github-copilot-sdk>=0.2.1,<=0.2.1; python_version >= '3.11'",
]
+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.0a260428"
version = "1.0.0a260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"ollama>=0.5.3,<0.5.4",
]
@@ -241,6 +241,85 @@ OpenAIChatOptionsT = TypeVar(
# endregion
# region Helpers
def _annotations_to_output_text(annotations: Sequence[Annotation] | None) -> list[dict[str, Any]]:
"""Convert framework `Annotation` objects to Responses API `output_text` annotation dicts.
Citations from `file_search`, `code_interpreter` file paths, and url citations all collapse
to `Annotation(type="citation", ...)` in the framework. The original API form is recovered
here so assistant messages roundtrip cleanly through history forwarding.
Each Responses API annotation dict carries at most one `start_index`/`end_index` pair, so an
`Annotation` with multiple `annotated_regions` is fanned out into one entry per region.
Regions missing valid integer span bounds are skipped.
"""
if not annotations:
return []
out: list[dict[str, Any]] = []
for annotation in annotations:
if annotation.get("type") != "citation":
continue
props = annotation.get("additional_properties") or {}
regions = annotation.get("annotated_regions") or []
file_id = annotation.get("file_id")
url = annotation.get("url")
title = annotation.get("title")
container_id = props.get("container_id")
if container_id and file_id:
for region in regions:
start = region.get("start_index")
end = region.get("end_index")
if not (isinstance(start, int) and isinstance(end, int)):
continue
entry: dict[str, Any] = {
"type": "container_file_citation",
"container_id": container_id,
"file_id": file_id,
"start_index": start,
"end_index": end,
}
if url:
entry["filename"] = url
out.append(entry)
elif url and not file_id and regions:
for region in regions:
start = region.get("start_index")
end = region.get("end_index")
if not (isinstance(start, int) and isinstance(end, int)):
continue
out.append({
"type": "url_citation",
"url": url,
"title": title or "",
"start_index": start,
"end_index": end,
})
elif file_id and url:
entry = {
"type": "file_citation",
"file_id": file_id,
"filename": url,
}
if (idx := props.get("index")) is not None:
entry["index"] = idx
out.append(entry)
elif file_id:
entry = {
"type": "file_path",
"file_id": file_id,
}
if (idx := props.get("index")) is not None:
entry["index"] = idx
out.append(entry)
return out
# endregion
# region ResponsesClient
@@ -1374,7 +1453,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
return {
"type": "output_text",
"text": content.text,
"annotations": [],
"annotations": _annotations_to_output_text(getattr(content, "annotations", None)),
}
return {
"type": "input_text",
@@ -1522,6 +1601,13 @@ class RawOpenAIChatClient( # type: ignore[misc]
"approve": content.approved,
}
case "hosted_file":
# `input_file` is an input-only content type in the Responses API and is rejected
# inside an assistant message. Hosted-file content on an assistant message
# represents a citation produced by a hosted tool (e.g., file_search) and cannot be
# meaningfully replayed as input — drop it. The accompanying text annotations carry
# the citation context for round-tripping.
if role == "assistant":
return {}
return {
"type": "input_file",
"file_id": content.file_id,
@@ -2502,45 +2588,63 @@ class RawOpenAIChatClient( # type: ignore[misc]
ann_type = _get_ann_value("type")
ann_file_id = _get_ann_value("file_id")
# Hosted-file citations attach as text annotations (matching the non-streaming path)
# so they don't roundtrip as standalone `input_file` items in assistant history.
if ann_type == "file_path":
if ann_file_id:
annotation_obj = Annotation(
type="citation",
file_id=str(ann_file_id),
additional_properties={
"annotation_index": event.annotation_index,
"index": _get_ann_value("index"),
},
raw_representation=annotation,
)
contents.append(
Content.from_hosted_file(
file_id=str(ann_file_id),
additional_properties={
"annotation_index": event.annotation_index,
"index": _get_ann_value("index"),
},
raw_representation=event,
)
Content.from_text(text="", annotations=[annotation_obj], raw_representation=event)
)
elif ann_type == "file_citation":
if ann_file_id:
ann_filename = _get_ann_value("filename")
annotation_obj = Annotation(
type="citation",
file_id=str(ann_file_id),
url=ann_filename,
additional_properties={
"annotation_index": event.annotation_index,
"index": _get_ann_value("index"),
},
raw_representation=annotation,
)
contents.append(
Content.from_hosted_file(
file_id=str(ann_file_id),
additional_properties={
"annotation_index": event.annotation_index,
"filename": _get_ann_value("filename"),
"index": _get_ann_value("index"),
},
raw_representation=event,
)
Content.from_text(text="", annotations=[annotation_obj], raw_representation=event)
)
elif ann_type == "container_file_citation":
if ann_file_id:
ann_filename = _get_ann_value("filename")
ann_start = _get_ann_value("start_index")
ann_end = _get_ann_value("end_index")
annotation_obj = Annotation(
type="citation",
file_id=str(ann_file_id),
url=ann_filename,
additional_properties={
"annotation_index": event.annotation_index,
"container_id": _get_ann_value("container_id"),
},
raw_representation=annotation,
)
if ann_start is not None and ann_end is not None:
annotation_obj["annotated_regions"] = [
TextSpanRegion(
type="text_span",
start_index=ann_start,
end_index=ann_end,
)
]
contents.append(
Content.from_hosted_file(
file_id=str(ann_file_id),
additional_properties={
"annotation_index": event.annotation_index,
"container_id": _get_ann_value("container_id"),
"filename": _get_ann_value("filename"),
"start_index": _get_ann_value("start_index"),
"end_index": _get_ann_value("end_index"),
},
raw_representation=event,
)
Content.from_text(text="", annotations=[annotation_obj], raw_representation=event)
)
elif ann_type == "url_citation":
ann_url = _get_ann_value("url")
+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.2.1"
version = "1.2.2"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"openai>=1.99.0,<3",
]
@@ -1914,6 +1914,285 @@ def test_hosted_file_content_preparation() -> None:
assert result["file_id"] == "file_abc123"
def test_assistant_text_preserves_citation_annotations_on_roundtrip() -> None:
"""Citation annotations on assistant text should survive serialization back to the Responses API.
Previously `output_text.annotations` was hardcoded to `[]`, silently dropping `file_search`
citation context on every roundtrip. Preserving them keeps citations intact across
multi-agent forwarding.
"""
from agent_framework._types import Annotation, TextSpanRegion
client = OpenAIChatClient(model="test-model", api_key="test-key")
text_content = Content.from_text(
"Per the docs, the answer is X. See also the report.",
annotations=[
Annotation(
type="citation",
file_id="file-abc123",
url="guidelines.md",
additional_properties={"index": 12},
),
Annotation(
type="citation",
title="Quarterly Report",
url="https://example.com/report",
annotated_regions=[TextSpanRegion(type="text_span", start_index=40, end_index=46)],
),
Annotation(
type="citation",
file_id="file-container456",
url="data.csv",
additional_properties={"container_id": "container-789"},
annotated_regions=[TextSpanRegion(type="text_span", start_index=0, end_index=3)],
),
],
)
result = client._prepare_content_for_openai("assistant", text_content)
assert result["type"] == "output_text"
annotations = result["annotations"]
assert len(annotations) == 3
file_citation = next(a for a in annotations if a["type"] == "file_citation")
assert file_citation["file_id"] == "file-abc123"
assert file_citation["filename"] == "guidelines.md"
assert file_citation["index"] == 12
url_citation = next(a for a in annotations if a["type"] == "url_citation")
assert url_citation["url"] == "https://example.com/report"
assert url_citation["title"] == "Quarterly Report"
assert url_citation["start_index"] == 40
assert url_citation["end_index"] == 46
container = next(a for a in annotations if a["type"] == "container_file_citation")
assert container["file_id"] == "file-container456"
assert container["container_id"] == "container-789"
assert container["filename"] == "data.csv"
assert container["start_index"] == 0
assert container["end_index"] == 3
def test_assistant_text_preserves_file_path_annotation() -> None:
"""A `file_path`-style citation (file_id only, no url) should serialize as `file_path`."""
from agent_framework._types import Annotation
client = OpenAIChatClient(model="test-model", api_key="test-key")
text_content = Content.from_text(
"See attached.",
annotations=[
Annotation(
type="citation",
file_id="file-only",
additional_properties={"index": 42},
),
],
)
result = client._prepare_content_for_openai("assistant", text_content)
assert result["type"] == "output_text"
annotations = result["annotations"]
assert annotations == [{"type": "file_path", "file_id": "file-only", "index": 42}]
def test_assistant_text_fans_out_multiple_annotated_regions() -> None:
"""A url_citation with multiple `annotated_regions` should emit one entry per region.
The Responses API annotation dict carries one start/end pair, so a framework Annotation
with N regions must produce N output annotation entries.
"""
from agent_framework._types import Annotation, TextSpanRegion
client = OpenAIChatClient(model="test-model", api_key="test-key")
text_content = Content.from_text(
"See report. The report says X. Also report.",
annotations=[
Annotation(
type="citation",
title="Report",
url="https://example.com/report",
annotated_regions=[
TextSpanRegion(type="text_span", start_index=4, end_index=10),
TextSpanRegion(type="text_span", start_index=16, end_index=22),
TextSpanRegion(type="text_span", start_index=36, end_index=42),
],
),
],
)
result = client._prepare_content_for_openai("assistant", text_content)
annotations = result["annotations"]
assert len(annotations) == 3
assert all(a["type"] == "url_citation" for a in annotations)
spans = [(a["start_index"], a["end_index"]) for a in annotations]
assert spans == [(4, 10), (16, 22), (36, 42)]
def test_assistant_text_skips_regions_with_invalid_span() -> None:
"""Regions missing integer start/end bounds are skipped rather than emitted with `None`."""
from agent_framework._types import Annotation, TextSpanRegion
client = OpenAIChatClient(model="test-model", api_key="test-key")
text_content = Content.from_text(
"See report.",
annotations=[
Annotation(
type="citation",
title="Report",
url="https://example.com/report",
annotated_regions=[
TextSpanRegion(type="text_span"), # type: ignore[typeddict-item]
TextSpanRegion(type="text_span", start_index=4, end_index=10),
],
),
],
)
result = client._prepare_content_for_openai("assistant", text_content)
annotations = result["annotations"]
assert len(annotations) == 1
assert annotations[0]["start_index"] == 4
assert annotations[0]["end_index"] == 10
def test_assistant_text_without_annotations_emits_empty_list() -> None:
"""Plain assistant text should still emit `annotations: []` (Azure validation requires the field)."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
result = client._prepare_content_for_openai("assistant", Content.from_text("hello"))
assert result["type"] == "output_text"
assert result["text"] == "hello"
assert result["annotations"] == []
def test_streamed_file_citation_coalesces_onto_surrounding_text() -> None:
"""Streamed citation events emit empty-text Content with annotations; `_finalize_response`
coalesces consecutive text contents and unions their annotations, so the citation lands on
the merged assistant text content (not a stray empty-text entry).
Without this, span indices in the annotation would reference `text == ""` after roundtrip.
"""
text_event = MagicMock()
text_event.type = "response.output_text.delta"
text_event.delta = "Hello world."
text_event.item_id = "item_1"
text_event.output_index = 0
text_event.content_index = 0
citation_event = MagicMock()
citation_event.type = "response.output_text.annotation.added"
citation_event.annotation_index = 0
citation_event.annotation = {
"type": "file_citation",
"file_id": "file-abc",
"filename": "guidelines.md",
"index": 5,
}
client = OpenAIChatClient(model="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
update1 = client._parse_chunk_from_openai(text_event, chat_options, function_call_ids)
update2 = client._parse_chunk_from_openai(citation_event, chat_options, function_call_ids)
response = ChatResponse.from_updates([update1, update2])
assert len(response.messages) == 1
contents = response.messages[0].contents
assert len(contents) == 1
merged = contents[0]
assert merged.type == "text"
assert merged.text == "Hello world."
assert merged.annotations is not None
assert len(merged.annotations) == 1
assert merged.annotations[0]["file_id"] == "file-abc"
def test_streamed_file_citation_roundtrips_as_assistant_history() -> None:
"""End-to-end: file_citation arrives via streaming, then gets forwarded as assistant history.
Reproduces the user-reported sequential/group-chat workflow bug where one agent's
`file_search` citations became `input_file` items in the next agent's request and were
rejected by the Responses API.
"""
client = OpenAIChatClient(model="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
text_event = MagicMock()
text_event.type = "response.output_text.delta"
text_event.delta = "According to the docs, the answer is X."
text_event.item_id = "item_1"
text_event.output_index = 0
text_event.content_index = 0
citation_event = MagicMock()
citation_event.type = "response.output_text.annotation.added"
citation_event.annotation_index = 0
citation_event.annotation = {
"type": "file_citation",
"file_id": "file-xyz789",
"filename": "guidelines.md",
"index": 12,
}
update1 = client._parse_chunk_from_openai(text_event, chat_options, function_call_ids)
update2 = client._parse_chunk_from_openai(citation_event, chat_options, function_call_ids)
assistant_history = Message(
role="assistant",
contents=[*update1.contents, *update2.contents],
)
prepared = client._prepare_message_for_openai(assistant_history)
assert len(prepared) == 1
content_items = prepared[0].get("content", [])
types = [c.get("type") for c in content_items]
assert "input_file" not in types, f"input_file leaked into assistant history: {types}"
output_text_items = [c for c in content_items if c.get("type") == "output_text"]
assert any(
any(a.get("type") == "file_citation" and a.get("file_id") == "file-xyz789" for a in c.get("annotations", []))
for c in output_text_items
), "file_citation annotation should survive the streaming → history roundtrip"
def test_hosted_file_in_assistant_message_does_not_emit_input_file() -> None:
"""Hosted file citations attached to an assistant message must not roundtrip as `input_file`.
The Responses API rejects `input_file` items inside an assistant role's content array;
`input_file` is an input-only content type. This guards the multi-agent / sequential workflow
case where one agent's `file_search` citations get forwarded as history to the next call.
"""
client = OpenAIChatClient(model="test-model", api_key="test-key")
assistant_msg = Message(
role="assistant",
contents=[
Content.from_text("According to the docs, the answer is X."),
Content.from_hosted_file(file_id="file_abc123"),
],
)
prepared = client._prepare_message_for_openai(assistant_msg)
assert len(prepared) == 1
assistant_item = prepared[0]
assert assistant_item["role"] == "assistant"
content_types = [c.get("type") for c in assistant_item.get("content", [])]
assert "input_file" not in content_types, (
f"`input_file` is not valid inside an assistant message; got {content_types}"
)
assert "output_text" in content_types
def test_function_approval_response_with_mcp_tool_call() -> None:
"""Test function approval response content with MCP server tool call content."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
@@ -2682,7 +2961,7 @@ def test_streaming_response_in_progress_type() -> None:
def test_streaming_annotation_added_with_file_path() -> None:
"""Test streaming annotation added event with file_path type extracts HostedFileContent."""
"""Streaming `file_path` should attach as a text annotation, matching non-streaming."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -2700,15 +2979,23 @@ def test_streaming_annotation_added_with_file_path() -> None:
assert len(response.contents) == 1
content = response.contents[0]
assert content.type == "hosted_file"
assert content.file_id == "file-abc123"
assert content.additional_properties is not None
assert content.additional_properties.get("annotation_index") == 0
assert content.additional_properties.get("index") == 42
assert content.type == "text"
assert content.annotations is not None
assert len(content.annotations) == 1
annotation = content.annotations[0]
assert annotation["type"] == "citation"
assert annotation["file_id"] == "file-abc123"
assert annotation["additional_properties"]["annotation_index"] == 0
assert annotation["additional_properties"]["index"] == 42
def test_streaming_annotation_added_with_file_citation() -> None:
"""Test streaming annotation added event with file_citation type extracts HostedFileContent."""
"""Streaming `file_citation` should attach as a text annotation, matching non-streaming.
Previously the streaming path produced a standalone `HostedFileContent`, which then
serialized as `input_file` in assistant history and was rejected by the Responses API.
Annotations on text content roundtrip cleanly.
"""
client = OpenAIChatClient(model="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -2727,15 +3014,19 @@ def test_streaming_annotation_added_with_file_citation() -> None:
assert len(response.contents) == 1
content = response.contents[0]
assert content.type == "hosted_file"
assert content.file_id == "file-xyz789"
assert content.additional_properties is not None
assert content.additional_properties.get("filename") == "sample.txt"
assert content.additional_properties.get("index") == 15
assert content.type == "text"
assert content.annotations is not None
assert len(content.annotations) == 1
annotation = content.annotations[0]
assert annotation["type"] == "citation"
assert annotation["file_id"] == "file-xyz789"
assert annotation["url"] == "sample.txt"
assert annotation["additional_properties"]["annotation_index"] == 1
assert annotation["additional_properties"]["index"] == 15
def test_streaming_annotation_added_with_container_file_citation() -> None:
"""Test streaming annotation added event with container_file_citation type."""
"""Streaming `container_file_citation` should attach as a text annotation."""
client = OpenAIChatClient(model="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
@@ -2756,13 +3047,19 @@ def test_streaming_annotation_added_with_container_file_citation() -> None:
assert len(response.contents) == 1
content = response.contents[0]
assert content.type == "hosted_file"
assert content.file_id == "file-container123"
assert content.additional_properties is not None
assert content.additional_properties.get("container_id") == "container-456"
assert content.additional_properties.get("filename") == "data.csv"
assert content.additional_properties.get("start_index") == 10
assert content.additional_properties.get("end_index") == 50
assert content.type == "text"
assert content.annotations is not None
assert len(content.annotations) == 1
annotation = content.annotations[0]
assert annotation["type"] == "citation"
assert annotation["file_id"] == "file-container123"
assert annotation["url"] == "data.csv"
assert annotation["additional_properties"]["container_id"] == "container-456"
assert annotation["annotated_regions"] is not None
assert len(annotation["annotated_regions"]) == 1
region = annotation["annotated_regions"][0]
assert region["start_index"] == 10
assert region["end_index"] == 50
def test_streaming_annotation_added_with_url_citation() -> None:
@@ -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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<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.0b260428"
version = "1.0.0b260429"
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.2.1,<2",
"agent-framework-core>=1.2.2,<2",
"redis>=6.4.0,<7.2.1",
"redisvl>=0.11.0,<0.16",
"numpy>=2.2.6,<3"
+5 -5
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.2.1"
version = "1.2.2"
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.2.1",
"agent-framework-core[all]==1.2.2",
]
[dependency-groups]
@@ -52,8 +52,9 @@ dev = [
[tool.uv]
package = false
prerelease = "if-necessary-or-explicit"
# Keep transitive litellm below the compromised 1.82.7/1.82.8 releases.
constraint-dependencies = ["litellm<1.82.7"]
# Security floors for transitive deps; overrides bypass litellm[proxy]'s strict pins.
constraint-dependencies = ["litellm>=1.83.7", "fastapi-sso>=0.19.0"]
override-dependencies = ["mcp[ws]>=1.27.0", "uvicorn[standard]>=0.34.0"]
environments = [
"sys_platform == 'darwin'",
"sys_platform == 'linux'",
@@ -93,7 +94,6 @@ agent-framework-orchestrations = { workspace = true }
agent-framework-purview = { workspace = true }
agent-framework-redis = { workspace = true }
agent-framework-azure-contentunderstanding = { workspace = true }
litellm = { url = "https://files.pythonhosted.org/packages/57/77/0c6eca2cb049793ddf8ce9cdcd5123a35666c4962514788c4fc90edf1d3b/litellm-1.82.1-py3-none-any.whl" }
[tool.ruff]
line-length = 120
@@ -1199,6 +1199,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.10",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
@@ -1345,19 +1358,6 @@
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1464,19 +1464,6 @@
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
}
}
}
+313 -343
View File
File diff suppressed because it is too large Load Diff