Merge and move scripts (#4308)

* .NET: Add Microsoft Fabric sample #3674 (#4230)

Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>

* Python: Phase 2: Embedding clients for Ollama, Bedrock, and Azure AI Inference (#4207)

* Phase 2: Embedding clients for Ollama, Bedrock, and Azure AI Inference

Add embedding client implementations to existing provider packages:

- OllamaEmbeddingClient: Text embeddings via Ollama's embed API
- BedrockEmbeddingClient: Text embeddings via Amazon Titan on Bedrock
- AzureAIInferenceEmbeddingClient: Text and image embeddings via Azure AI
  Inference, supporting Content | str input with separate model IDs for
  text (AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID) and image
  (AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID) endpoints

Additional changes:
- Rename EmbeddingCoT -> EmbeddingT, EmbeddingOptionsCoT -> EmbeddingOptionsT
- Add otel_provider_name passthrough to all embedding clients
- Register integration pytest marker in all packages
- Add lazy-loading namespace exports for Ollama and Bedrock embeddings
- Add image embedding sample using Cohere-embed-v3-english
- Add azure-ai-inference dependency to azure-ai package

Part of #1188

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

* Fix mypy duplicate name and ruff lint issues

- Rename second 'vector' variable to 'img_vector' in image embedding loop
- Combine nested with statements in tests
- Remove unused result assignments in tests

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

* updates from feedback

* Fix CI failures in embedding usage handling

- Fix Azure AI embedding mypy issues by normalizing vectors to list[float],
  safely accumulating optional usage token fields, and filtering None entries
  before constructing GeneratedEmbeddings
- Avoid Bandit false positive by initializing usage details as an empty dict
- Update OpenAI embedding tests to assert canonical usage keys
  (input_token_count/total_token_count)

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

---------

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

* [Purview] Mark responses as responses and fix epoch bug for python long overflow (#4225)

* .NET: Support InvokeMcpTool for declarative workflows (#4204)

* Initial implementation of InvokeMcpTool in declarative workflow

* Cleaned up sample implementation

* Updated sample comments.

* Added missing executor routing attribute

* Fix PR comments.

* Updated based on PR comments.

* Updated based on PR comments.

* Removed unnecessary using statement.

* Update Python package versions to rc2 (#4258)

- Bump core and azure-ai to 1.0.0rc2
- Bump preview packages to 1.0.0b260225
- Update dependencies to >=1.0.0rc2
- Add CHANGELOG entries for changes since rc1
- Update uv.lock

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

* .NET: Fixing issue where OpenTelemetry span is never exported in .NET in-process workflow execution (#4196)

* 1. Add reproduction test for issue #4155: workflow.run Activity never stopped in streaming OffThread path

The WorkflowRunActivity_IsStopped_Streaming_OffThread test demonstrates that
the workflow.run OpenTelemetry Activity created in StreamingRunEventStream.RunLoopAsync
is started but never stopped when using the OffThread/Default streaming execution.
The background run loop keeps running after event consumption completes, so the
using Activity? declaration never disposes until explicit StopAsync() is called.

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

2. Fix workflow.run Activity never stopped in streaming OffThread execution (#4155)

The workflow.run OpenTelemetry Activity in StreamingRunEventStream.RunLoopAsync
was scoped to the method lifetime via 'using'. Since the run loop only exits on
cancellation, the Activity was never stopped/exported until explicit disposal.

Fix: Remove 'using' and explicitly dispose the Activity when the workflow reaches
Idle status (all supersteps complete). A safety-net disposal in the finally block
handles cancellation and error paths.

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

* Add root-level workflow.session activity spanning run loop lifetime\n\nImplements two-level telemetry hierarchy per PR feedback from lokitoth:\n- workflow.session: spans the entire run loop / stream lifetime\n- workflow_invoke: per input-to-halt cycle, nested within the session\n\nThis ensures the session activity stays open across multiple turns,\nwhile individual run activities are created and disposed per cycle.\n\nAlso fixes linkedSource CancellationTokenSource disposal leak in\nStreamingRunEventStream (added using declaration)."

* Address Copilot review: fix Activity/CTS disposal, rename activity, add error tag\n\n1. LockstepRunEventStream: Remove 'using' from Activity in async iterator\n   and manually dispose in finally block (fixes #4155 pattern). Also dispose\n   linkedSource CTS in finally to prevent leak.\n2. Tags.cs: Add ErrorMessage (\"error.message\") tag for runtime errors,\n   distinct from BuildErrorMessage (\"build.error.message\").\n3. ActivityNames: Rename WorkflowRun from \"workflow_invoke\" to \"workflow.run\"\n   for cross-language consistency.\n4. WorkflowTelemetryContext: Fix XML doc to say \"outer/parent span\" instead\n   of \"root-level span\".\n5. ObservabilityTests: Assert WorkflowSession absence when DisableWorkflowRun\n   is true.\n6. WorkflowRunActivityStopTests: Fix streaming test race by disposing\n   StreamingRun before asserting activities are stopped.\n7. StreamingRunEventStream/LockstepRunEventStream: Use Tags.ErrorMessage\n   instead of Tags.BuildErrorMessage for runtime error events."

* Review fixes: revert workflow_invoke rename, use 'using' for linkedSource, move SessionStarted earlier\n\n- Revert ActivityNames.WorkflowRun back to \"workflow_invoke\" (OTEL semantic convention contract)\n- Use 'using' declaration for linkedSource CTS in LockstepRunEventStream (no timing sensitivity)\n- Move SessionStarted event before WaitForInputAsync in StreamingRunEventStream to match Lockstep behavior"

* Improve naming and comments in WorkflowRunActivityStopTests"

* Prevent session Activity.Current leak in lockstep mode, add nesting test

Save and restore Activity.Current in LockstepRunEventStream.Start() so the
session activity doesn't leak into caller code via AsyncLocal. Re-establish
Activity.Current = sessionActivity before creating the run activity in
TakeEventStreamAsync to preserve parent-child nesting.

Add test verifying app activities after RunAsync are not parented under the
session, and that the workflow_invoke activity nests under the session."

* Fix stale XML doc: WorkflowRun -> WorkflowInvoke in ObservabilityTests

---------

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

* Python / .NET Samples - Restructure and Improve Samples (Feature Branc… (#4092)

* Python: .NET Samples - Restructure and Improve Samples (Feature Branch) (#4091)

* Moved by agent (#4094)

* Fix readme links

* .NET Samples - Create `04-hosting` learning path step (#4098)

* Agent move

* Agent reorderd

* Remove A2A section from README 

Removed A2A section from the Getting Started README.

* Agent fixed links

* Fix broken sample links in durable-agents README (#4101)

* Initial plan

* Fix broken internal links in documentation

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Revert template link changes; keep only durable-agents README fix

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* .NET Samples - Create `03-workflows` learning path step (#4102)

* Fix solution project path

* Python: Fix broken markdown links to repo resources (outside /docs) (#4105)

* Initial plan

* Fix broken markdown links to repo resources

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Update README to rename .NET Workflows Samples section

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* .NET Samples - Create `02-agents` learning path step (#4107)

* .NET: Fix broken relative link in GroupChatToolApproval README (#4108)

* Initial plan

* Fix broken link in GroupChatToolApproval README

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Update labeler configuration for workflow samples

* .NET - Reorder Agents samples to start from Step01 instead of Step04 (#4110)

* Fix solution

* Resolve new sample paths

* Move new AgentSkills and AgentWithMemory_Step04 samples

* Fix link

* Fix readme path

* fix: update stale dotnet/samples/Durable path reference in AGENTS.md

Co-authored-by: crickman <66376200+crickman@users.noreply.github.com>

* Moved new sample

* Update solution

* Resolve merge (new sample)

* Sync to new sample - FoundryAgents_Step21_BingCustomSearch

* Updated README

* .NET Samples - Configuration Naming Update (#4149)

* .NET: Restore AzureFunctions index parity with ConsoleApps under DurableAgents samples (#4221)

* Clean-up `05_host_your_agent`

* Config setting consistency

* Refine samples

* AGENTS.md

* Move new samples

* Re-order samples

* Move new project and fixup solution

* Fixup model config

* Fix up new UT project

---------

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

* Python: Fix Bedrock embedding test stub missing meta attribute (#4287)

* Fix Bedrock embedding test stub missing meta attribute

* Increase test coverage so gate passes

* Python: (ag-ui): fix approval payloads being re-processed on subsequent conversation turns (#4232)

* Fix ag-ui tool call issue

* Safe json fix

* Python: Update workflow orchestration samples to use AzureOpenAIResponsesClient (#4285)

* Update workflow orchestration samples to use AzureOpenAIResponsesClient

* Fix broken link

* Move scripts to scripts folder

---------

Co-authored-by: Roger Barreto <19890735+rogerbarreto@users.noreply.github.com>
Co-authored-by: Chris <66376200+crickman@users.noreply.github.com>
Co-authored-by: Eduard van Valkenburg <eavanvalkenburg@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Rishabh Chawla <rishabhchawla1995@gmail.com>
Co-authored-by: Peter Ibekwe <109177538+peibekwe@users.noreply.github.com>
Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com>
Co-authored-by: Ben Thomas <ben.thomas@microsoft.com>
Co-authored-by: alliscode <bentho@microsoft.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Evan Mattson <35585003+moonbox3@users.noreply.github.com>
This commit is contained in:
westey
2026-02-26 10:49:07 +00:00
committed by GitHub
Unverified
parent cc1ef730e3
commit 8b191de936
871 changed files with 7688 additions and 1696 deletions
+27 -1
View File
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0rc2] - 2026-02-25
### Added
- **agent-framework-core**: Support Agent Skills ([#4210](https://github.com/microsoft/agent-framework/pull/4210))
- **agent-framework-core**: Add embedding abstractions and OpenAI implementation (Phase 1) ([#4153](https://github.com/microsoft/agent-framework/pull/4153))
- **agent-framework-core**: Add Foundry Memory Context Provider ([#3943](https://github.com/microsoft/agent-framework/pull/3943))
- **agent-framework-core**: Add `max_function_calls` to `FunctionInvocationConfiguration` ([#4175](https://github.com/microsoft/agent-framework/pull/4175))
- **agent-framework-core**: Add `CreateConversationExecutor`, fix input routing, remove unused handler layer ([#4159](https://github.com/microsoft/agent-framework/pull/4159))
- **agent-framework-azure-ai-search**: Azure AI Search provider improvements - EmbeddingGenerator, async context manager, KB message handling ([#4212](https://github.com/microsoft/agent-framework/pull/4212))
- **agent-framework-azure-ai-search**: Enhance Azure AI Search Citations with Document URLs in Foundry V2 ([#4028](https://github.com/microsoft/agent-framework/pull/4028))
- **agent-framework-ag-ui**: Add Workflow Support, Harden Streaming Semantics, and add Dynamic Handoff Demo ([#3911](https://github.com/microsoft/agent-framework/pull/3911))
### Changed
- **agent-framework-declarative**: [BREAKING] Add `InvokeFunctionTool` action for declarative workflows ([#3716](https://github.com/microsoft/agent-framework/pull/3716))
### Fixed
- **agent-framework-core**: Fix thread corruption when `max_iterations` is reached ([#4234](https://github.com/microsoft/agent-framework/pull/4234))
- **agent-framework-core**: Fix workflow runner concurrent processing ([#4143](https://github.com/microsoft/agent-framework/pull/4143))
- **agent-framework-core**: Fix doubled `tool_call` arguments in `MESSAGES_SNAPSHOT` when streaming ([#4200](https://github.com/microsoft/agent-framework/pull/4200))
- **agent-framework-core**: Fix OpenAI chat client compatibility with third-party endpoints and OTel 0.4.14 ([#4161](https://github.com/microsoft/agent-framework/pull/4161))
- **agent-framework-claude**: Fix `structured_output` propagation in `ClaudeAgent` ([#4137](https://github.com/microsoft/agent-framework/pull/4137))
## [1.0.0rc1] - 2026-02-19
Release candidate for **agent-framework-core** and **agent-framework-azure-ai** packages.
@@ -675,7 +700,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.0.0rc1...HEAD
[Unreleased]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc2...HEAD
[1.0.0rc2]: https://github.com/microsoft/agent-framework/compare/python-1.0.0rc1...python-1.0.0rc2
[1.0.0rc1]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260212...python-1.0.0rc1
[1.0.0b260212]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260210...python-1.0.0b260212
[1.0.0b260210]: https://github.com/microsoft/agent-framework/compare/python-1.0.0b260130...python-1.0.0b260210
+7 -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"a2a-sdk>=0.3.5",
]
@@ -37,6 +37,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -46,6 +47,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -79,6 +83,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_a2a"
test = "pytest --cov=agent_framework_a2a --cov-report=term-missing:skip-covered tests"
@@ -56,6 +56,7 @@ from ._utils import (
get_conversation_id_from_update,
get_role_value,
make_json_safe,
normalize_agui_role,
)
if TYPE_CHECKING:
@@ -450,7 +451,7 @@ async def _resolve_approval_responses(
_convert_approval_results_to_tool_messages(messages)
def _convert_approval_results_to_tool_messages(messages: list[Any]) -> None:
def _convert_approval_results_to_tool_messages(messages: list[Message]) -> None:
"""Convert function_result content in user messages to proper tool messages.
After approval processing, tool results end up in user messages. OpenAI and other
@@ -462,14 +463,14 @@ def _convert_approval_results_to_tool_messages(messages: list[Any]) -> None:
Args:
messages: List of Message objects to process
"""
result: list[Any] = []
result: list[Message] = []
for msg in messages:
if get_role_value(msg) != "user":
result.append(msg)
continue
msg_contents = cast(list[Content], getattr(msg, "contents", None) or [])
msg_contents = msg.contents or []
function_results: list[Content] = [content for content in msg_contents if content.type == "function_result"]
other_contents: list[Content] = [content for content in msg_contents if content.type != "function_result"]
@@ -492,6 +493,68 @@ def _convert_approval_results_to_tool_messages(messages: list[Any]) -> None:
messages[:] = result
def _clean_resolved_approvals_from_snapshot(
snapshot_messages: list[dict[str, Any]],
resolved_messages: list[Message],
) -> None:
"""Replace approval payloads in snapshot messages with actual tool results.
After _resolve_approval_responses executes approved tools, the snapshot still
contains the raw approval payload (e.g. ``{"accepted": true}``). When this
snapshot is sent back to CopilotKit via ``MessagesSnapshotEvent``, the approval
payload persists in the conversation history. On the next turn CopilotKit
re-sends the full history and the adapter re-detects the approval, causing the
tool to be re-executed.
This function replaces approval tool-message content in ``snapshot_messages``
with the real tool result so the approval payload no longer appears in the
history sent to the client.
Args:
snapshot_messages: Raw AG-UI snapshot messages (mutated in place).
resolved_messages: Provider messages after approval resolution.
"""
# Build call_id → result text from resolved tool messages
result_by_call_id: dict[str, str] = {}
for msg in resolved_messages:
if get_role_value(msg) != "tool":
continue
for content in msg.contents or []:
if content.type == "function_result" and content.call_id:
result_text = (
content.result if isinstance(content.result, str) else json.dumps(make_json_safe(content.result))
)
result_by_call_id[str(content.call_id)] = result_text
if not result_by_call_id:
return
for snap_msg in snapshot_messages:
if normalize_agui_role(snap_msg.get("role", "")) != "tool":
continue
raw_content = snap_msg.get("content")
if not isinstance(raw_content, str):
continue
# Check if this is an approval payload
try:
parsed = json.loads(raw_content)
except (json.JSONDecodeError, TypeError):
continue
if not isinstance(parsed, dict) or "accepted" not in parsed:
continue
# Find matching tool result by toolCallId
tool_call_id = snap_msg.get("toolCallId") or snap_msg.get("tool_call_id") or ""
replacement = result_by_call_id.get(str(tool_call_id))
if replacement is not None:
snap_msg["content"] = replacement
logger.info(
"Replaced approval payload in snapshot for tool_call_id=%s with actual result",
tool_call_id,
)
def _build_messages_snapshot(
flow: FlowState,
snapshot_messages: list[dict[str, Any]],
@@ -646,6 +709,10 @@ async def run_agent_stream(
tools_for_execution = tools if tools is not None else server_tools
await _resolve_approval_responses(messages, tools_for_execution, agent, run_kwargs)
# Defense-in-depth: replace approval payloads in snapshot with actual tool results
# so CopilotKit does not re-send stale approval content on subsequent turns.
_clean_resolved_approvals_from_snapshot(snapshot_messages, messages)
# Feature #3: Emit StateSnapshotEvent for approved state-changing tools before agent runs
approved_state_updates = _extract_approved_state_updates(messages, predictive_handler)
approved_state_snapshot_emitted = False
@@ -331,10 +331,6 @@ wrapped_agent = AgentFrameworkAgent(
orchestrators=[MyCustomOrchestrator(), DefaultOrchestrator()],
)
## Documentation
For detailed documentation, see [DESIGN.md](DESIGN.md).
## License
MIT
+5 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "agent-framework-ag-ui"
version = "1.0.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"ag-ui-protocol>=0.1.9",
"fastapi>=0.115.0",
"uvicorn>=0.30.0"
@@ -45,6 +45,9 @@ packages = ["agent_framework_ag_ui", "agent_framework_ag_ui_examples"]
asyncio_mode = "auto"
testpaths = ["tests/ag_ui"]
pythonpath = ["."]
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
line-length = 120
@@ -866,3 +866,45 @@ def test_agui_messages_to_snapshot_format_basic():
assert result[0]["content"] == "Hello"
assert result[1]["role"] == "assistant"
assert result[1]["content"] == "Hi there"
def test_agui_fresh_approval_is_still_processed():
"""A fresh approval (no assistant response after it) must still produce function_approval_response.
On Turn 2, the approval is fresh (no subsequent assistant message), so it
must be processed normally to execute the tool.
"""
messages_input = [
# Turn 1: user asks something
{"role": "user", "content": "What time is it?", "id": "msg_1"},
# Turn 1: assistant calls a tool
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_456",
"type": "function",
"function": {"name": "get_datetime", "arguments": "{}"},
}
],
"id": "msg_2",
},
# Turn 2: user approves (no assistant message after this)
{
"role": "tool",
"content": json.dumps({"accepted": True}),
"toolCallId": "call_456",
"id": "msg_3",
},
]
messages = agui_messages_to_agent_framework(messages_input)
# The fresh approval SHOULD produce a function_approval_response
approval_contents = [
content for msg in messages for content in (msg.contents or []) if content.type == "function_approval_response"
]
assert len(approval_contents) == 1, "Fresh approval should produce function_approval_response"
assert approval_contents[0].approved is True
assert approval_contents[0].function_call.name == "get_datetime"
@@ -262,3 +262,141 @@ def test_sanitize_tool_history_filters_confirm_changes_from_assistant_messages()
# (the approval response is handled separately by the framework)
tool_call_ids = {str(msg.contents[0].call_id) for msg in tool_messages}
assert "call_c1" not in tool_call_ids # No synthetic result for confirm_changes
# ---------------------------------------------------------------------------
# Tests for _clean_resolved_approvals_from_snapshot
# ---------------------------------------------------------------------------
def test_clean_resolved_approvals_from_snapshot() -> None:
"""Approval payload in snapshot should be replaced with the actual tool result."""
import json
from agent_framework_ag_ui._agent_run import _clean_resolved_approvals_from_snapshot
# Snapshot still has the approval payload
snapshot_messages = [
{"role": "user", "content": "What time is it?", "id": "msg_1"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{"id": "call_123", "type": "function", "function": {"name": "get_datetime", "arguments": "{}"}}
],
"id": "msg_2",
},
{
"role": "tool",
"content": json.dumps({"accepted": True}),
"toolCallId": "call_123",
"id": "msg_3",
},
]
# Resolved provider messages have the actual tool result
resolved_messages = [
Message(role="user", contents=[Content.from_text(text="What time is it?")]),
Message(
role="assistant",
contents=[Content.from_function_call(call_id="call_123", name="get_datetime", arguments="{}")],
),
Message(
role="tool",
contents=[Content.from_function_result(call_id="call_123", result="2024-01-01 12:00:00")],
),
]
_clean_resolved_approvals_from_snapshot(snapshot_messages, resolved_messages)
# The approval payload should now be replaced with the tool result
tool_snap = snapshot_messages[2]
assert tool_snap["content"] == "2024-01-01 12:00:00"
def test_clean_resolved_approvals_from_snapshot_no_approvals() -> None:
"""When there are no approval payloads, snapshot should be unchanged."""
from agent_framework_ag_ui._agent_run import _clean_resolved_approvals_from_snapshot # type: ignore
snapshot_messages = [
{"role": "user", "content": "Hello", "id": "msg_1"},
{"role": "assistant", "content": "Hi there", "id": "msg_2"},
]
original = [dict(m) for m in snapshot_messages]
resolved_messages = [
Message(role="user", contents=[Content.from_text(text="Hello")]),
Message(role="assistant", contents=[Content.from_text(text="Hi there")]),
]
_clean_resolved_approvals_from_snapshot(snapshot_messages, resolved_messages)
# Nothing should have changed
assert snapshot_messages == original
def test_cleaned_snapshot_prevents_approval_reprocessing() -> None:
"""After snapshot cleaning, approval payload is replaced so it won't re-trigger on next turn.
Simulates what happens on Turn 2: the approval is processed, the tool executes,
and _clean_resolved_approvals_from_snapshot replaces the approval payload with the
real tool result. On Turn 3, CopilotKit re-sends the cleaned snapshot, which no
longer contains an approval payload — so no function_approval_response is produced.
"""
import json
from agent_framework_ag_ui._agent_run import _clean_resolved_approvals_from_snapshot
from agent_framework_ag_ui._message_adapters import normalize_agui_input_messages
# Turn 2 snapshot: still has the raw approval payload
snapshot_messages = [
{"role": "user", "content": "What time is it?", "id": "msg_1"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{"id": "call_789", "type": "function", "function": {"name": "get_datetime", "arguments": "{}"}}
],
"id": "msg_2",
},
{
"role": "tool",
"content": json.dumps({"accepted": True}),
"toolCallId": "call_789",
"id": "msg_3",
},
]
# Resolved provider messages after tool execution
resolved_messages = [
Message(role="user", contents=[Content.from_text(text="What time is it?")]),
Message(
role="assistant",
contents=[Content.from_function_call(call_id="call_789", name="get_datetime", arguments="{}")],
),
Message(
role="tool",
contents=[Content.from_function_result(call_id="call_789", result="2024-01-01 12:00:00")],
),
]
# Fix B: clean the snapshot
_clean_resolved_approvals_from_snapshot(snapshot_messages, resolved_messages)
# Snapshot should now have the real tool result
assert snapshot_messages[2]["content"] == "2024-01-01 12:00:00"
# Simulate Turn 3: CopilotKit re-sends the cleaned snapshot + new messages
turn3_messages = list(snapshot_messages) + [
{"role": "assistant", "content": "It is 12:00 PM.", "id": "msg_4"},
{"role": "user", "content": "Thanks!", "id": "msg_5"},
]
provider_messages, _ = normalize_agui_input_messages(turn3_messages)
# No function_approval_response should exist — the approval payload is gone
for msg in provider_messages:
for content in msg.contents or []:
assert content.type != "function_approval_response", (
f"Stale approval was re-processed on subsequent turn: {content}"
)
+7 -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"anthropic>=0.70.0,<1",
]
@@ -37,6 +37,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -46,6 +47,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -80,6 +84,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_anthropic"
test = "pytest --cov=agent_framework_anthropic --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"
@@ -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"azure-search-documents==11.7.0b2",
]
@@ -47,6 +47,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -82,6 +85,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai_search"
test = "pytest --cov=agent_framework_azure_ai_search --cov-report=term-missing:skip-covered tests"
@@ -1297,7 +1297,6 @@ class TestPrepareMessagesForKbSearch:
assert result[0].content[0].text == "hello"
def test_image_uri_content(self) -> None:
from agent_framework import Content
img = Content.from_uri(uri="https://example.com/photo.png", media_type="image/png")
messages = [Message(role="user", contents=[img])]
@@ -1309,7 +1308,6 @@ class TestPrepareMessagesForKbSearch:
assert result[0].content[0].image.url == "https://example.com/photo.png"
def test_mixed_text_and_image_content(self) -> None:
from agent_framework import Content
text = Content.from_text("describe this image")
img = Content.from_uri(uri="https://example.com/img.jpg", media_type="image/jpeg")
@@ -1319,7 +1317,6 @@ class TestPrepareMessagesForKbSearch:
assert len(result[0].content) == 2
def test_skips_non_text_non_image_content(self) -> None:
from agent_framework import Content
error = Content.from_error(message="oops")
messages = [Message(role="user", contents=[error])]
@@ -1327,7 +1324,6 @@ class TestPrepareMessagesForKbSearch:
assert len(result) == 0 # message had no usable content
def test_skips_empty_text(self) -> None:
from agent_framework import Content
empty = Content.from_text("")
messages = [Message(role="user", contents=[empty])]
@@ -1341,7 +1337,6 @@ class TestPrepareMessagesForKbSearch:
assert result[0].content[0].text == "fallback text"
def test_data_uri_image(self) -> None:
from agent_framework import Content
img = Content.from_data(data=b"\x89PNG", media_type="image/png")
messages = [Message(role="user", contents=[img])]
@@ -1352,7 +1347,6 @@ class TestPrepareMessagesForKbSearch:
assert isinstance(result[0].content[0], KnowledgeBaseMessageImageContent)
def test_non_image_uri_skipped(self) -> None:
from agent_framework import Content
pdf = Content.from_uri(uri="https://example.com/doc.pdf", media_type="application/pdf")
messages = [Message(role="user", contents=[pdf])]
@@ -1568,9 +1562,7 @@ class TestParseMessagesFromKbResponse:
KnowledgeBaseMessage(role="assistant", content=[KnowledgeBaseMessageTextContent(text="answer")]),
],
references=[
KnowledgeBaseWebReference(
id="ref-1", activity_source=0, url="https://example.com", title="Example"
),
KnowledgeBaseWebReference(id="ref-1", activity_source=0, url="https://example.com", title="Example"),
],
)
result = AzureAISearchContextProvider._parse_messages_from_kb_response(response)
@@ -5,6 +5,12 @@ import importlib.metadata
from ._agent_provider import AzureAIAgentsProvider
from ._chat_client import AzureAIAgentClient, AzureAIAgentOptions
from ._client import AzureAIClient, AzureAIProjectAgentOptions, RawAzureAIClient
from ._embedding_client import (
AzureAIInferenceEmbeddingClient,
AzureAIInferenceEmbeddingOptions,
AzureAIInferenceEmbeddingSettings,
RawAzureAIInferenceEmbeddingClient,
)
from ._foundry_memory_provider import FoundryMemoryProvider
from ._project_provider import AzureAIProjectAgentProvider
from ._shared import AzureAISettings
@@ -19,10 +25,14 @@ __all__ = [
"AzureAIAgentOptions",
"AzureAIAgentsProvider",
"AzureAIClient",
"AzureAIInferenceEmbeddingClient",
"AzureAIInferenceEmbeddingOptions",
"AzureAIInferenceEmbeddingSettings",
"AzureAIProjectAgentOptions",
"AzureAIProjectAgentProvider",
"AzureAISettings",
"FoundryMemoryProvider",
"RawAzureAIClient",
"RawAzureAIInferenceEmbeddingClient",
"__version__",
]
@@ -0,0 +1,396 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
import sys
from collections.abc import Sequence
from contextlib import suppress
from typing import Any, ClassVar, Generic, TypedDict
from agent_framework import (
BaseEmbeddingClient,
Content,
Embedding,
EmbeddingGenerationOptions,
GeneratedEmbeddings,
UsageDetails,
load_settings,
)
from agent_framework.observability import EmbeddingTelemetryLayer
from azure.ai.inference.aio import EmbeddingsClient, ImageEmbeddingsClient
from azure.ai.inference.models import ImageEmbeddingInput
from azure.core.credentials import AzureKeyCredential
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
else:
from typing_extensions import TypeVar # type: ignore # pragma: no cover
logger = logging.getLogger("agent_framework.azure_ai")
_IMAGE_MEDIA_PREFIXES = ("image/",)
class AzureAIInferenceEmbeddingOptions(EmbeddingGenerationOptions, total=False):
"""Azure AI Inference-specific embedding options.
Extends EmbeddingGenerationOptions with Azure AI Inference-specific fields.
Examples:
.. code-block:: python
from agent_framework_azure_ai import AzureAIInferenceEmbeddingOptions
options: AzureAIInferenceEmbeddingOptions = {
"model_id": "text-embedding-3-small",
"dimensions": 1536,
"input_type": "document",
"encoding_format": "float",
}
"""
input_type: str
"""Input type hint for the model. Common values: ``"text"``, ``"query"``, ``"document"``."""
image_model_id: str
"""Override model for image embeddings. Falls back to the client's ``image_model_id``."""
encoding_format: str
"""Output encoding format.
Common values: ``"float"``, ``"base64"``, ``"int8"``, ``"uint8"``,
``"binary"``, ``"ubinary"``.
"""
extra_parameters: dict[str, Any]
"""Additional model-specific parameters passed directly to the API."""
AzureAIInferenceEmbeddingOptionsT = TypeVar(
"AzureAIInferenceEmbeddingOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="AzureAIInferenceEmbeddingOptions",
covariant=True,
)
class AzureAIInferenceEmbeddingSettings(TypedDict, total=False):
"""Azure AI Inference embedding settings."""
endpoint: str | None
api_key: str | None
embedding_model_id: str | None
image_embedding_model_id: str | None
class RawAzureAIInferenceEmbeddingClient(
BaseEmbeddingClient[Content | str, list[float], AzureAIInferenceEmbeddingOptionsT],
Generic[AzureAIInferenceEmbeddingOptionsT],
):
"""Raw Azure AI Inference embedding client without telemetry.
Accepts both text (``str``) and image (``Content``) inputs. Text and image
inputs within a single batch are separated and dispatched to
``EmbeddingsClient`` and ``ImageEmbeddingsClient`` respectively. Results
are reassembled in the original input order.
Keyword Args:
model_id: The text embedding model deployment name (e.g. "text-embedding-3-small").
Can also be set via environment variable AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID.
image_model_id: The image embedding model deployment name (e.g. "Cohere-embed-v3-english").
Can also be set via environment variable AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID.
Falls back to ``model_id`` if not provided.
endpoint: The Azure AI Inference endpoint URL.
Can also be set via environment variable AZURE_AI_INFERENCE_ENDPOINT.
api_key: API key for authentication.
Can also be set via environment variable AZURE_AI_INFERENCE_API_KEY.
text_client: Optional pre-configured ``EmbeddingsClient``.
image_client: Optional pre-configured ``ImageEmbeddingsClient``.
credential: Optional ``AzureKeyCredential`` or token credential. If not provided,
one is created from ``api_key``.
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
"""
def __init__(
self,
*,
model_id: str | None = None,
image_model_id: str | None = None,
endpoint: str | None = None,
api_key: str | None = None,
text_client: EmbeddingsClient | None = None,
image_client: ImageEmbeddingsClient | None = None,
credential: AzureKeyCredential | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize a raw Azure AI Inference embedding client."""
settings = load_settings(
AzureAIInferenceEmbeddingSettings,
env_prefix="AZURE_AI_INFERENCE_",
required_fields=["endpoint", "embedding_model_id"],
endpoint=endpoint,
api_key=api_key,
embedding_model_id=model_id,
image_embedding_model_id=image_model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
self.model_id = settings["embedding_model_id"] # type: ignore[reportTypedDictNotRequiredAccess]
self.image_model_id: str = settings.get("image_embedding_model_id") or self.model_id # type: ignore[assignment]
resolved_endpoint = settings["endpoint"] # type: ignore[reportTypedDictNotRequiredAccess]
if credential is None and settings.get("api_key"):
credential = AzureKeyCredential(settings["api_key"]) # type: ignore[arg-type]
if credential is None and text_client is None and image_client is None:
raise ValueError("Either 'api_key', 'credential', or pre-configured client(s) must be provided.")
self._text_client = text_client or EmbeddingsClient(
endpoint=resolved_endpoint, # type: ignore[arg-type]
credential=credential, # type: ignore[arg-type]
)
self._image_client = image_client or ImageEmbeddingsClient(
endpoint=resolved_endpoint, # type: ignore[arg-type]
credential=credential, # type: ignore[arg-type]
)
self._endpoint = resolved_endpoint
super().__init__(**kwargs)
async def close(self) -> None:
"""Close the underlying SDK clients and release resources."""
with suppress(Exception):
await self._text_client.close()
with suppress(Exception):
await self._image_client.close()
async def __aenter__(self) -> RawAzureAIInferenceEmbeddingClient[AzureAIInferenceEmbeddingOptionsT]:
"""Enter the async context manager."""
return self
async def __aexit__(self, *args: Any) -> None:
"""Exit the async context manager and close clients."""
await self.close()
def service_url(self) -> str:
"""Get the URL of the service."""
return self._endpoint or ""
async def get_embeddings(
self,
values: Sequence[Content | str],
*,
options: AzureAIInferenceEmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[list[float]]:
"""Generate embeddings for text and/or image inputs.
Text inputs (``str`` or ``Content`` with ``type="text"``) are sent to the
text embeddings endpoint. Image inputs (``Content`` with an image
``media_type``) are sent to the image embeddings endpoint. Results are
returned in the same order as the input.
Args:
values: A sequence of text strings or ``Content`` instances.
options: Optional embedding generation options.
Returns:
Generated embeddings with usage metadata.
Raises:
ValueError: If model_id is not provided or an unsupported content type is encountered.
"""
if not values:
return GeneratedEmbeddings([], options=options) # type: ignore[reportReturnType]
opts: dict[str, Any] = dict(options) if options else {}
# Separate text and image inputs, tracking original indices.
text_items: list[tuple[int, str]] = []
image_items: list[tuple[int, ImageEmbeddingInput]] = []
for idx, value in enumerate(values):
if isinstance(value, str):
text_items.append((idx, value))
elif isinstance(value, Content):
if value.type == "text" and value.text is not None:
text_items.append((idx, value.text))
elif (
value.type in ("data", "uri")
and value.media_type
and value.media_type.startswith(_IMAGE_MEDIA_PREFIXES[0])
):
if not value.uri:
raise ValueError(f"Image Content at index {idx} has no URI.")
image_input = ImageEmbeddingInput(image=value.uri, text=value.text)
image_items.append((idx, image_input))
else:
raise ValueError(
f"Unsupported Content type '{value.type}' with media_type "
f"'{value.media_type}' at index {idx}. Expected text content or "
f"image content (media_type starting with 'image/')."
)
else:
raise ValueError(f"Unsupported input type {type(value).__name__} at index {idx}.")
# Build shared API kwargs (without model, which differs per client).
common_kwargs: dict[str, Any] = {}
if dimensions := opts.get("dimensions"):
common_kwargs["dimensions"] = dimensions
if encoding_format := opts.get("encoding_format"):
common_kwargs["encoding_format"] = encoding_format
if input_type := opts.get("input_type"):
common_kwargs["input_type"] = input_type
if extra_parameters := opts.get("extra_parameters"):
common_kwargs["model_extras"] = extra_parameters
# Allocate results array.
embeddings: list[Embedding[list[float]] | None] = [None] * len(values)
usage_details: UsageDetails = {}
# Embed text inputs.
if text_items:
if not (text_model := opts.get("model_id") or self.model_id):
raise ValueError("An model_id is required, either in the client or options, for text inputs.")
text_inputs = [t for _, t in text_items]
response = await self._text_client.embed(
input=text_inputs,
model=text_model,
**common_kwargs,
)
for i, item in enumerate(response.data):
original_idx = text_items[i][0]
vector: list[float] = [float(v) for v in item.embedding]
embeddings[original_idx] = Embedding(
vector=vector,
dimensions=len(vector),
model_id=response.model or text_model,
)
if response.usage:
usage_details["input_token_count"] = (usage_details.get("input_token_count") or 0) + (
response.usage.prompt_tokens or 0
)
usage_details["output_token_count"] = (usage_details.get("output_token_count") or 0) + (
getattr(response.usage, "completion_tokens", 0) or 0
)
# Embed image inputs.
if image_items:
if not (image_model := opts.get("image_model_id") or self.image_model_id):
raise ValueError("An image_model_id is required, either in the client or options, for image inputs.")
image_inputs = [img for _, img in image_items]
response = await self._image_client.embed(
input=image_inputs,
model=image_model,
**common_kwargs,
)
for i, item in enumerate(response.data):
original_idx = image_items[i][0]
image_vector: list[float] = [float(v) for v in item.embedding]
embeddings[original_idx] = Embedding(
vector=image_vector,
dimensions=len(image_vector),
model_id=response.model or image_model,
)
if response.usage:
usage_details["input_token_count"] = (usage_details.get("input_token_count") or 0) + (
response.usage.prompt_tokens or 0
)
usage_details["output_token_count"] = (usage_details.get("output_token_count") or 0) + (
getattr(response.usage, "completion_tokens", 0) or 0
)
return GeneratedEmbeddings(
[embedding for embedding in embeddings if embedding is not None],
options=options,
usage=usage_details,
) # type: ignore[reportReturnType]
class AzureAIInferenceEmbeddingClient(
EmbeddingTelemetryLayer[Content | str, list[float], AzureAIInferenceEmbeddingOptionsT],
RawAzureAIInferenceEmbeddingClient[AzureAIInferenceEmbeddingOptionsT],
Generic[AzureAIInferenceEmbeddingOptionsT],
):
"""Azure AI Inference embedding client with telemetry support.
Supports both text and image inputs in a single client. Pass plain strings
or ``Content`` instances created with ``Content.from_text()`` or
``Content.from_data()``.
Keyword Args:
model_id: The text embedding model deployment name (e.g. "text-embedding-3-small").
Can also be set via environment variable AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID.
image_model_id: The image embedding model deployment name
(e.g. "Cohere-embed-v3-english"). Can also be set via environment variable
AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID. Falls back to ``model_id``.
endpoint: The Azure AI Inference endpoint URL.
Can also be set via environment variable AZURE_AI_INFERENCE_ENDPOINT.
api_key: API key for authentication.
Can also be set via environment variable AZURE_AI_INFERENCE_API_KEY.
text_client: Optional pre-configured ``EmbeddingsClient``.
image_client: Optional pre-configured ``ImageEmbeddingsClient``.
credential: Optional ``AzureKeyCredential`` or token credential.
otel_provider_name: Override for the OpenTelemetry provider name.
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
Examples:
.. code-block:: python
from agent_framework_azure_ai import AzureAIInferenceEmbeddingClient
# Using environment variables
# Set AZURE_AI_INFERENCE_ENDPOINT=https://your-endpoint.inference.ai.azure.com
# Set AZURE_AI_INFERENCE_API_KEY=your-key
# Set AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID=text-embedding-3-small
# Set AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID=Cohere-embed-v3-english
client = AzureAIInferenceEmbeddingClient()
# Text embeddings
result = await client.get_embeddings(["Hello, world!"])
# Image embeddings
from agent_framework import Content
image = Content.from_data(data=image_bytes, media_type="image/png")
result = await client.get_embeddings([image])
# Mixed text and image
result = await client.get_embeddings(["hello", image])
"""
OTEL_PROVIDER_NAME: ClassVar[str] = "azure.ai.inference" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
*,
model_id: str | None = None,
image_model_id: str | None = None,
endpoint: str | None = None,
api_key: str | None = None,
text_client: EmbeddingsClient | None = None,
image_client: ImageEmbeddingsClient | None = None,
credential: AzureKeyCredential | None = None,
otel_provider_name: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize an Azure AI Inference embedding client."""
super().__init__(
model_id=model_id,
image_model_id=image_model_id,
endpoint=endpoint,
api_key=api_key,
text_client=text_client,
image_client=image_client,
credential=credential,
otel_provider_name=otel_provider_name,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
**kwargs,
)
+8 -2
View File
@@ -4,7 +4,7 @@ description = "Azure AI Foundry integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0rc1"
version = "1.0.0rc2"
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.0rc1",
"agent-framework-core>=1.0.0rc2",
"azure-ai-agents == 1.2.0b5",
"azure-ai-inference>=1.0.0b9",
"aiohttp",
]
@@ -38,6 +39,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -45,6 +47,9 @@ asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -78,6 +83,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai"
test = "pytest --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests"
@@ -0,0 +1,316 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import os
from collections.abc import Sequence
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import Content
from agent_framework_azure_ai import (
AzureAIInferenceEmbeddingClient,
AzureAIInferenceEmbeddingOptions,
RawAzureAIInferenceEmbeddingClient,
)
def _make_embed_response(
embeddings: Sequence[list[float]],
model: str = "test-model",
prompt_tokens: int = 10,
) -> MagicMock:
"""Create a mock EmbeddingsResult."""
data = []
for emb in embeddings:
item = MagicMock()
item.embedding = emb
data.append(item)
usage = MagicMock()
usage.prompt_tokens = prompt_tokens
usage.completion_tokens = 0
result = MagicMock()
result.data = data
result.model = model
result.usage = usage
return result
@pytest.fixture
def mock_text_client() -> AsyncMock:
"""Create a mock text EmbeddingsClient."""
client = AsyncMock()
client.embed = AsyncMock(return_value=_make_embed_response([[0.1, 0.2, 0.3]]))
return client
@pytest.fixture
def mock_image_client() -> AsyncMock:
"""Create a mock image ImageEmbeddingsClient."""
client = AsyncMock()
client.embed = AsyncMock(return_value=_make_embed_response([[0.4, 0.5, 0.6]]))
return client
@pytest.fixture
def raw_client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> RawAzureAIInferenceEmbeddingClient[Any]:
"""Create a RawAzureAIInferenceEmbeddingClient with mocked SDK clients."""
return RawAzureAIInferenceEmbeddingClient(
model_id="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
image_client=mock_image_client,
)
@pytest.fixture
def client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> AzureAIInferenceEmbeddingClient[Any]:
"""Create an AzureAIInferenceEmbeddingClient with mocked SDK clients."""
return AzureAIInferenceEmbeddingClient(
model_id="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
image_client=mock_image_client,
)
class TestRawAzureAIInferenceEmbeddingClient:
"""Tests for the raw Azure AI Inference embedding client."""
async def test_text_embeddings(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""Text inputs are dispatched to the text client."""
result = await raw_client.get_embeddings(["hello", "world"])
assert result is not None
call_kwargs = mock_text_client.embed.call_args
assert call_kwargs.kwargs["input"] == ["hello", "world"]
assert call_kwargs.kwargs["model"] == "test-model"
async def test_text_content_embeddings(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""Content.from_text() inputs are dispatched to the text client."""
text_content = Content.from_text("hello")
await raw_client.get_embeddings([text_content])
mock_text_client.embed.assert_called_once()
call_kwargs = mock_text_client.embed.call_args
assert call_kwargs.kwargs["input"] == ["hello"]
async def test_image_content_embeddings(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_image_client: AsyncMock
) -> None:
"""Image Content inputs are dispatched to the image client."""
image_content = Content.from_data(data=b"\x89PNG", media_type="image/png")
await raw_client.get_embeddings([image_content])
mock_image_client.embed.assert_called_once()
call_kwargs = mock_image_client.embed.call_args
image_inputs = call_kwargs.kwargs["input"]
assert len(image_inputs) == 1
assert image_inputs[0].image == image_content.uri
async def test_mixed_text_and_image(
self,
raw_client: RawAzureAIInferenceEmbeddingClient[Any],
mock_text_client: AsyncMock,
mock_image_client: AsyncMock,
) -> None:
"""Mixed text and image inputs are dispatched to the correct clients."""
mock_text_client.embed.return_value = _make_embed_response([[0.1, 0.2]])
mock_image_client.embed.return_value = _make_embed_response([[0.3, 0.4]])
image = Content.from_data(data=b"\x89PNG", media_type="image/png")
await raw_client.get_embeddings(["hello", image, "world"])
# Text client gets "hello" and "world"
text_call = mock_text_client.embed.call_args
assert text_call.kwargs["input"] == ["hello", "world"]
# Image client gets the image
image_call = mock_image_client.embed.call_args
assert len(image_call.kwargs["input"]) == 1
async def test_empty_input(self, raw_client: RawAzureAIInferenceEmbeddingClient[Any]) -> None:
"""Empty input returns empty result."""
result = await raw_client.get_embeddings([])
assert len(result) == 0
async def test_options_passed_through(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""Options are passed through to the SDK."""
options: AzureAIInferenceEmbeddingOptions = {
"dimensions": 512,
"input_type": "document",
"encoding_format": "float",
}
await raw_client.get_embeddings(["hello"], options=options)
call_kwargs = mock_text_client.embed.call_args
assert call_kwargs.kwargs["dimensions"] == 512
assert call_kwargs.kwargs["input_type"] == "document"
assert call_kwargs.kwargs["encoding_format"] == "float"
async def test_model_override_in_options(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""model_id in options overrides the default."""
options: AzureAIInferenceEmbeddingOptions = {"model_id": "custom-model"}
await raw_client.get_embeddings(["hello"], options=options)
call_kwargs = mock_text_client.embed.call_args
assert call_kwargs.kwargs["model"] == "custom-model"
async def test_unsupported_content_type_raises(self, raw_client: RawAzureAIInferenceEmbeddingClient[Any]) -> None:
"""Non-text, non-image Content raises ValueError."""
error_content = Content("error", message="fail")
with pytest.raises(ValueError, match="Unsupported Content type"):
await raw_client.get_embeddings([error_content])
async def test_usage_metadata(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""Usage metadata is populated from the response."""
mock_text_client.embed.return_value = _make_embed_response([[0.1, 0.2]], prompt_tokens=42)
result = await raw_client.get_embeddings(["hello"])
assert result.usage is not None
assert result.usage["input_token_count"] == 42
def test_service_url(self, raw_client: RawAzureAIInferenceEmbeddingClient[Any]) -> None:
"""service_url returns the configured endpoint."""
assert raw_client.service_url() == "https://test.inference.ai.azure.com"
def test_settings_from_env(self) -> None:
"""Settings are loaded from environment variables."""
with (
patch.dict(
os.environ,
{
"AZURE_AI_INFERENCE_ENDPOINT": "https://env.inference.ai.azure.com",
"AZURE_AI_INFERENCE_API_KEY": "env-key",
"AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID": "env-model",
},
),
patch("agent_framework_azure_ai._embedding_client.EmbeddingsClient"),
patch("agent_framework_azure_ai._embedding_client.ImageEmbeddingsClient"),
):
client = RawAzureAIInferenceEmbeddingClient()
assert client.model_id == "env-model"
assert client.image_model_id == "env-model" # falls back to model_id
def test_image_model_id_from_env(self) -> None:
"""image_model_id is loaded from its own environment variable."""
with (
patch.dict(
os.environ,
{
"AZURE_AI_INFERENCE_ENDPOINT": "https://env.inference.ai.azure.com",
"AZURE_AI_INFERENCE_API_KEY": "env-key",
"AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID": "text-model",
"AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID": "image-model",
},
),
patch("agent_framework_azure_ai._embedding_client.EmbeddingsClient"),
patch("agent_framework_azure_ai._embedding_client.ImageEmbeddingsClient"),
):
client = RawAzureAIInferenceEmbeddingClient()
assert client.model_id == "text-model"
assert client.image_model_id == "image-model"
def test_image_model_id_explicit(self, mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> None:
"""image_model_id can be set explicitly."""
client = RawAzureAIInferenceEmbeddingClient(
model_id="text-model",
image_model_id="image-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
image_client=mock_image_client,
)
assert client.model_id == "text-model"
assert client.image_model_id == "image-model"
async def test_image_model_id_sent_to_image_client(
self, mock_text_client: AsyncMock, mock_image_client: AsyncMock
) -> None:
"""image_model_id is passed to the image client embed call."""
client = RawAzureAIInferenceEmbeddingClient(
model_id="text-model",
image_model_id="image-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
image_client=mock_image_client,
)
image_content = Content.from_data(data=b"\x89PNG", media_type="image/png")
await client.get_embeddings([image_content])
call_kwargs = mock_image_client.embed.call_args
assert call_kwargs.kwargs["model"] == "image-model"
class TestAzureAIInferenceEmbeddingClient:
"""Tests for the telemetry-enabled Azure AI Inference embedding client."""
async def test_text_embeddings(
self, client: AzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""Text embeddings work through the telemetry layer."""
result = await client.get_embeddings(["hello"])
assert len(result) == 1
assert result[0].vector == [0.1, 0.2, 0.3]
async def test_otel_provider_name_default(self) -> None:
"""Default OTEL provider name is azure.ai.inference."""
assert AzureAIInferenceEmbeddingClient.OTEL_PROVIDER_NAME == "azure.ai.inference"
async def test_otel_provider_name_override(self, mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> None:
"""OTEL provider name can be overridden."""
client = AzureAIInferenceEmbeddingClient(
model_id="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
text_client=mock_text_client,
image_client=mock_image_client,
otel_provider_name="custom-provider",
)
assert client.otel_provider_name == "custom-provider"
_SKIP_REASON = "Azure AI Inference integration tests disabled"
def _integration_tests_enabled() -> bool:
return bool(
os.environ.get("AZURE_AI_INFERENCE_ENDPOINT")
and os.environ.get("AZURE_AI_INFERENCE_API_KEY")
and os.environ.get("AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID")
)
skip_if_azure_ai_inference_integration_tests_disabled = pytest.mark.skipif(
not _integration_tests_enabled(),
reason=_SKIP_REASON,
)
class TestAzureAIInferenceEmbeddingIntegration:
"""Integration tests requiring a live Azure AI Inference endpoint."""
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_ai_inference_integration_tests_disabled
async def test_text_embedding_live(self) -> None:
"""Generate text embeddings against a live endpoint."""
client = AzureAIInferenceEmbeddingClient()
result = await client.get_embeddings(["Hello, world!"])
assert len(result) == 1
assert len(result[0].vector) > 0
assert result[0].model_id is not None
@@ -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"agent-framework-durabletask",
"azure-functions",
"azure-functions-durable",
@@ -41,6 +41,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
pythonpath = ["tests/integration_tests"]
@@ -88,6 +89,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azurefunctions"
test = "pytest --cov=agent_framework_azurefunctions --cov-report=term-missing:skip-covered tests"
@@ -3,6 +3,7 @@
import importlib.metadata
from ._chat_client import BedrockChatClient, BedrockChatOptions, BedrockGuardrailConfig, BedrockSettings
from ._embedding_client import BedrockEmbeddingClient, BedrockEmbeddingOptions, BedrockEmbeddingSettings
try:
__version__ = importlib.metadata.version(__name__)
@@ -12,6 +13,9 @@ except importlib.metadata.PackageNotFoundError:
__all__ = [
"BedrockChatClient",
"BedrockChatOptions",
"BedrockEmbeddingClient",
"BedrockEmbeddingOptions",
"BedrockEmbeddingSettings",
"BedrockGuardrailConfig",
"BedrockSettings",
"__version__",
@@ -0,0 +1,292 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import asyncio
import json
import logging
import sys
from collections.abc import Sequence
from typing import Any, ClassVar, Generic, TypedDict
from agent_framework import (
AGENT_FRAMEWORK_USER_AGENT,
BaseEmbeddingClient,
Embedding,
EmbeddingGenerationOptions,
GeneratedEmbeddings,
SecretString,
UsageDetails,
load_settings,
)
from agent_framework.observability import EmbeddingTelemetryLayer
from boto3.session import Session as Boto3Session
from botocore.client import BaseClient
from botocore.config import Config as BotoConfig
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
else:
from typing_extensions import TypeVar # type: ignore # pragma: no cover
logger = logging.getLogger("agent_framework.bedrock")
DEFAULT_REGION = "us-east-1"
class BedrockEmbeddingSettings(TypedDict, total=False):
"""Bedrock embedding settings."""
region: str | None
embedding_model_id: str | None
access_key: SecretString | None
secret_key: SecretString | None
session_token: SecretString | None
class BedrockEmbeddingOptions(EmbeddingGenerationOptions, total=False):
"""Bedrock-specific embedding options.
Extends EmbeddingGenerationOptions with Bedrock-specific fields.
Examples:
.. code-block:: python
from agent_framework_bedrock import BedrockEmbeddingOptions
options: BedrockEmbeddingOptions = {
"model_id": "amazon.titan-embed-text-v2:0",
"dimensions": 1024,
"normalize": True,
}
"""
normalize: bool
BedrockEmbeddingOptionsT = TypeVar(
"BedrockEmbeddingOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="BedrockEmbeddingOptions",
covariant=True,
)
class RawBedrockEmbeddingClient(
BaseEmbeddingClient[str, list[float], BedrockEmbeddingOptionsT],
Generic[BedrockEmbeddingOptionsT],
):
"""Raw Bedrock embedding client without telemetry.
Keyword Args:
model_id: The Bedrock embedding model ID (e.g. "amazon.titan-embed-text-v2:0").
Can also be set via environment variable BEDROCK_EMBEDDING_MODEL_ID.
region: AWS region. Will try to load from BEDROCK_REGION env var,
if not set, the regular Boto3 configuration/loading applies
(which may include other env vars, config files, or instance metadata).
access_key: AWS access key for manual credential injection.
secret_key: AWS secret key paired with access_key.
session_token: AWS session token for temporary credentials.
client: Preconfigured Bedrock runtime client.
boto3_session: Custom boto3 session used to build the runtime client.
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
"""
def __init__(
self,
*,
region: str | None = None,
model_id: str | None = None,
access_key: str | None = None,
secret_key: str | None = None,
session_token: str | None = None,
client: BaseClient | None = None,
boto3_session: Boto3Session | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize a raw Bedrock embedding client."""
settings = load_settings(
BedrockEmbeddingSettings,
env_prefix="BEDROCK_",
required_fields=["embedding_model_id"],
region=region,
embedding_model_id=model_id,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
resolved_region = settings.get("region") or DEFAULT_REGION
if client is None:
if not boto3_session:
session_kwargs: dict[str, Any] = {}
if region := settings.get("region"):
session_kwargs["region_name"] = region
if (access_key := settings.get("access_key")) and (secret_key := settings.get("secret_key")):
session_kwargs["aws_access_key_id"] = access_key.get_secret_value() # type: ignore[union-attr]
session_kwargs["aws_secret_access_key"] = secret_key.get_secret_value() # type: ignore[union-attr]
if session_token := settings.get("session_token"):
session_kwargs["aws_session_token"] = session_token.get_secret_value() # type: ignore[union-attr]
boto3_session = Boto3Session(**session_kwargs)
client = boto3_session.client(
"bedrock-runtime",
region_name=boto3_session.region_name or resolved_region,
config=BotoConfig(user_agent_extra=AGENT_FRAMEWORK_USER_AGENT),
)
self._bedrock_client = client
self.model_id = settings["embedding_model_id"] # type: ignore[assignment]
self.region = resolved_region
super().__init__(**kwargs)
def service_url(self) -> str:
"""Get the URL of the service."""
return str(self._bedrock_client.meta.endpoint_url)
async def get_embeddings(
self,
values: Sequence[str],
*,
options: BedrockEmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[list[float]]:
"""Call the Bedrock invoke_model API for embeddings.
Uses the Amazon Titan Embeddings model format. Each value is embedded
individually since Titan's invoke_model API accepts one input at a time.
Args:
values: The text values to generate embeddings for.
options: Optional embedding generation options.
Returns:
Generated embeddings with usage metadata.
Raises:
ValueError: If model_id is not provided or values is empty.
"""
if not values:
return GeneratedEmbeddings([], options=options)
opts: dict[str, Any] = dict(options) if options else {}
model = opts.get("model_id") or self.model_id
if not model:
raise ValueError("model_id is required")
embedding_results = await asyncio.gather(
*(self._generate_embedding_for_text(opts, model, text) for text in values)
)
embeddings: list[Embedding[list[float]]] = []
total_input_tokens = 0
for embedding, input_tokens in embedding_results:
embeddings.append(embedding)
total_input_tokens += input_tokens
usage_dict: UsageDetails | None = None
if total_input_tokens > 0:
usage_dict = {"input_token_count": total_input_tokens}
return GeneratedEmbeddings(embeddings, options=options, usage=usage_dict)
async def _generate_embedding_for_text(
self,
opts: dict[str, Any],
model: str,
text: str,
) -> tuple[Embedding[list[float]], int]:
body: dict[str, Any] = {"inputText": text}
if dimensions := opts.get("dimensions"):
body["dimensions"] = dimensions
if (normalize := opts.get("normalize")) is not None:
body["normalize"] = normalize
response = await asyncio.to_thread(
self._bedrock_client.invoke_model,
modelId=model,
contentType="application/json",
accept="application/json",
body=json.dumps(body),
)
response_body = json.loads(response["body"].read())
embedding = Embedding(
vector=response_body["embedding"],
dimensions=len(response_body["embedding"]),
model_id=model,
)
input_tokens = int(response_body.get("inputTextTokenCount", 0))
return embedding, input_tokens
class BedrockEmbeddingClient(
EmbeddingTelemetryLayer[str, list[float], BedrockEmbeddingOptionsT],
RawBedrockEmbeddingClient[BedrockEmbeddingOptionsT],
Generic[BedrockEmbeddingOptionsT],
):
"""Bedrock embedding client with telemetry support.
Uses the Amazon Titan Embeddings model via Bedrock's invoke_model API.
Keyword Args:
model_id: The Bedrock embedding model ID (e.g. "amazon.titan-embed-text-v2:0").
Can also be set via environment variable BEDROCK_EMBEDDING_MODEL_ID.
region: AWS region. Defaults to "us-east-1".
Can also be set via environment variable BEDROCK_REGION.
access_key: AWS access key for manual credential injection.
secret_key: AWS secret key paired with access_key.
session_token: AWS session token for temporary credentials.
client: Preconfigured Bedrock runtime client.
boto3_session: Custom boto3 session used to build the runtime client.
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
Examples:
.. code-block:: python
from agent_framework_bedrock import BedrockEmbeddingClient
# Using default AWS credentials
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
)
# Generate embeddings
result = await client.get_embeddings(["Hello, world!"])
print(result[0].vector)
"""
OTEL_PROVIDER_NAME: ClassVar[str] = "aws.bedrock" # type: ignore[reportIncompatibleVariableOverride, misc]
def __init__(
self,
*,
region: str | None = None,
model_id: str | None = None,
access_key: str | None = None,
secret_key: str | None = None,
session_token: str | None = None,
client: BaseClient | None = None,
boto3_session: Boto3Session | None = None,
otel_provider_name: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize a Bedrock embedding client."""
super().__init__(
region=region,
model_id=model_id,
access_key=access_key,
secret_key=secret_key,
session_token=session_token,
client=client,
boto3_session=boto3_session,
otel_provider_name=otel_provider_name,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
**kwargs,
)
+5 -3
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.0b260219"
version = "1.0.0b260225"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,12 +23,11 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"boto3>=1.35.0,<2.0.0",
"botocore>=1.35.0,<2.0.0",
]
[tool.uv]
prerelease = "if-necessary-or-explicit"
environments = [
@@ -46,6 +45,9 @@ addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
markers = [
"integration: marks tests as integration tests that require external services",
]
timeout = 120
[tool.ruff]
@@ -0,0 +1,169 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import json
import os
from typing import Any
from unittest.mock import MagicMock
import pytest
from agent_framework import Embedding, GeneratedEmbeddings
from agent_framework_bedrock import BedrockEmbeddingClient, BedrockEmbeddingOptions
class _StubBedrockEmbeddingRuntime:
"""Stub for the Bedrock runtime client that handles invoke_model for embeddings."""
def __init__(self) -> None:
self.calls: list[dict[str, Any]] = []
self.meta = MagicMock(endpoint_url="https://bedrock-runtime.us-west-2.amazonaws.com")
def invoke_model(self, **kwargs: Any) -> dict[str, Any]:
self.calls.append(kwargs)
body = json.loads(kwargs.get("body", "{}"))
# Simulate Titan embedding response
dimensions = body.get("dimensions", 3)
return {
"body": MagicMock(
read=lambda: json.dumps({
"embedding": [0.1 * (i + 1) for i in range(dimensions)],
"inputTextTokenCount": 5,
}).encode()
),
}
async def test_bedrock_embedding_construction() -> None:
"""Test construction with explicit parameters."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
assert client.model_id == "amazon.titan-embed-text-v2:0"
assert client.region == "us-west-2"
async def test_bedrock_embedding_construction_missing_model_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that missing model_id raises an error."""
monkeypatch.delenv("BEDROCK_EMBEDDING_MODEL_ID", raising=False)
from agent_framework.exceptions import SettingNotFoundError
with pytest.raises(SettingNotFoundError):
BedrockEmbeddingClient(region="us-west-2")
async def test_bedrock_embedding_get_embeddings() -> None:
"""Test generating embeddings via the Bedrock invoke_model API."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
result = await client.get_embeddings(["hello", "world"])
assert isinstance(result, GeneratedEmbeddings)
assert len(result) == 2
assert len(result[0].vector) == 3
assert len(result[1].vector) == 3
assert result[0].model_id == "amazon.titan-embed-text-v2:0"
assert result.usage == {"input_token_count": 10}
# Two calls since Titan processes one input at a time
assert len(stub.calls) == 2
call_texts = {json.loads(call["body"])["inputText"] for call in stub.calls}
assert call_texts == {"hello", "world"}
async def test_bedrock_embedding_get_embeddings_empty_input() -> None:
"""Test generating embeddings with empty input."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
result = await client.get_embeddings([])
assert isinstance(result, GeneratedEmbeddings)
assert len(result) == 0
assert len(stub.calls) == 0
async def test_bedrock_embedding_get_embeddings_with_options() -> None:
"""Test generating embeddings with custom options."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
options: BedrockEmbeddingOptions = {
"dimensions": 5,
"normalize": True,
}
result = await client.get_embeddings(["hello"], options=options)
assert len(result) == 1
assert len(result[0].vector) == 5
body = json.loads(stub.calls[0]["body"])
assert body["dimensions"] == 5
assert body["normalize"] is True
async def test_bedrock_embedding_get_embeddings_no_model_raises() -> None:
"""Test that missing model_id at call time raises ValueError."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
region="us-west-2",
client=stub,
)
client.model_id = None # type: ignore[assignment]
with pytest.raises(ValueError, match="model_id is required"):
await client.get_embeddings(["hello"])
async def test_bedrock_embedding_default_region() -> None:
"""Test that default region is us-east-1."""
stub = _StubBedrockEmbeddingRuntime()
client = BedrockEmbeddingClient(
model_id="amazon.titan-embed-text-v2:0",
client=stub,
)
assert client.region == "us-east-1"
# region: Integration Tests
skip_if_bedrock_embedding_integration_tests_disabled = pytest.mark.skipif(
os.getenv("BEDROCK_EMBEDDING_MODEL_ID", "") in ("", "test-model")
or not (os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("BEDROCK_ACCESS_KEY")),
reason="No real Bedrock embedding model or AWS credentials provided; skipping integration tests.",
)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_bedrock_embedding_integration_tests_disabled
async def test_bedrock_embedding_integration() -> None:
"""Integration test for Bedrock embedding client."""
client = BedrockEmbeddingClient()
result = await client.get_embeddings(["Hello, world!", "How are you?"])
assert isinstance(result, GeneratedEmbeddings)
assert len(result) == 2
for embedding in result:
assert isinstance(embedding, Embedding)
assert isinstance(embedding.vector, list)
assert len(embedding.vector) > 0
assert all(isinstance(v, float) for v in embedding.vector)
+7 -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"openai-chatkit>=1.4.0,<2.0.0",
]
@@ -36,6 +36,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -43,6 +44,9 @@ asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -80,6 +84,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_chatkit"
test = "pytest --cov=agent_framework_chatkit --cov-report=term-missing:skip-covered tests"
+7 -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"claude-agent-sdk>=0.1.25",
]
@@ -37,6 +37,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -46,6 +47,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -80,6 +84,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_claude"
test = "pytest --cov=agent_framework_claude --cov-report=term-missing:skip-covered tests"
+7 -3
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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"microsoft-agents-copilotstudio-client>=0.3.1",
]
@@ -37,6 +37,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -46,6 +47,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -58,7 +62,6 @@ omit = [
[tool.pyright]
extends = "../../pyproject.toml"
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
@@ -80,6 +83,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_copilotstudio"
test = "pytest --cov=agent_framework_copilotstudio --cov-report=term-missing:skip-covered tests"
+1 -1
View File
@@ -220,7 +220,7 @@ if __name__ == "__main__":
- [Getting Started with Agents](../../samples/02-agents): Basic agent creation and tool usage
- [Chat Client Examples](../../samples/02-agents/chat_client): Direct chat client usage patterns
- [Azure AI Integration](https://github.com/microsoft/agent-framework/tree/main/python/packages/azure-ai): Azure AI integration
- [.NET Workflows Samples](../../../dotnet/samples/GettingStarted/Workflows): Advanced multi-agent patterns (.NET)
- [Workflows Samples](../../samples/03-workflows): Advanced multi-agent patterns
## Agent Framework Documentation
@@ -667,16 +667,12 @@ class SupportsFileSearchTool(Protocol):
# region SupportsGetEmbeddings Protocol
# Contravariant/covariant TypeVars for the Protocol
# Contravariant TypeVars for the Protocol
EmbeddingInputContraT = TypeVar(
"EmbeddingInputContraT",
default="str",
contravariant=True,
)
EmbeddingCoT = TypeVar(
"EmbeddingCoT",
default="list[float]",
)
EmbeddingOptionsContraT = TypeVar(
"EmbeddingOptionsContraT",
bound=TypedDict, # type: ignore[valid-type]
@@ -686,7 +682,7 @@ EmbeddingOptionsContraT = TypeVar(
@runtime_checkable
class SupportsGetEmbeddings(Protocol[EmbeddingInputContraT, EmbeddingCoT, EmbeddingOptionsContraT]):
class SupportsGetEmbeddings(Protocol[EmbeddingInputContraT, EmbeddingT, EmbeddingOptionsContraT]):
"""Protocol for an embedding client that can generate embeddings.
This protocol enables duck-typing for embedding generation. Any class that
@@ -714,7 +710,7 @@ class SupportsGetEmbeddings(Protocol[EmbeddingInputContraT, EmbeddingCoT, Embedd
values: Sequence[EmbeddingInputContraT],
*,
options: EmbeddingOptionsContraT | None = None,
) -> Awaitable[GeneratedEmbeddings[EmbeddingCoT]]:
) -> Awaitable[GeneratedEmbeddings[EmbeddingT]]:
"""Generate embeddings for the given values.
Args:
@@ -733,15 +729,15 @@ class SupportsGetEmbeddings(Protocol[EmbeddingInputContraT, EmbeddingCoT, Embedd
# region BaseEmbeddingClient
# Covariant for the BaseEmbeddingClient
EmbeddingOptionsCoT = TypeVar(
"EmbeddingOptionsCoT",
EmbeddingOptionsT = TypeVar(
"EmbeddingOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="EmbeddingGenerationOptions",
covariant=True,
)
class BaseEmbeddingClient(SerializationMixin, ABC, Generic[EmbeddingInputT, EmbeddingT, EmbeddingOptionsCoT]):
class BaseEmbeddingClient(SerializationMixin, ABC, Generic[EmbeddingInputT, EmbeddingT, EmbeddingOptionsT]):
"""Abstract base class for embedding clients.
Subclasses implement ``get_embeddings`` to provide the actual
@@ -785,7 +781,7 @@ class BaseEmbeddingClient(SerializationMixin, ABC, Generic[EmbeddingInputT, Embe
self,
values: Sequence[EmbeddingInputT],
*,
options: EmbeddingOptionsCoT | None = None,
options: EmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[EmbeddingT]:
"""Generate embeddings for the given values.
@@ -115,6 +115,7 @@ class _FileAgentSkill:
source_path: str
resource_names: list[str] = field(default_factory=list)
# endregion
# region Private module-level functions (skill discovery, parsing, security)
@@ -165,9 +166,7 @@ def _has_symlink_in_path(full_path: str, directory_path: str) -> bool:
try:
relative = Path(full_path).relative_to(dir_path)
except ValueError as exc:
raise ValueError(
f"full_path {full_path!r} does not start with directory_path {directory_path!r}"
) from exc
raise ValueError(f"full_path {full_path!r} does not start with directory_path {directory_path!r}") from exc
current = dir_path
for part in relative.parts:
@@ -436,6 +435,7 @@ def _build_skills_instruction_prompt(
return template.format("\n".join(lines))
# endregion
# region Public API
@@ -494,7 +494,9 @@ class FileAgentSkillsProvider(BaseContextProvider):
"""
super().__init__(source_id or self.DEFAULT_SOURCE_ID)
resolved_paths: Sequence[str] = [str(skill_paths)] if isinstance(skill_paths, (str, Path)) else [str(p) for p in skill_paths]
resolved_paths: Sequence[str] = (
[str(skill_paths)] if isinstance(skill_paths, (str, Path)) else [str(p) for p in skill_paths]
)
self._skills = _discover_and_load_skills(resolved_paths)
self._skills_instruction_prompt = _build_skills_instruction_prompt(skills_instruction_prompt, self._skills)
@@ -594,4 +596,5 @@ class FileAgentSkillsProvider(BaseContextProvider):
logger.exception("Failed to read resource '%s' from skill '%s'", resource_name, skill_name)
return f"Error: Failed to read resource '{resource_name}' from skill '{skill_name}'."
# endregion
@@ -377,6 +377,12 @@ class UsageDetails(TypedDict, total=False):
This is a non-closed dictionary, so any specific provider fields can be added as needed.
Whenever they can be mapped to standard fields, they will be.
Keys:
input_token_count: The number of input tokens used.
output_token_count: The number of output tokens generated.
total_token_count: The total number of tokens (input + output).
"""
input_token_count: int | None
@@ -3289,7 +3295,7 @@ class GeneratedEmbeddings(list[Embedding[EmbeddingT]], Generic[EmbeddingT, Embed
embeddings: Iterable[Embedding[EmbeddingT]] | None = None,
*,
options: EmbeddingOptionsT | None = None,
usage: dict[str, Any] | None = None,
usage: UsageDetails | None = None,
additional_properties: dict[str, Any] | None = None,
) -> None:
super().__init__(embeddings or [])
@@ -8,6 +8,9 @@ This module lazily re-exports objects from:
Supported classes:
- BedrockChatClient
- BedrockChatOptions
- BedrockEmbeddingClient
- BedrockEmbeddingOptions
- BedrockEmbeddingSettings
- BedrockGuardrailConfig
- BedrockSettings
"""
@@ -17,7 +20,15 @@ from typing import Any
IMPORT_PATH = "agent_framework_bedrock"
PACKAGE_NAME = "agent-framework-bedrock"
_IMPORTS = ["BedrockChatClient", "BedrockChatOptions", "BedrockGuardrailConfig", "BedrockSettings"]
_IMPORTS = [
"BedrockChatClient",
"BedrockChatOptions",
"BedrockEmbeddingClient",
"BedrockEmbeddingOptions",
"BedrockEmbeddingSettings",
"BedrockGuardrailConfig",
"BedrockSettings",
]
def __getattr__(name: str) -> Any:
@@ -3,6 +3,9 @@
from agent_framework_bedrock import (
BedrockChatClient,
BedrockChatOptions,
BedrockEmbeddingClient,
BedrockEmbeddingOptions,
BedrockEmbeddingSettings,
BedrockGuardrailConfig,
BedrockSettings,
)
@@ -10,6 +13,9 @@ from agent_framework_bedrock import (
__all__ = [
"BedrockChatClient",
"BedrockChatOptions",
"BedrockEmbeddingClient",
"BedrockEmbeddingOptions",
"BedrockEmbeddingSettings",
"BedrockGuardrailConfig",
"BedrockSettings",
]
@@ -99,6 +99,7 @@ class AzureOpenAIEmbeddingClient(
credential: AzureCredentialTypes | AzureTokenProvider | None = None,
default_headers: Mapping[str, str] | None = None,
async_client: AsyncAzureOpenAI | None = None,
otel_provider_name: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
@@ -133,4 +134,5 @@ class AzureOpenAIEmbeddingClient(
credential=credential,
default_headers=default_headers,
client=async_client,
otel_provider_name=otel_provider_name,
)
@@ -1279,15 +1279,15 @@ class ChatTelemetryLayer(Generic[OptionsCoT]):
return _get_response()
EmbeddingOptionsCoT = TypeVar(
"EmbeddingOptionsCoT",
EmbeddingOptionsT = TypeVar(
"EmbeddingOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="EmbeddingGenerationOptions",
covariant=True,
)
class EmbeddingTelemetryLayer(Generic[EmbeddingInputT, EmbeddingT, EmbeddingOptionsCoT]):
class EmbeddingTelemetryLayer(Generic[EmbeddingInputT, EmbeddingT, EmbeddingOptionsT]):
"""Layer that wraps embedding client get_embeddings with OpenTelemetry tracing."""
def __init__(self, *args: Any, otel_provider_name: str | None = None, **kwargs: Any) -> None:
@@ -1301,7 +1301,7 @@ class EmbeddingTelemetryLayer(Generic[EmbeddingInputT, EmbeddingT, EmbeddingOpti
self,
values: Sequence[EmbeddingInputT],
*,
options: EmbeddingOptionsCoT | None = None,
options: EmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[EmbeddingT]:
"""Trace embedding generation with OpenTelemetry spans and metrics."""
global OBSERVABILITY_SETTINGS
@@ -7,6 +7,10 @@ This module lazily re-exports objects from:
Supported classes:
- OllamaChatClient
- OllamaChatOptions
- OllamaEmbeddingClient
- OllamaEmbeddingOptions
- OllamaEmbeddingSettings
- OllamaSettings
"""
@@ -15,7 +19,14 @@ from typing import Any
IMPORT_PATH = "agent_framework_ollama"
PACKAGE_NAME = "agent-framework-ollama"
_IMPORTS = ["OllamaChatClient", "OllamaSettings"]
_IMPORTS = [
"OllamaChatClient",
"OllamaChatOptions",
"OllamaEmbeddingClient",
"OllamaEmbeddingOptions",
"OllamaEmbeddingSettings",
"OllamaSettings",
]
def __getattr__(name: str) -> Any:
@@ -2,10 +2,18 @@
from agent_framework_ollama import (
OllamaChatClient,
OllamaChatOptions,
OllamaEmbeddingClient,
OllamaEmbeddingOptions,
OllamaEmbeddingSettings,
OllamaSettings,
)
__all__ = [
"OllamaChatClient",
"OllamaChatOptions",
"OllamaEmbeddingClient",
"OllamaEmbeddingOptions",
"OllamaEmbeddingSettings",
"OllamaSettings",
]
@@ -12,7 +12,7 @@ from openai import AsyncOpenAI
from .._clients import BaseEmbeddingClient
from .._settings import load_settings
from .._types import Embedding, EmbeddingGenerationOptions, GeneratedEmbeddings
from .._types import Embedding, EmbeddingGenerationOptions, GeneratedEmbeddings, UsageDetails
from ..observability import EmbeddingTelemetryLayer
from ._shared import OpenAIBase, OpenAIConfigMixin, OpenAISettings
@@ -116,11 +116,11 @@ class RawOpenAIEmbeddingClient(
)
)
usage_dict: dict[str, Any] | None = None
usage_dict: UsageDetails | None = None
if response.usage:
usage_dict = {
"prompt_tokens": response.usage.prompt_tokens,
"total_tokens": response.usage.total_tokens,
"input_token_count": response.usage.prompt_tokens,
"total_token_count": response.usage.total_tokens,
}
return GeneratedEmbeddings(embeddings, options=options, usage=usage_dict)
@@ -143,6 +143,7 @@ class OpenAIEmbeddingClient(
default_headers: Additional HTTP headers.
async_client: Pre-configured AsyncOpenAI client.
base_url: Custom API base URL.
otel_provider_name: Override the OpenTelemetry provider name for telemetry.
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
@@ -176,6 +177,7 @@ class OpenAIEmbeddingClient(
default_headers: Mapping[str, str] | None = None,
async_client: AsyncOpenAI | None = None,
base_url: str | None = None,
otel_provider_name: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
@@ -208,4 +210,5 @@ class OpenAIEmbeddingClient(
org_id=openai_settings["org_id"],
default_headers=default_headers,
client=async_client,
otel_provider_name=otel_provider_name,
)
+5 -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.0.0rc1"
version = "1.0.0rc2"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -91,6 +91,9 @@ asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.coverage.run]
omit = [
@@ -124,6 +127,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework"
test = "pytest --cov=agent_framework --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"
@@ -937,8 +937,7 @@ async def test_max_iterations_no_orphaned_function_calls(chat_client_base: Suppo
orphaned_calls = all_call_ids - all_result_ids
assert not orphaned_calls, (
f"Response contains orphaned FunctionCallContent without matching "
f"FunctionResultContent: {orphaned_calls}."
f"Response contains orphaned FunctionCallContent without matching FunctionResultContent: {orphaned_calls}."
)
@@ -1123,8 +1122,7 @@ async def test_max_iterations_thread_integrity_with_agent(chat_client_base: Supp
orphaned_calls = all_call_ids - all_result_ids
assert not orphaned_calls, (
f"Response contains orphaned function calls {orphaned_calls}. "
f"This would cause API errors on the next call."
f"Response contains orphaned function calls {orphaned_calls}. This would cause API errors on the next call."
)
@@ -2594,3 +2594,189 @@ async def test_agent_no_instructions_in_default_or_options(
span = spans[0]
assert OtelAttr.SYSTEM_INSTRUCTIONS not in span.attributes
# region Additional coverage tests
def test_get_instructions_from_options_none():
"""Test _get_instructions_from_options returns None for None input."""
from agent_framework.observability import _get_instructions_from_options
assert _get_instructions_from_options(None) is None
def test_get_instructions_from_options_non_dict():
"""Test _get_instructions_from_options returns None for non-dict input."""
from agent_framework.observability import _get_instructions_from_options
assert _get_instructions_from_options("not a dict") is None
assert _get_instructions_from_options(42) is None
def test_get_instructions_from_options_dict_with_instructions():
"""Test _get_instructions_from_options extracts instructions from dict."""
from agent_framework.observability import _get_instructions_from_options
assert _get_instructions_from_options({"instructions": "do stuff"}) == "do stuff"
assert _get_instructions_from_options({"other_key": "value"}) is None
def test_get_span_attributes_with_non_dict_options():
"""Test _get_span_attributes handles non-dict options gracefully."""
from agent_framework.observability import _get_span_attributes
# Pass options as a non-dict value; should not crash
attrs = _get_span_attributes(
operation_name="chat",
provider_name="test",
all_options="not_a_dict",
)
assert attrs[OtelAttr.OPERATION] == "chat"
def test_capture_response_with_error_type(span_exporter: InMemorySpanExporter):
"""Test _capture_response includes error_type in duration histogram attributes."""
from agent_framework.observability import OtelAttr, _capture_response, get_tracer
span_exporter.clear()
tracer = get_tracer()
from agent_framework.observability import _get_duration_histogram, _get_token_usage_histogram
token_histogram = _get_token_usage_histogram()
duration_histogram = _get_duration_histogram()
attrs = {
"gen_ai.request.model": "test-model",
OtelAttr.ERROR_TYPE: "ValueError",
}
with tracer.start_as_current_span("test_span") as span:
_capture_response(
span=span,
attributes=attrs,
token_usage_histogram=token_histogram,
operation_duration_histogram=duration_histogram,
duration=0.5,
)
spans = span_exporter.get_finished_spans()
assert len(spans) == 1
assert spans[0].attributes.get(OtelAttr.ERROR_TYPE) == "ValueError"
def test_configure_otel_providers_with_env_file_path(monkeypatch, tmp_path):
"""Test configure_otel_providers with env_file_path creates new settings."""
import importlib
monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)
observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)
env_file = tmp_path / ".env"
env_file.write_text("ENABLE_INSTRUMENTATION=true\n")
observability.configure_otel_providers(
env_file_path=str(env_file),
enable_sensitive_data=True,
vs_code_extension_port=None,
)
assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True
assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True
def test_configure_otel_providers_with_env_file_and_vs_code_port(monkeypatch, tmp_path):
"""Test configure_otel_providers with env_file_path and vs_code_extension_port."""
import importlib
monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)
observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)
env_file = tmp_path / ".env"
env_file.write_text("ENABLE_INSTRUMENTATION=true\n")
observability.configure_otel_providers(
env_file_path=str(env_file),
env_file_encoding="utf-8",
vs_code_extension_port=4317,
)
assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True
assert observability.OBSERVABILITY_SETTINGS.vs_code_extension_port == 4317
def test_get_exporters_from_env_with_env_file_path(monkeypatch, tmp_path):
"""Test _get_exporters_from_env loads dotenv when env_file_path is provided."""
from agent_framework.observability import _get_exporters_from_env
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)
# Create a .env file with no OTEL endpoints so it returns empty
env_file = tmp_path / ".env"
env_file.write_text("SOME_VAR=value\n")
exporters = _get_exporters_from_env(env_file_path=str(env_file))
assert exporters == []
def test_create_resource_with_env_file_path(monkeypatch, tmp_path):
"""Test create_resource loads dotenv when env_file_path is provided."""
from agent_framework.observability import create_resource
monkeypatch.delenv("OTEL_SERVICE_NAME", raising=False)
monkeypatch.delenv("OTEL_SERVICE_VERSION", raising=False)
monkeypatch.delenv("OTEL_RESOURCE_ATTRIBUTES", raising=False)
env_file = tmp_path / ".env"
env_file.write_text("OTEL_SERVICE_NAME=my_test_service\n")
resource = create_resource(env_file_path=str(env_file))
assert resource.attributes.get("service.name") == "my_test_service"
def test_get_meter_typeerror_fallback():
"""Test get_meter falls back when TypeError is raised (old OTel versions)."""
from unittest.mock import patch as mock_patch
from agent_framework.observability import get_meter
call_count = 0
def mock_get_meter(*args, **kwargs):
nonlocal call_count
call_count += 1
if "attributes" in kwargs:
raise TypeError("unexpected keyword argument 'attributes'")
from opentelemetry import metrics
return metrics.get_meter_provider().get_meter(*args, **{k: v for k, v in kwargs.items() if k != "attributes"})
with mock_patch("agent_framework.observability.metrics.get_meter", side_effect=mock_get_meter):
meter = get_meter(name="test", attributes={"key": "val"})
assert meter is not None
assert call_count == 2
@@ -38,6 +38,7 @@ def _symlinks_supported(tmp: Path) -> bool:
test_link.unlink(missing_ok=True)
test_target.unlink(missing_ok=True)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@@ -100,8 +100,8 @@ async def test_openai_get_embeddings_usage(openai_unit_test_env: None) -> None:
result = await client.get_embeddings(["test"])
assert result.usage is not None
assert result.usage["prompt_tokens"] == 10
assert result.usage["total_tokens"] == 10
assert result.usage["input_token_count"] == 10
assert result.usage["total_token_count"] == 10
async def test_openai_options_passthrough_dimensions(openai_unit_test_env: None) -> None:
@@ -284,7 +284,7 @@ async def test_integration_openai_get_embeddings() -> None:
assert all(isinstance(v, float) for v in result[0].vector)
assert result[0].model_id is not None
assert result.usage is not None
assert result.usage["prompt_tokens"] > 0
assert result.usage["input_token_count"] > 0
@skip_if_openai_integration_tests_disabled
@@ -327,7 +327,7 @@ async def test_integration_azure_openai_get_embeddings() -> None:
assert all(isinstance(v, float) for v in result[0].vector)
assert result[0].model_id is not None
assert result.usage is not None
assert result.usage["prompt_tokens"] > 0
assert result.usage["input_token_count"] > 0
@skip_if_azure_openai_integration_tests_disabled
+7 -3
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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"powerfx>=0.0.31; python_version < '3.14'",
"pyyaml>=6.0,<7.0",
]
@@ -39,9 +39,9 @@ environments = [
"sys_platform == 'win32'"
]
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -51,6 +51,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -88,6 +91,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_declarative"
test = "pytest --cov=agent_framework_declarative --cov-report=term-missing:skip-covered tests"
+6 -3
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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"python-dotenv>=1.0.0",
@@ -54,6 +54,9 @@ addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -66,7 +69,6 @@ omit = [
[tool.pyright]
extends = "../../pyproject.toml"
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
@@ -89,6 +91,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_devui"
test = "pytest --cov=agent_framework_devui --cov-report=term-missing:skip-covered tests"
+4 -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"durabletask>=1.3.0",
"durabletask-azuremanaged>=1.3.0",
"python-dateutil>=2.8.0",
@@ -43,6 +43,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
pythonpath = ["tests/integration_tests"]
@@ -94,6 +95,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_durabletask"
test = "pytest --cov=agent_framework_durabletask --cov-report=term-missing:skip-covered tests"
+7 -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"foundry-local-sdk>=0.5.1,<1",
]
@@ -37,6 +37,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -44,6 +45,9 @@ asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -78,6 +82,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_foundry_local"
test = "pytest --cov=agent_framework_foundry_local --cov-report=term-missing:skip-covered tests"
@@ -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"github-copilot-sdk>=0.1.0",
]
@@ -37,6 +37,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -46,6 +47,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -58,7 +62,6 @@ omit = [
[tool.pyright]
extends = "../../pyproject.toml"
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
@@ -80,6 +83,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_github_copilot"
test = "pytest --cov=agent_framework_github_copilot --cov-report=term-missing:skip-covered tests"
+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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
]
[project.optional-dependencies]
+7 -3
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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"mem0ai>=1.0.0",
]
@@ -37,6 +37,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -46,6 +47,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -58,7 +62,6 @@ omit = [
[tool.pyright]
extends = "../../pyproject.toml"
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
@@ -80,6 +83,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_mem0"
test = "pytest --cov=agent_framework_mem0 --cov-report=term-missing:skip-covered tests"
@@ -3,6 +3,7 @@
import importlib.metadata
from ._chat_client import OllamaChatClient, OllamaChatOptions, OllamaSettings
from ._embedding_client import OllamaEmbeddingClient, OllamaEmbeddingOptions, OllamaEmbeddingSettings
try:
__version__ = importlib.metadata.version(__name__)
@@ -12,6 +13,9 @@ except importlib.metadata.PackageNotFoundError:
__all__ = [
"OllamaChatClient",
"OllamaChatOptions",
"OllamaEmbeddingClient",
"OllamaEmbeddingOptions",
"OllamaEmbeddingSettings",
"OllamaSettings",
"__version__",
]
@@ -0,0 +1,230 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
import sys
from collections.abc import Sequence
from typing import Any, ClassVar, Generic, TypedDict
from agent_framework import (
BaseEmbeddingClient,
Embedding,
EmbeddingGenerationOptions,
GeneratedEmbeddings,
UsageDetails,
load_settings,
)
from agent_framework.observability import EmbeddingTelemetryLayer
from ollama import AsyncClient
if sys.version_info >= (3, 13):
from typing import TypeVar # type: ignore # pragma: no cover
else:
from typing_extensions import TypeVar # type: ignore # pragma: no cover
logger = logging.getLogger("agent_framework.ollama")
class OllamaEmbeddingOptions(EmbeddingGenerationOptions, total=False):
"""Ollama-specific embedding options.
Extends EmbeddingGenerationOptions with Ollama-specific fields.
Examples:
.. code-block:: python
from agent_framework_ollama import OllamaEmbeddingOptions
options: OllamaEmbeddingOptions = {
"model_id": "nomic-embed-text",
"dimensions": 768,
"truncate": True,
}
"""
truncate: bool
"""Whether to truncate input text that exceeds the model's context length.
When True, input that is too long will be silently truncated.
When False (default), the request will fail if input exceeds the context length.
"""
keep_alive: float | str
"""How long to keep the model loaded in memory (e.g. ``"5m"``, ``300``)."""
OllamaEmbeddingOptionsT = TypeVar(
"OllamaEmbeddingOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="OllamaEmbeddingOptions",
covariant=True,
)
class OllamaEmbeddingSettings(TypedDict, total=False):
"""Ollama embedding settings."""
host: str | None
embedding_model_id: str | None
class RawOllamaEmbeddingClient(
BaseEmbeddingClient[str, list[float], OllamaEmbeddingOptionsT],
Generic[OllamaEmbeddingOptionsT],
):
"""Raw Ollama embedding client without telemetry.
Keyword Args:
model_id: The Ollama embedding model ID (e.g. "nomic-embed-text").
Can also be set via environment variable OLLAMA_EMBEDDING_MODEL_ID.
host: Ollama server URL. Defaults to http://localhost:11434.
Can also be set via environment variable OLLAMA_HOST.
client: Optional pre-configured Ollama AsyncClient.
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
"""
def __init__(
self,
*,
model_id: str | None = None,
host: str | None = None,
client: AsyncClient | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize a raw Ollama embedding client."""
ollama_settings = load_settings(
OllamaEmbeddingSettings,
env_prefix="OLLAMA_",
required_fields=["embedding_model_id"],
host=host,
embedding_model_id=model_id,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
self.model_id = ollama_settings["embedding_model_id"]
self.client = client or AsyncClient(host=ollama_settings.get("host"))
self.host = str(self.client._client.base_url) # pyright: ignore[reportUnknownMemberType,reportPrivateUsage,reportUnknownArgumentType]
super().__init__(**kwargs)
def service_url(self) -> str:
"""Get the URL of the service."""
return self.host
async def get_embeddings(
self,
values: Sequence[str],
*,
options: OllamaEmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[list[float]]:
"""Call the Ollama embed API.
Args:
values: The text values to generate embeddings for.
options: Optional embedding generation options.
Returns:
Generated embeddings with usage metadata.
Raises:
ValueError: If model_id is not provided or values is empty.
"""
if not values:
return GeneratedEmbeddings([], options=options)
opts: dict[str, Any] = dict(options) if options else {}
model = opts.get("model_id") or self.model_id
if not model:
raise ValueError("model_id is required")
kwargs: dict[str, Any] = {"model": model, "input": list(values)}
if (truncate := opts.get("truncate")) is not None:
kwargs["truncate"] = truncate
if keep_alive := opts.get("keep_alive"):
kwargs["keep_alive"] = keep_alive
if dimensions := opts.get("dimensions"):
kwargs["dimensions"] = dimensions
response = await self.client.embed(**kwargs)
embeddings = [
Embedding(
vector=list(emb),
dimensions=len(emb),
model_id=response.get("model") or model,
)
for emb in response.get("embeddings", [])
]
usage_dict: UsageDetails | None = None
prompt_eval_count = response.get("prompt_eval_count")
if prompt_eval_count is not None:
usage_dict = {"input_token_count": prompt_eval_count}
return GeneratedEmbeddings(embeddings, options=options, usage=usage_dict)
class OllamaEmbeddingClient(
EmbeddingTelemetryLayer[str, list[float], OllamaEmbeddingOptionsT],
RawOllamaEmbeddingClient[OllamaEmbeddingOptionsT],
Generic[OllamaEmbeddingOptionsT],
):
"""Ollama embedding client with telemetry support.
Keyword Args:
model_id: The Ollama embedding model ID (e.g. "nomic-embed-text").
Can also be set via environment variable OLLAMA_EMBEDDING_MODEL_ID.
host: Ollama server URL. Defaults to http://localhost:11434.
Can also be set via environment variable OLLAMA_HOST.
client: Optional pre-configured Ollama AsyncClient.
env_file_path: Path to .env file for settings.
env_file_encoding: Encoding for .env file.
Examples:
.. code-block:: python
from agent_framework_ollama import OllamaEmbeddingClient
# Using environment variables
# Set OLLAMA_EMBEDDING_MODEL_ID=nomic-embed-text
client = OllamaEmbeddingClient()
# Or passing parameters directly
client = OllamaEmbeddingClient(
model_id="nomic-embed-text",
host="http://localhost:11434",
)
# Generate embeddings
result = await client.get_embeddings(["Hello, world!"])
print(result[0].vector)
"""
OTEL_PROVIDER_NAME: ClassVar[str] = "ollama"
def __init__(
self,
*,
model_id: str | None = None,
host: str | None = None,
client: AsyncClient | None = None,
otel_provider_name: str | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
**kwargs: Any,
) -> None:
"""Initialize an Ollama embedding client."""
super().__init__(
model_id=model_id,
host=host,
client=client,
otel_provider_name=otel_provider_name,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
**kwargs,
)
+7 -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"ollama >= 0.5.3",
]
@@ -37,12 +37,16 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
markers = [
"integration: marks tests as integration tests that require external services",
]
timeout = 120
[tool.ruff]
@@ -82,6 +86,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_ollama"
test = "pytest --cov=agent_framework_ollama --cov-report=term-missing:skip-covered tests"
@@ -0,0 +1,150 @@
# Copyright (c) Microsoft. All rights reserved.
import os
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import Embedding, GeneratedEmbeddings
from agent_framework_ollama import OllamaEmbeddingClient, OllamaEmbeddingOptions
# region: Unit Tests
def test_ollama_embedding_construction(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test construction with explicit parameters."""
monkeypatch.setenv("OLLAMA_EMBEDDING_MODEL_ID", "nomic-embed-text")
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client_cls.return_value = MagicMock()
client = OllamaEmbeddingClient()
assert client.model_id == "nomic-embed-text"
def test_ollama_embedding_construction_with_params() -> None:
"""Test construction with explicit parameters."""
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client_cls.return_value = MagicMock()
client = OllamaEmbeddingClient(
model_id="nomic-embed-text",
host="http://localhost:11434",
)
assert client.model_id == "nomic-embed-text"
def test_ollama_embedding_construction_missing_model_raises(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that missing model_id raises an error."""
monkeypatch.delenv("OLLAMA_EMBEDDING_MODEL_ID", raising=False)
monkeypatch.delenv("OLLAMA_MODEL_ID", raising=False)
from agent_framework.exceptions import SettingNotFoundError
with pytest.raises(SettingNotFoundError):
OllamaEmbeddingClient()
async def test_ollama_embedding_get_embeddings() -> None:
"""Test generating embeddings via the Ollama API."""
mock_response = {
"model": "nomic-embed-text",
"embeddings": [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]],
"prompt_eval_count": 10,
}
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client = MagicMock()
mock_client.embed = AsyncMock(return_value=mock_response)
mock_client_cls.return_value = mock_client
client = OllamaEmbeddingClient(model_id="nomic-embed-text")
result = await client.get_embeddings(["hello", "world"])
assert isinstance(result, GeneratedEmbeddings)
assert len(result) == 2
assert result[0].vector == [0.1, 0.2, 0.3]
assert result[1].vector == [0.4, 0.5, 0.6]
assert result[0].model_id == "nomic-embed-text"
assert result.usage == {"input_token_count": 10}
mock_client.embed.assert_called_once_with(
model="nomic-embed-text",
input=["hello", "world"],
)
async def test_ollama_embedding_get_embeddings_empty_input() -> None:
"""Test generating embeddings with empty input."""
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
client = OllamaEmbeddingClient(model_id="nomic-embed-text")
result = await client.get_embeddings([])
assert isinstance(result, GeneratedEmbeddings)
assert len(result) == 0
mock_client.embed.assert_not_called()
async def test_ollama_embedding_get_embeddings_with_options() -> None:
"""Test generating embeddings with custom options."""
mock_response = {
"model": "nomic-embed-text",
"embeddings": [[0.1, 0.2, 0.3]],
}
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client = MagicMock()
mock_client.embed = AsyncMock(return_value=mock_response)
mock_client_cls.return_value = mock_client
client = OllamaEmbeddingClient(model_id="nomic-embed-text")
options: OllamaEmbeddingOptions = {
"truncate": True,
"dimensions": 512,
}
result = await client.get_embeddings(["hello"], options=options)
assert len(result) == 1
mock_client.embed.assert_called_once_with(
model="nomic-embed-text",
input=["hello"],
truncate=True,
dimensions=512,
)
async def test_ollama_embedding_get_embeddings_no_model_raises() -> None:
"""Test that missing model_id at call time raises ValueError."""
with patch("agent_framework_ollama._embedding_client.AsyncClient") as mock_client_cls:
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
client = OllamaEmbeddingClient(model_id="nomic-embed-text")
client.model_id = None # type: ignore[assignment]
with pytest.raises(ValueError, match="model_id is required"):
await client.get_embeddings(["hello"])
# region: Integration Tests
skip_if_ollama_embedding_integration_tests_disabled = pytest.mark.skipif(
os.getenv("OLLAMA_EMBEDDING_MODEL_ID", "") in ("", "test-model"),
reason="No real Ollama embedding model provided; skipping integration tests.",
)
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_ollama_embedding_integration_tests_disabled
async def test_ollama_embedding_integration() -> None:
"""Integration test for Ollama embedding client."""
client = OllamaEmbeddingClient()
result = await client.get_embeddings(["Hello, world!", "How are you?"])
assert isinstance(result, GeneratedEmbeddings)
assert len(result) == 2
for embedding in result:
assert isinstance(embedding, Embedding)
assert isinstance(embedding.vector, list)
assert len(embedding.vector) > 0
assert all(isinstance(v, float) for v in embedding.vector)
@@ -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
]
[tool.uv]
@@ -44,6 +44,9 @@ asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -78,6 +81,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_orchestrations"
test = "pytest --cov=agent_framework_orchestrations --cov-report=term-missing:skip-covered -n auto --dist worksteal tests"
@@ -106,7 +106,7 @@ class PurviewPolicyMiddleware(AgentMiddleware):
if context.result and not context.stream:
should_block_response, _ = await self._processor.process_messages(
context.result.messages, # type: ignore[union-attr]
Activity.UPLOAD_TEXT,
Activity.DOWNLOAD_TEXT,
session_id=session_id,
user_id=resolved_user_id,
)
@@ -210,7 +210,7 @@ class PurviewChatPolicyMiddleware(ChatMiddleware):
messages = getattr(result_obj, "messages", None)
if messages:
should_block_response, _ = await self._processor.process_messages(
messages, Activity.UPLOAD_TEXT, session_id=session_id_response, user_id=resolved_user_id
messages, Activity.DOWNLOAD_TEXT, session_id=session_id_response, user_id=resolved_user_id
)
if should_block_response:
from agent_framework import ChatResponse, Message
@@ -158,7 +158,8 @@ class ScopedContentProcessor:
name=f"Agent Framework Message {message_id}",
is_truncated=False,
correlation_id=correlation_id,
sequence_number=time.time_ns(),
# This would be c# ticks equivalent and needs to fit inside c# long
sequence_number=time.time_ns() // 100 + 621355968000000000,
)
activity_meta = ActivityMetadata(activity=activity)
+7 -3
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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"azure-core>=1.30.0",
"httpx>=0.27.0",
]
@@ -39,12 +39,16 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
filterwarnings = []
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -57,7 +61,6 @@ omit = [
[tool.pyright]
extends = "../../pyproject.toml"
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
@@ -79,6 +82,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_purview"
test = "pytest --cov=agent_framework_purview --cov-report=term-missing:skip-covered tests"
@@ -148,8 +148,10 @@ class TestPurviewPolicyMiddleware:
await middleware.process(context, mock_next)
assert mock_process.call_count == 2
for call in mock_process.call_args_list:
assert call[0][1] == Activity.UPLOAD_TEXT
# First call (pre-check) should be UPLOAD_TEXT for user prompt
assert mock_process.call_args_list[0][0][1] == Activity.UPLOAD_TEXT
# Second call (post-check) should be DOWNLOAD_TEXT for agent response
assert mock_process.call_args_list[1][0][1] == Activity.DOWNLOAD_TEXT
async def test_middleware_streaming_skips_post_check(
self, middleware: PurviewPolicyMiddleware, mock_agent: MagicMock
+7 -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.0b260219"
version = "1.0.0b260225"
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.0.0rc1",
"agent-framework-core>=1.0.0rc2",
"redis>=6.4.0",
"redisvl>=0.8.2",
"numpy>=2.2.6"
@@ -39,6 +39,7 @@ environments = [
[tool.uv-dynamic-versioning]
fallback-version = "0.0.0"
[tool.pytest.ini_options]
testpaths = 'tests'
addopts = "-ra -q -r fEX"
@@ -48,6 +49,9 @@ filterwarnings = [
"ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*"
]
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
@@ -81,6 +85,7 @@ exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks]
mypy = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_redis"
test = "pytest --cov=agent_framework_redis --cov-report=term-missing:skip-covered tests"
+2 -2
View File
@@ -4,7 +4,7 @@ description = "Microsoft Agent Framework for building AI Agents with Python. Thi
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md"
requires-python = ">=3.10"
version = "1.0.0rc1"
version = "1.0.0rc2"
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.0.0rc1",
"agent-framework-core[all]==1.0.0rc2",
]
[dependency-groups]
@@ -0,0 +1,87 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "agent-framework-azure-ai",
# ]
# ///
# Run with: uv run samples/02-agents/embeddings/azure_ai_inference_embeddings.py
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import pathlib
from agent_framework import Content
from agent_framework_azure_ai import AzureAIInferenceEmbeddingClient
from dotenv import load_dotenv
load_dotenv()
"""Azure AI Inference Image Embedding Example
This sample demonstrates how to generate image embeddings using the
Azure AI Inference embedding client with the Cohere-embed-v3-english model.
Images are passed as ``Content`` objects created with ``Content.from_data()``.
Prerequisites:
Set the following environment variables or add them to a .env file:
- AZURE_AI_INFERENCE_ENDPOINT: Your Azure AI model inference endpoint URL
- AZURE_AI_INFERENCE_API_KEY: Your API key
- AZURE_AI_INFERENCE_EMBEDDING_MODEL_ID: The text embedding model name
(e.g. "text-embedding-3-small")
- AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL_ID: The image embedding model name
(e.g. "Cohere-embed-v3-english")
"""
SAMPLE_IMAGE_PATH = pathlib.Path(__file__).parent.parent.parent / "shared" / "sample_assets" / "sample_image.jpg"
async def main() -> None:
"""Generate image embeddings with Azure AI Inference."""
async with AzureAIInferenceEmbeddingClient() as client:
# 1. Generate an image embedding.
image_bytes = SAMPLE_IMAGE_PATH.read_bytes()
image_content = Content.from_data(data=image_bytes, media_type="image/jpeg")
result = await client.get_embeddings([image_content])
print(f"Image embedding dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
print(f"Model: {result[0].model_id}")
print(f"Usage: {result.usage}")
print()
# 2. Generate image and text embeddings separately in one call.
# The client dispatches text to the text endpoint and images to the image
# endpoint, then reassembles results in the original input order.
result = await client.get_embeddings(["A half-timbered house in a forested valley", image_content])
print(f"Text embedding dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
print(f"Image embedding dimensions: {result[1].dimensions}")
print(f"First 5 values: {result[1].vector[:5]}")
print()
# 3. Generate image embeddings with input_type option.
result = await client.get_embeddings(
[image_content],
options={"input_type": "document"},
)
print(f"Document embedding dimensions: {result[0].dimensions}")
print(f"First 5 values: {result[0].vector[:5]}")
if __name__ == "__main__":
asyncio.run(main())
"""
Sample output (using Cohere-embed-v3-english):
Image embedding dimensions: 1024
First 5 values: [0.023, -0.045, 0.067, -0.089, 0.011]
Model: Cohere-embed-v3-english
Usage: {'prompt_tokens': 1, 'total_tokens': 1}
Image+text (separate) results:
Text embedding dimensions: 1536
Image embedding dimensions: 1024
Document embedding dimensions: 1024
"""
+8
View File
@@ -160,6 +160,14 @@ Sequential orchestration uses a few small adapter nodes for plumbing:
These may appear in event streams (executor_invoked/executor_completed). They're analogous to
concurrents dispatcher and aggregator and can be ignored if you only care about agent activity.
### AzureOpenAIResponsesClient vs AzureAIAgent
Workflow and orchestration samples use `AzureOpenAIResponsesClient` rather than the CRUD-style `AzureAIAgent` client. The key difference:
- **`AzureOpenAIResponsesClient`** — A lightweight client that uses the underlying Agent Service V2 (Responses API) for non-CRUD-style agents. Orchestrations use this client because agents are created locally and do not require server-side lifecycle management (create/update/delete). This is the recommended client for orchestration patterns (Sequential, Concurrent, Handoff, GroupChat, Magentic).
- **`AzureAIAgent`** — A CRUD-style client for server-managed agents. Use this when you need persistent, server-side agent definitions with features like file search, code interpreter sessions, or thread management provided by the Azure AI Agent Service.
### Environment Variables
Workflow samples that use `AzureOpenAIResponsesClient` expect:
@@ -18,10 +18,11 @@ Run with:
"""
import asyncio
import os
from pathlib import Path
from typing import Any
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.declarative import WorkflowFactory
from azure.identity import AzureCliCredential
from pydantic import BaseModel, Field
@@ -196,8 +197,12 @@ def format_order_confirmation(order_data: dict[str, Any], order_calculation: dic
async def main():
"""Run the agent to function tool workflow."""
# Create Azure OpenAI client
chat_client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create Azure OpenAI Responses client
chat_client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create the order analysis agent with structured output
order_analysis_agent = chat_client.as_agent(
@@ -6,7 +6,7 @@ This sample demonstrates an agent with function tools responding to user queries
The workflow showcases:
- **Function Tools**: Agent equipped with tools to query menu data
- **Real Azure OpenAI Agent**: Uses `AzureOpenAIChatClient` to create an agent with tools
- **Real Azure OpenAI Agent**: Uses `AzureOpenAIResponsesClient` to create an agent with tools
- **Agent Registration**: Shows how to register agents with the `WorkflowFactory`
## Tools
@@ -72,7 +72,11 @@ Session Complete
```python
# Create the agent with tools
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
menu_agent = client.as_agent(
name="MenuAgent",
instructions="You are a helpful restaurant menu assistant...",
@@ -92,6 +92,10 @@ from agent_framework.orchestrations import (
These may appear in event streams (executor_invoked/executor_completed). They're analogous to concurrent's dispatcher and aggregator and can be ignored if you only care about agent activity.
## Why AzureOpenAIResponsesClient?
Orchestration samples use `AzureOpenAIResponsesClient` rather than the CRUD-style `AzureAIAgent` client. Orchestrations create agents locally and do not require server-side lifecycle management (create/update/delete). `AzureOpenAIResponsesClient` is a lightweight client that uses the underlying Agent Service V2 (Responses API) for non-CRUD-style agents, which is ideal for orchestration patterns like Sequential, Concurrent, Handoff, GroupChat, and Magentic.
## Environment Variables
Orchestration samples that use `AzureOpenAIResponsesClient` expect:
@@ -1,10 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import Message
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -26,14 +27,20 @@ Demonstrates:
- Workflow completion when idle with no pending work
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
- Familiarity with Workflow events (WorkflowEvent)
"""
async def main() -> None:
# 1) Create three domain agents using AzureOpenAIChatClient
client = AzureOpenAIChatClient(credential=AzureCliCredential())
# 1) Create three domain agents using AzureOpenAIResponsesClient
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher = client.as_agent(
instructions=(
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import (
@@ -12,7 +13,7 @@ from agent_framework import (
WorkflowContext,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -29,21 +30,23 @@ and emit AgentExecutorResponse outputs, which allows reuse of the high-level
ConcurrentBuilder API and the default aggregator.
Demonstrates:
- Executors that create their Agent in __init__ (via AzureOpenAIChatClient)
- Executors that create their Agent in __init__ (via AzureOpenAIResponsesClient)
- A @handler that converts AgentExecutorRequest -> AgentExecutorResponse
- ConcurrentBuilder(participants=[...]) to build fan-out/fan-in
- Default aggregator returning list[Message] (one user + one assistant per agent)
- Workflow completion when all participants become idle
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient (az login + required env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
class ResearcherExec(Executor):
agent: Agent
def __init__(self, client: AzureOpenAIChatClient, id: str = "researcher"):
def __init__(self, client: AzureOpenAIResponsesClient, id: str = "researcher"):
self.agent = client.as_agent(
instructions=(
"You're an expert market and product researcher. Given a prompt, provide concise, factual insights,"
@@ -63,7 +66,7 @@ class ResearcherExec(Executor):
class MarketerExec(Executor):
agent: Agent
def __init__(self, client: AzureOpenAIChatClient, id: str = "marketer"):
def __init__(self, client: AzureOpenAIResponsesClient, id: str = "marketer"):
self.agent = client.as_agent(
instructions=(
"You're a creative marketing strategist. Craft compelling value propositions and target messaging"
@@ -83,7 +86,7 @@ class MarketerExec(Executor):
class LegalExec(Executor):
agent: Agent
def __init__(self, client: AzureOpenAIChatClient, id: str = "legal"):
def __init__(self, client: AzureOpenAIResponsesClient, id: str = "legal"):
self.agent = client.as_agent(
instructions=(
"You're a cautious legal/compliance reviewer. Highlight constraints, disclaimers, and policy concerns"
@@ -101,7 +104,11 @@ class LegalExec(Executor):
async def main() -> None:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher = ResearcherExec(client)
marketer = MarketerExec(client)
@@ -1,10 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import Message
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import ConcurrentBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -17,7 +18,7 @@ Sample: Concurrent Orchestration with Custom Aggregator
Build a concurrent workflow with ConcurrentBuilder that fans out one prompt to
multiple domain agents and fans in their responses. Override the default
aggregator with a custom async callback that uses AzureOpenAIChatClient.get_response()
aggregator with a custom async callback that uses AzureOpenAIResponsesClient.get_response()
to synthesize a concise, consolidated summary from the experts' outputs.
The workflow completes when all participants become idle.
@@ -28,12 +29,18 @@ Demonstrates:
- Workflow output yielded with the synthesized summary string
Prerequisites:
- Azure OpenAI configured for AzureOpenAIChatClient (az login + required env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
async def main() -> None:
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher = client.as_agent(
instructions=(
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import (
@@ -8,7 +9,7 @@ from agent_framework import (
AgentResponseUpdate,
Message,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -25,7 +26,9 @@ What it does:
- Coordinates a researcher and writer agent to solve tasks collaboratively
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
ORCHESTRATOR_AGENT_INSTRUCTIONS = """
@@ -39,8 +42,12 @@ Guidelines:
async def main() -> None:
# Create a chat client using Azure OpenAI and Azure CLI credentials for all agents
client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create a Responses client using Azure OpenAI and Azure CLI credentials for all agents
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Orchestrator agent that manages the conversation
# Note: This agent (and the underlying chat client) must support structured outputs.
@@ -2,6 +2,7 @@
import asyncio
import logging
import os
from typing import cast
from agent_framework import (
@@ -9,7 +10,7 @@ from agent_framework import (
AgentResponseUpdate,
Message,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -38,15 +39,21 @@ Participants represent:
- Doctor from Scandinavia (public health, equity, societal support)
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
# Load environment variables from .env file
load_dotenv()
def _get_chat_client() -> AzureOpenAIChatClient:
return AzureOpenAIChatClient(credential=AzureCliCredential())
def _get_chat_client() -> AzureOpenAIResponsesClient:
return AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
async def main() -> None:
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import (
@@ -8,7 +9,7 @@ from agent_framework import (
AgentResponseUpdate,
Message,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatBuilder, GroupChatState
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -24,7 +25,9 @@ What it does:
- Uses a pure Python function to control speaker selection based on conversation state
Prerequisites:
- OpenAI environment variables configured for OpenAIChatClient
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
@@ -36,8 +39,12 @@ def round_robin_selector(state: GroupChatState) -> str:
async def main() -> None:
# Create a chat client using Azure OpenAI and Azure CLI credentials for all agents
client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Create a Responses client using Azure OpenAI and Azure CLI credentials for all agents
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Participant agents
expert = Agent(
@@ -2,6 +2,7 @@
import asyncio
import logging
import os
from typing import cast
from agent_framework import (
@@ -10,7 +11,7 @@ from agent_framework import (
Message,
resolve_agent_id,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import HandoffBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -28,8 +29,9 @@ Routing Pattern:
User -> Coordinator -> Specialist (iterates N times) -> Handoff -> Final Output
Prerequisites:
- `az login` (Azure CLI authentication)
- Environment variables for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run `az login` before executing the sample.
Key Concepts:
- Autonomous interaction mode: agents iterate until they handoff
@@ -41,7 +43,7 @@ load_dotenv()
def create_agents(
client: AzureOpenAIChatClient,
client: AzureOpenAIResponsesClient,
) -> tuple[Agent, Agent, Agent]:
"""Create coordinator and specialists for autonomous iteration."""
coordinator = client.as_agent(
@@ -77,7 +79,11 @@ def create_agents(
async def main() -> None:
"""Run an autonomous handoff workflow with specialist iteration enabled."""
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
coordinator, research_agent, summary_agent = create_agents(client)
# Build the workflow with autonomous mode
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Annotated, cast
from agent_framework import (
@@ -11,7 +12,7 @@ from agent_framework import (
WorkflowRunState,
tool,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import HandoffAgentUserRequest, HandoffBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -25,8 +26,9 @@ A handoff workflow defines a pattern that assembles agents in a mesh topology, a
them to transfer control to each other based on the conversation context.
Prerequisites:
- `az login` (Azure CLI authentication)
- Environment variables configured for AzureOpenAIChatClient (AZURE_OPENAI_ENDPOINT, etc.)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run `az login` before executing the sample.
Key Concepts:
- Auto-registered handoff tools: HandoffBuilder automatically creates handoff tools
@@ -58,11 +60,11 @@ def process_return(order_number: Annotated[str, "Order number to process return
return f"Return initiated successfully for order {order_number}. You will receive return instructions via email."
def create_agents(client: AzureOpenAIChatClient) -> tuple[Agent, Agent, Agent, Agent]:
def create_agents(client: AzureOpenAIResponsesClient) -> tuple[Agent, Agent, Agent, Agent]:
"""Create and configure the triage and specialist agents.
Args:
client: The AzureOpenAIChatClient to use for creating agents.
client: The AzureOpenAIResponsesClient to use for creating agents.
Returns:
Tuple of (triage_agent, refund_agent, order_agent, return_agent)
@@ -192,8 +194,12 @@ async def main() -> None:
the demo reproducible and testable. In a production application, you would
replace the scripted_responses with actual user input collection.
"""
# Initialize the Azure OpenAI chat client
client = AzureOpenAIChatClient(credential=AzureCliCredential())
# Initialize the Azure OpenAI Responses client
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
# Create all agents: triage + specialists
triage, refund, order, support = create_agents(client)
@@ -3,6 +3,7 @@
import asyncio
import json
import logging
import os
from typing import cast
from agent_framework import (
@@ -11,8 +12,9 @@ from agent_framework import (
Message,
WorkflowEvent,
)
from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import GroupChatRequestSentEvent, MagenticBuilder, MagenticProgressLedger
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
logging.basicConfig(level=logging.WARNING)
@@ -40,7 +42,9 @@ energy efficiency and CO2 emissions of several ML models, streams intermediate
events, and prints the final answer. The workflow completes when idle.
Prerequisites:
- OpenAI credentials configured for `OpenAIChatClient` and `OpenAIResponsesClient`.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
# Load environment variables from .env file
@@ -48,25 +52,29 @@ load_dotenv()
async def main() -> None:
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher_agent = Agent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions=(
"You are a Researcher. You find information without additional computation or quantitative analysis."
),
# This agent requires the gpt-4o-search-preview model to perform web searches.
client=OpenAIChatClient(model_id="gpt-4o-search-preview"),
client=client,
)
# Create code interpreter tool using instance method
coder_client = OpenAIResponsesClient()
code_interpreter_tool = coder_client.get_code_interpreter_tool()
code_interpreter_tool = client.get_code_interpreter_tool()
coder_agent = Agent(
name="CoderAgent",
description="A helpful assistant that writes and executes code to process and analyze data.",
instructions="You solve questions using code. Please provide detailed analysis and computation process.",
client=coder_client,
client=client,
tools=code_interpreter_tool,
)
@@ -75,7 +83,7 @@ async def main() -> None:
name="MagenticManager",
description="Orchestrator that coordinates the research and coding workflow",
instructions="You coordinate a team to complete complex tasks efficiently.",
client=OpenAIChatClient(),
client=client,
)
print("\nBuilding Magentic Workflow...")
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from datetime import datetime
from pathlib import Path
from typing import cast
@@ -14,9 +15,9 @@ from agent_framework import (
WorkflowEvent,
WorkflowRunState,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest
from azure.identity._credentials import AzureCliCredential
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
@@ -38,7 +39,9 @@ Concepts highlighted here:
`responses` mapping so we can inject the stored human reply during restoration.
Prerequisites:
- OpenAI environment variables configured for `OpenAIChatClient`.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
TASK = (
@@ -61,14 +64,22 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage):
name="ResearcherAgent",
description="Collects background facts and references for the project.",
instructions=("You are the research lead. Gather crisp bullet points the team should know."),
client=AzureOpenAIChatClient(credential=AzureCliCredential()),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
writer = Agent(
name="WriterAgent",
description="Synthesizes the final brief for stakeholders.",
instructions=("You convert the research notes into a structured brief with milestones and risks."),
client=AzureOpenAIChatClient(credential=AzureCliCredential()),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
# Create a manager agent for orchestration
@@ -76,7 +87,11 @@ def build_workflow(checkpoint_storage: FileCheckpointStorage):
name="MagenticManager",
description="Orchestrator that coordinates the research and writing workflow",
instructions="You coordinate a team to complete complex tasks efficiently.",
client=AzureOpenAIChatClient(credential=AzureCliCredential()),
client=AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
),
)
# The builder wires in the Magentic orchestrator, sets the plan review path, and
@@ -2,6 +2,7 @@
import asyncio
import json
import os
from collections.abc import AsyncIterable
from typing import cast
@@ -11,8 +12,9 @@ from agent_framework import (
Message,
WorkflowEvent,
)
from agent_framework.openai import OpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import MagenticBuilder, MagenticPlanReviewRequest, MagenticPlanReviewResponse
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
# Load environment variables from .env file
@@ -35,7 +37,9 @@ Plan review options:
- revise(feedback): Provide textual feedback to modify the plan
Prerequisites:
- OpenAI credentials configured for `OpenAIChatClient`.
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
# Keep track of the last response to format output nicely in streaming mode
@@ -96,25 +100,31 @@ async def process_event_stream(stream: AsyncIterable[WorkflowEvent]) -> dict[str
async def main() -> None:
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
researcher_agent = Agent(
name="ResearcherAgent",
description="Specialist in research and information gathering",
instructions="You are a Researcher. You find information and gather facts.",
client=OpenAIChatClient(model_id="gpt-4o"),
client=client,
)
analyst_agent = Agent(
name="AnalystAgent",
description="Data analyst who processes and summarizes research findings",
instructions="You are an Analyst. You analyze findings and create summaries.",
client=OpenAIChatClient(model_id="gpt-4o"),
client=client,
)
manager_agent = Agent(
name="MagenticManager",
description="Orchestrator that coordinates the workflow",
instructions="You coordinate a team to complete tasks efficiently.",
client=OpenAIChatClient(model_id="gpt-4o"),
client=client,
)
print("\nBuilding Magentic Workflow with Human Plan Review...")
@@ -1,10 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import cast
from agent_framework import Message
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -28,13 +29,19 @@ Note on internal adapters:
You can safely ignore them when focusing on agent progress.
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
async def main() -> None:
# 1) Create agents
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
writer = client.as_agent(
instructions=("You are a concise copywriter. Provide a single, punchy marketing sentence based on the prompt."),
@@ -1,6 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Any
from agent_framework import (
@@ -10,7 +11,7 @@ from agent_framework import (
WorkflowContext,
handler,
)
from agent_framework.azure import AzureOpenAIChatClient
from agent_framework.azure import AzureOpenAIResponsesClient
from agent_framework.orchestrations import SequentialBuilder
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
@@ -32,7 +33,9 @@ Custom executor contract:
- Emit the updated conversation via ctx.send_message([...])
Prerequisites:
- Azure OpenAI access configured for AzureOpenAIChatClient (use az login + env vars)
- AZURE_AI_PROJECT_ENDPOINT must be your Azure AI Foundry Agent Service (V2) project endpoint.
- Azure OpenAI configured for AzureOpenAIResponsesClient with required environment variables.
- Authentication via azure-identity. Use AzureCliCredential and run az login before executing the sample.
"""
@@ -62,7 +65,11 @@ class Summarizer(Executor):
async def main() -> None:
# 1) Create a content agent
client = AzureOpenAIChatClient(credential=AzureCliCredential())
client = AzureOpenAIResponsesClient(
project_endpoint=os.environ["AZURE_AI_PROJECT_ENDPOINT"],
deployment_name=os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"],
credential=AzureCliCredential(),
)
content = client.as_agent(
instructions="Produce a concise paragraph answering the user's request.",
name="content",
+1 -1
View File
@@ -25,7 +25,7 @@ Make sure to set the following environment variables before running the example:
For quick testing and demonstration, you can use the pre-built .NET A2A servers from this repository:
**Quick Testing Reference**: Use the .NET A2A Client Server sample at:
`..\agent-framework\dotnet\samples\A2AClientServer`
`..\agent-framework\dotnet\samples\05-end-to-end\A2AClientServer`
### Run Python A2A Sample
```powershell
+1 -1
View File
@@ -21,7 +21,7 @@ Start with `01-get-started/` and work through the numbered files:
3. **[03_multi_turn.py](./01-get-started/03_multi_turn.py)** — Multi-turn conversations with `AgentThread`
4. **[04_memory.py](./01-get-started/04_memory.py)** — Agent memory with `ContextProvider`
5. **[05_first_workflow.py](./01-get-started/05_first_workflow.py)** — Build a workflow with executors and edges
6. **[06_host_your_agent.py](./01-get-started/06_host_your_agent.py)** — Host your agent via A2A
6. **[06_host_your_agent.py](./01-get-started/06_host_your_agent.py)** — Host your agent via Azure Functions
## Prerequisites
Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

+50 -28
View File
@@ -96,7 +96,7 @@ wheels = [
[[package]]
name = "agent-framework"
version = "1.0.0rc1"
version = "1.0.0rc2"
source = { virtual = "." }
dependencies = [
{ name = "agent-framework-core", extra = ["all"], marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -145,7 +145,7 @@ dev = [
[[package]]
name = "agent-framework-a2a"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/a2a" }
dependencies = [
{ name = "a2a-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -160,7 +160,7 @@ requires-dist = [
[[package]]
name = "agent-framework-ag-ui"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/ag-ui" }
dependencies = [
{ name = "ag-ui-protocol", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -188,7 +188,7 @@ provides-extras = ["dev"]
[[package]]
name = "agent-framework-anthropic"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/anthropic" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -203,12 +203,13 @@ requires-dist = [
[[package]]
name = "agent-framework-azure-ai"
version = "1.0.0rc1"
version = "1.0.0rc2"
source = { editable = "packages/azure-ai" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "aiohttp", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "azure-ai-agents", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "azure-ai-inference", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
[package.metadata]
@@ -216,11 +217,12 @@ requires-dist = [
{ name = "agent-framework-core", editable = "packages/core" },
{ name = "aiohttp" },
{ name = "azure-ai-agents", specifier = "==1.2.0b5" },
{ name = "azure-ai-inference", specifier = ">=1.0.0b9" },
]
[[package]]
name = "agent-framework-azure-ai-search"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/azure-ai-search" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -235,7 +237,7 @@ requires-dist = [
[[package]]
name = "agent-framework-azurefunctions"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/azurefunctions" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -257,7 +259,7 @@ dev = []
[[package]]
name = "agent-framework-bedrock"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/bedrock" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -274,7 +276,7 @@ requires-dist = [
[[package]]
name = "agent-framework-chatkit"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/chatkit" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -289,7 +291,7 @@ requires-dist = [
[[package]]
name = "agent-framework-claude"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/claude" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -304,7 +306,7 @@ requires-dist = [
[[package]]
name = "agent-framework-copilotstudio"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/copilotstudio" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -319,7 +321,7 @@ requires-dist = [
[[package]]
name = "agent-framework-core"
version = "1.0.0rc1"
version = "1.0.0rc2"
source = { editable = "packages/core" }
dependencies = [
{ name = "azure-ai-projects", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -399,7 +401,7 @@ provides-extras = ["all"]
[[package]]
name = "agent-framework-declarative"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/declarative" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -424,7 +426,7 @@ dev = [{ name = "types-pyyaml" }]
[[package]]
name = "agent-framework-devui"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/devui" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -460,7 +462,7 @@ provides-extras = ["dev", "all"]
[[package]]
name = "agent-framework-durabletask"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/durabletask" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -487,7 +489,7 @@ dev = [{ name = "types-python-dateutil", specifier = ">=2.9.0" }]
[[package]]
name = "agent-framework-foundry-local"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/foundry_local" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -502,7 +504,7 @@ requires-dist = [
[[package]]
name = "agent-framework-github-copilot"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/github_copilot" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -517,7 +519,7 @@ requires-dist = [
[[package]]
name = "agent-framework-lab"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/lab" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -596,7 +598,7 @@ dev = [
[[package]]
name = "agent-framework-mem0"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/mem0" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -611,7 +613,7 @@ requires-dist = [
[[package]]
name = "agent-framework-ollama"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/ollama" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -626,7 +628,7 @@ requires-dist = [
[[package]]
name = "agent-framework-orchestrations"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/orchestrations" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -637,7 +639,7 @@ requires-dist = [{ name = "agent-framework-core", editable = "packages/core" }]
[[package]]
name = "agent-framework-purview"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/purview" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -654,7 +656,7 @@ requires-dist = [
[[package]]
name = "agent-framework-redis"
version = "1.0.0b260219"
version = "1.0.0b260225"
source = { editable = "packages/redis" }
dependencies = [
{ name = "agent-framework-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
@@ -979,6 +981,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/6d/15070d23d7a94833a210da09d5d7ed3c24838bb84f0463895e5d159f1695/azure_ai_agents-1.2.0b5-py3-none-any.whl", hash = "sha256:257d0d24a6bf13eed4819cfa5c12fb222e5908deafb3cbfd5711d3a511cc4e88", size = 217948, upload-time = "2025-09-30T01:55:04.155Z" },
]
[[package]]
name = "azure-ai-inference"
version = "1.0.0b9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/6a/ed85592e5c64e08c291992f58b1a94dab6869f28fb0f40fd753dced73ba6/azure_ai_inference-1.0.0b9.tar.gz", hash = "sha256:1feb496bd84b01ee2691befc04358fa25d7c344d8288e99364438859ad7cd5a4", size = 182408, upload-time = "2025-02-15T00:37:28.464Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/0f/27520da74769db6e58327d96c98e7b9a07ce686dff582c9a5ec60b03f9dd/azure_ai_inference-1.0.0b9-py3-none-any.whl", hash = "sha256:49823732e674092dad83bb8b0d1b65aa73111fab924d61349eb2a8cdc0493990", size = 124885, upload-time = "2025-02-15T00:37:29.964Z" },
]
[[package]]
name = "azure-ai-projects"
version = "2.0.0b3"
@@ -1362,7 +1378,7 @@ name = "clr-loader"
version = "0.2.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605, upload-time = "2026-01-03T23:13:06.984Z" }
wheels = [
@@ -1841,7 +1857,7 @@ name = "exceptiongroup"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform == 'darwin') or (python_full_version < '3.13' and sys_platform == 'linux') or (python_full_version < '3.13' and sys_platform == 'win32')" },
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
@@ -2307,6 +2323,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" },
{ url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" },
{ url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" },
{ url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" },
{ url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" },
{ url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" },
{ url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" },
@@ -2314,6 +2331,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" },
{ url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" },
{ url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" },
{ url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" },
{ url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" },
{ url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" },
{ url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" },
@@ -2322,6 +2340,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" },
{ url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" },
{ url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" },
{ url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" },
{ url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" },
{ url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" },
{ url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" },
@@ -2330,6 +2349,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" },
{ url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" },
{ url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" },
{ url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" },
{ url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" },
{ url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" },
{ url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" },
@@ -2338,6 +2358,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" },
{ url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" },
{ url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" },
{ url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" },
{ url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" },
{ url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" },
{ url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" },
@@ -2346,6 +2367,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" },
{ url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" },
{ url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" },
{ url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" },
{ url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" },
{ url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" },
{ url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },
@@ -4567,8 +4589,8 @@ name = "powerfx"
version = "0.0.34"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "pythonnet", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "cffi", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
{ name = "pythonnet", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555, upload-time = "2025-12-22T15:50:59.682Z" }
wheels = [
@@ -5231,7 +5253,7 @@ name = "pythonnet"
version = "3.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "clr-loader", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "clr-loader", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux') or (python_full_version < '3.14' and sys_platform == 'win32')" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212, upload-time = "2024-12-13T08:30:44.393Z" }
wheels = [