mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
cc1ef730e3
commit
8b191de936
+27
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
@@ -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
|
||||
concurrent’s 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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
Generated
+50
-28
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user