Compare commits

..
Author SHA1 Message Date
e26c3580fb Fix docstring/load_dotenv ordering in foundry samples and add try/finally cleanup
Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/382df85c-cf3c-4802-97e0-a6df35fc9876

Co-authored-by: chetantoshniwal <255221507+chetantoshniwal@users.noreply.github.com>
2026-04-02 13:52:37 +02:00
Chetan ToshniwalandEduard van Valkenburg bbae6401b1 making version optional 2026-04-02 13:52:37 +02:00
a58f55876c Fix foundry samples: add model/env vars to code interpreter sample, fix optional agent version
Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/a758471e-f73b-4c1c-b5cb-1f1ae0b2971f

Co-authored-by: chetantoshniwal <255221507+chetantoshniwal@users.noreply.github.com>
2026-04-02 13:52:37 +02:00
Chetan ToshniwalandEduard van Valkenburg 922b85485d dynamic options dict to avoid imports 2026-04-02 13:52:37 +02:00
Chetan ToshniwalandEduard van Valkenburg 30920e1f9d Fixing samples with minor changes. 1. Adding model details for Anthropic, 2. Moving to environment variables for endpoint and deployment details 2026-04-02 13:52:37 +02:00
Eduard van ValkenburgandGitHub 95fd5ec658 Python: [BREAKING] Python: move Azure AI embeddings to Foundry (#5056)
* renamed AzureAIINferenceEmbeddings and lazy load azure-cosmos and env var rename

* updated coverage

* fix readme
2026-04-02 11:26:35 +00:00
chetantoshniwalGitHubchetantoshniwalcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
47d82911c0 Python: Fix server_tool_use input_json_delta handling and improve Anthropic samples (#5050)
* Fix server_tool_use input_json_delta handling and improve Anthropic samples

- Fix: Skip input_json_delta for server_tool_use content blocks in AnthropicClient streaming. Server-managed tools (e.g., skills with code interpreter) were producing Content.from_function_call(name='') entries that caused Anthropic API 400 errors on subsequent turns.

- Samples: Add dotenv loading and environment variable documentation to Anthropic Claude samples (MCP, permissions, session, shell, tools, URL, skills).

* Add regression test for server_tool_use + input_json_delta skip behavior

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/7c68dcb2-b577-4e36-b423-664b8fe3ac1d

Co-authored-by: chetantoshniwal <255221507+chetantoshniwal@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: chetantoshniwal <255221507+chetantoshniwal@users.noreply.github.com>
2026-04-02 09:43:59 +00:00
339e76d51f Python: Fix GitHubCopilotAgent to invoke context provider before_run/after_run hooks (#5013)
* Fix GitHubCopilotAgent not calling context provider hooks (#3984)

GitHubCopilotAgent accepted context_providers in its constructor but
never called before_run()/after_run() on them in _run_impl() or
_stream_updates(), silently ignoring all context providers.

Add _run_before_providers() helper to create SessionContext and invoke
before_run on each provider. Both _run_impl() and _stream_updates() now
run the full provider lifecycle: before_run before sending the prompt
(with provider instructions prepended) and after_run after receiving the
response. This follows the same pattern used by A2AAgent.

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

* Python: Fix GitHubCopilotAgent to invoke context provider before_run/after_run hooks

Fixes #3984

* fix(#3984): address review feedback for context provider integration

- Build prompt from session_context.get_messages(include_input=True) so
  provider-injected context_messages are included in both non-streaming
  and streaming paths (review comments #1, #2)
- Preserve timeout in opts (use get instead of pop) so providers can
  observe it via context.options (review comment #3)
- Eliminate streaming double-buffer: move after_run invocation to a
  ResponseStream result_hook (matching Agent class pattern) instead of
  maintaining a separate updates list in the generator (review comment #4)
- Improve _run_before_providers docstring

Add tests for:
- Context messages included in prompt (non-streaming + streaming)
- Error path: after_run NOT called when send_and_wait/streaming raises
- Multiple providers: forward before_run, reverse after_run ordering
- BaseHistoryProvider with load_messages=False is skipped
- Streaming after_run response contains aggregated updates
- Streaming with no updates still sets empty response
- Timeout preserved in session context options for providers

Note: _run_before_providers remains on GitHubCopilotAgent for now. A
follow-up PR should extract it to BaseAgent so subclasses can reuse it
without duplicating the provider iteration logic.

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

* Address review feedback for #3984: Python: [Bug]: GitHubCopilotAgent Memory Example

* refactor(#3984): promote _run_before_providers to BaseAgent

Move _run_before_providers from GitHubCopilotAgent into BaseAgent,
mirroring the existing _run_after_providers helper. Agent's
_prepare_session_and_messages now delegates to the shared base method,
eliminating the near-duplicate provider iteration logic that could
drift as the provider contract evolves.

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

* Address review feedback for #3984: Python: [Bug]: GitHubCopilotAgent Memory Example

* revert: keep _run_before_providers in GitHubCopilotAgent only

Undo the promotion of _run_before_providers to BaseAgent. The method
stays in GitHubCopilotAgent where it is needed, and _agents.py
retains its original inline provider iteration in RawAgent.

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

* fix: replace deprecated BaseContextProvider/BaseHistoryProvider with ContextProvider/HistoryProvider

Update imports and usages in GitHubCopilotAgent and its tests to use
the new non-deprecated class names from the core package.

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

* fix: address review feedback - reorder providers before session, wrap streaming after_run in try/except, assert after_run on skipped HistoryProvider

- Move _run_before_providers before _get_or_create_session so provider
  contributions can affect session configuration
- Wrap _run_after_providers in try/except in streaming _after_run_hook
  to prevent provider errors from replacing successful responses
- Add after_run assertion to test_history_provider_skip_when_load_messages_false

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

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-02 09:43:02 +00:00
Tao ChenandGitHub 62595b233f [BREAKING] Python: Refactor workflows kwargs (#5010)
* Refactor workflows kwargs usage

* Update sample

* Add tests

* Update samples

* Fix formatting

* Comments

* Comments 2

* Comments 3

* Fix test and typing
2026-04-02 09:40:39 +00:00
CopilotGitHubsphenrycopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Shawn Henry
fd253c0b0e Python: Move workflow-samples and agent-samples under declarative-agents directory (#5011)
* Move workflow-samples and agent-samples under declarative-agents and update all references

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f70f7d19-9256-4eec-b7db-28007d74440c

Co-authored-by: sphenry <6749825+sphenry@users.noreply.github.com>

* Fix relative paths in README files inside moved directories

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/f70f7d19-9256-4eec-b7db-28007d74440c

Co-authored-by: sphenry <6749825+sphenry@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sphenry <6749825+sphenry@users.noreply.github.com>
Co-authored-by: Shawn Henry <shahen@microsoft.com>
2026-04-02 09:34:33 +00:00
hashwnathandGitHub 7e8e9e3074 Python: Fix duplicate system message from instructions (#5049) (#5051)
Add deduplication to `prepend_instructions_to_messages()` to skip
instructions that are already present as leading messages with the
same role and text. This prevents duplicate system messages when
instructions are injected by multiple layers (e.g. Agent + chat client).

Fixes #5049
2026-04-02 09:20:50 +00:00
CopilotGitHubTaoChenOSUcopilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
7607acd009 Python: Add experimental decorator to all Evals pieces (#5040)
* Initial plan

* feat(python): add experimental decorator to all Evals pieces

Agent-Logs-Url: https://github.com/microsoft/agent-framework/sessions/99d71249-a5d6-4977-a5b5-6ffe0a3be2bc

Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TaoChenOSU <12570346+TaoChenOSU@users.noreply.github.com>
2026-04-02 09:18:35 +00:00
Tao ChenandGitHub 3d87cec304 Python: Fix SK migration samples (#5047)
* Fix SK migration samples

* Fix env vars for SK

* Hard code model for sheel tool samples
2026-04-02 08:40:34 +00:00
113 changed files with 2462 additions and 1876 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ body:
attributes:
label: Package Versions
description: List the agent-framework-* packages and versions you are using
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-azure-ai: 1.0.0"
placeholder: "e.g., agent-framework-core: 1.0.0, agent-framework-foundry: 1.0.0"
validations:
required: true
@@ -37,7 +37,6 @@ ENFORCED_TARGETS: set[str] = {
# Packages (sorted alphabetically)
"packages.anthropic.agent_framework_anthropic",
"packages.azure-ai-search.agent_framework_azure_ai_search",
"packages.azure-ai.agent_framework_azure_ai",
"packages.core.agent_framework",
"packages.core.agent_framework._workflows",
"packages.foundry.agent_framework_foundry",
+12 -9
View File
@@ -60,8 +60,8 @@ jobs:
environment: integration
timeout-minutes: 60
env:
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
@@ -95,8 +95,8 @@ jobs:
environment: integration
timeout-minutes: 60
env:
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
@@ -201,14 +201,15 @@ jobs:
timeout-minutes: 60
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FUNCTIONS_WORKER_RUNTIME: "python"
@@ -255,12 +256,14 @@ jobs:
environment: integration
timeout-minutes: 60
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
AZURE_AI_MODEL: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
FOUNDRY_AGENT_VERSION: ${{ vars.FOUNDRY_AGENT_VERSION }}
FOUNDRY_MODELS_ENDPOINT: ${{ vars.FOUNDRY_MODELS_ENDPOINT || '' }}
FOUNDRY_MODELS_API_KEY: ${{ secrets.FOUNDRY_MODELS_API_KEY || '' }}
FOUNDRY_EMBEDDING_MODEL: ${{ vars.FOUNDRY_EMBEDDING_MODEL || '' }}
FOUNDRY_IMAGE_EMBEDDING_MODEL: ${{ vars.FOUNDRY_IMAGE_EMBEDDING_MODEL || '' }}
LOCAL_MCP_URL: ${{ vars.LOCAL_MCP__URL }}
defaults:
run:
+12 -18
View File
@@ -37,7 +37,7 @@ jobs:
azureChanged: ${{ steps.filter.outputs.azure }}
miscChanged: ${{ steps.filter.outputs.misc }}
functionsChanged: ${{ steps.filter.outputs.functions }}
azureAiChanged: ${{ steps.filter.outputs.azure-ai }}
foundryChanged: ${{ steps.filter.outputs.foundry }}
cosmosChanged: ${{ steps.filter.outputs.cosmos }}
steps:
- uses: actions/checkout@v6
@@ -75,10 +75,10 @@ jobs:
functions:
- 'python/packages/azurefunctions/**'
- 'python/packages/durabletask/**'
azure-ai:
- 'python/packages/azure-ai/**'
foundry:
- 'python/packages/foundry/**'
- 'python/samples/**/providers/foundry/**'
- 'python/samples/02-agents/embeddings/foundry_embeddings.py'
cosmos:
- 'python/packages/azure-cosmos/**'
# run only if 'python' files were changed
@@ -139,8 +139,8 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
@@ -192,8 +192,8 @@ jobs:
runs-on: ubuntu-latest
environment: integration
env:
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
@@ -330,14 +330,15 @@ jobs:
environment: integration
env:
UV_PYTHON: "3.11"
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_EMBEDDING_MODEL: ${{ vars.OPENAI_EMBEDDING_MODEL_ID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FUNCTIONS_WORKER_RUNTIME: "python"
@@ -392,13 +393,11 @@ jobs:
github.event_name != 'pull_request' &&
needs.paths-filter.outputs.pythonChanges == 'true' &&
(github.event_name != 'merge_group' ||
needs.paths-filter.outputs.azureAiChanged == 'true' ||
needs.paths-filter.outputs.foundryChanged == 'true' ||
needs.paths-filter.outputs.coreChanged == 'true')
runs-on: ubuntu-latest
environment: integration
env:
AZURE_AI_PROJECT_ENDPOINT: ${{ secrets.AZUREAI__ENDPOINT }}
AZURE_AI_MODEL: ${{ vars.AZUREAI__DEPLOYMENTNAME }}
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
FOUNDRY_AGENT_NAME: ${{ vars.FOUNDRY_AGENT_NAME }}
@@ -432,11 +431,6 @@ jobs:
--timeout=120 --session-timeout=900 --timeout_method thread
--retries 2 --retry-delay 5
working-directory: ./python
- name: Test Azure AI samples
timeout-minutes: 10
if: env.RUN_SAMPLES_TESTS == 'true'
run: uv run pytest tests/samples/ -m "azure-ai"
working-directory: ./python
- name: Surface failing tests
if: always()
uses: pmeier/pytest-results-action@v0.7.2
+22 -16
View File
@@ -66,12 +66,13 @@ jobs:
# Azure OpenAI configuration
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_RESPONSES_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.AZUREOPENAI__CHATDEPLOYMENTNAME }}
AZURE_OPENAI_CHAT_MODEL: ${{ vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
AZURE_OPENAI_EMBEDDING_MODEL: ${{ vars.AZURE_OPENAI_EMBEDDING_DEPLOYMENT_NAME || vars.AZUREOPENAI__EMBEDDINGDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
# GitHub MCP
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
@@ -97,11 +98,12 @@ jobs:
echo "FOUNDRY_MODEL=$FOUNDRY_MODEL" >> .env
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
echo "AZURE_OPENAI_RESPONSES_MODEL=$AZURE_OPENAI_RESPONSES_MODEL" >> .env
echo "AZURE_OPENAI_CHAT_COMPLETION_MODEL=$AZURE_OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "AZURE_OPENAI_CHAT_MODEL=$AZURE_OPENAI_CHAT_MODEL" >> .env
echo "AZURE_OPENAI_EMBEDDING_MODEL=$AZURE_OPENAI_EMBEDDING_MODEL" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
echo "OPENAI_RESPONSES_MODEL=$OPENAI_RESPONSES_MODEL" >> .env
echo "GITHUB_PAT=$GITHUB_PAT" >> .env
- name: Run sample validation
@@ -122,8 +124,8 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults:
run:
working-directory: python
@@ -142,8 +144,8 @@ jobs:
run: |
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_MODEL=$OPENAI_MODEL" >> .env
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
echo "OPENAI_RESPONSES_MODEL=$OPENAI_RESPONSES_MODEL" >> .env
- name: Run sample validation
run: |
@@ -565,8 +567,8 @@ jobs:
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
defaults:
run:
@@ -589,8 +591,8 @@ jobs:
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
echo "OPENAI_RESPONSES_MODEL=$OPENAI_RESPONSES_MODEL" >> .env
- name: Run sample validation
run: |
@@ -610,14 +612,18 @@ jobs:
env:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT || vars.AZURE_AI_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# Azure OpenAI configuration
# Azure OpenAI configuration for AF
AZURE_OPENAI_ENDPOINT: ${{ vars.AZUREOPENAI__ENDPOINT }}
AZURE_OPENAI_MODEL: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME || vars.AZUREOPENAI__RESPONSESDEPLOYMENTNAME }}
# OpenAI configuration
# Azure OpenAI configuration for SK
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ vars.AZURE_OPENAI_DEPLOYMENT_NAME }}
# OpenAI key
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_CHAT_COMPLETION_MODEL: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_CHAT_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_MODEL: ${{ vars.OPENAI__RESPONSESMODELID }}
# OpenAI configuration for SK
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
# Copilot Studio
COPILOTSTUDIOAGENT__ENVIRONMENTID: ${{ secrets.COPILOTSTUDIOAGENT__ENVIRONMENTID }}
COPILOTSTUDIOAGENT__SCHEMANAME: ${{ secrets.COPILOTSTUDIOAGENT__SCHEMANAME }}
@@ -644,8 +650,8 @@ jobs:
echo "AZURE_OPENAI_ENDPOINT=$AZURE_OPENAI_ENDPOINT" >> .env
echo "AZURE_OPENAI_MODEL=$AZURE_OPENAI_MODEL" >> .env
echo "OPENAI_API_KEY=$OPENAI_API_KEY" >> .env
echo "OPENAI_CHAT_COMPLETION_MODEL=$OPENAI_CHAT_COMPLETION_MODEL" >> .env
echo "OPENAI_CHAT_MODEL=$OPENAI_CHAT_MODEL" >> .env
echo "OPENAI_RESPONSES_MODEL=$OPENAI_RESPONSES_MODEL" >> .env
echo "COPILOTSTUDIOAGENT__ENVIRONMENTID=$COPILOTSTUDIOAGENT__ENVIRONMENTID" >> .env
echo "COPILOTSTUDIOAGENT__SCHEMANAME=$COPILOTSTUDIOAGENT__SCHEMANAME" >> .env
echo "COPILOTSTUDIOAGENT__TENANTID=$COPILOTSTUDIOAGENT__TENANTID" >> .env
@@ -37,7 +37,7 @@ Key changes:
4. **New `FoundryChatClient`** in azure-ai for Azure AI Foundry Responses API access, built on `RawFoundryChatClient(RawOpenAIChatClient)`.
5. **All deprecated `AzureOpenAI*` classes** consolidated into a single file (`_deprecated_azure_openai.py`) in the azure-ai package for clean future deletion.
6. **Core's `agent_framework.openai` and `agent_framework.azure` namespaces** become lazy-loading gateways, preserving backward-compatible import paths while removing hard dependencies.
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_RESPONSES_MODEL_ID` / `OPENAI_CHAT_MODEL_ID`).
7. **Unified `model` parameter** replaces `model_id` (OpenAI), `deployment_name` (Azure OpenAI), and `model_deployment_name` (Azure AI) across all client constructors. The term `model` is intentionally generic: it naturally maps to an OpenAI model name *and* to an Azure OpenAI deployment name, making it straightforward to use `OpenAIChatClient` with either OpenAI or Azure OpenAI backends (via `AsyncAzureOpenAI`). Environment variables are similarly unified (e.g., `OPENAI_MODEL` instead of separate `OPENAI_CHAT_MODEL_ID` / `OPENAI_CHAT_COMPLETION_MODEL_ID`).
8. **`FoundryAgent`** replaces the pattern of `Agent(client=AzureAIClient(...))` for connecting to pre-configured agents in Azure AI Foundry (PromptAgents and HostedAgents). The underlying `RawFoundryAgentChatClient` is an implementation detail — most users interact only with `FoundryAgent`. `AzureAIAgentClient` is separately deprecated as it refers to the V1 Agents Service API. See below for design rationale.
### Foundry Agent Design: `FoundryAgentClient` vs `FoundryAgent`
@@ -177,7 +177,7 @@ This feature ports the vector store abstractions, embedding generator abstractio
**Goal:** Add embedding generators to all existing AF provider packages that have chat clients.
**Mergeable:** Yes — each is independent, added to existing provider packages.
#### 2.1 — Azure AI Inference embedding (in `packages/azure-ai/`)
#### 2.1 — Foundry inference embedding (in `packages/foundry/`)
#### 2.2 — Ollama embedding (in `packages/ollama/`)
#### 2.3 — Anthropic embedding (in `packages/anthropic/`)
#### 2.4 — Bedrock embedding (in `packages/bedrock/`)
+12 -3
View File
@@ -1,6 +1,15 @@
# Azure AI
# Microsoft Foundry
FOUNDRY_PROJECT_ENDPOINT=""
# Model used for FoundryChatClient
FOUNDRY_MODEL=""
# Foundry Agents (prompt or hosted agents)
FOUNDRY_AGENT_NAME=""
FOUNDRY_AGENT_VERSION=""
# Microsoft Foundry Models endpoint, used by embeddings
FOUNDRY_MODELS_ENDPOINT=""
FOUNDRY_MODELS_API_KEY=""
FOUNDRY_EMBEDDING_MODEL=""
FOUNDRY_IMAGE_EMBEDDING_MODEL=""
# Bing connection for web search (optional, used by samples with web search)
BING_CONNECTION_ID=""
# Azure AI Search (optional, used by AzureAISearchContextProvider samples)
@@ -13,12 +22,12 @@ AZURE_SEARCH_KNOWLEDGE_BASE_NAME=""
# (different from AZURE_AI_PROJECT_ENDPOINT - Knowledge Base needs OpenAI endpoint for model calls)
# OpenAI
OPENAI_API_KEY=""
OPENAI_CHAT_COMPLETION_MODEL=""
OPENAI_CHAT_MODEL=""
OPENAI_RESPONSES_MODEL=""
# Azure OpenAI
AZURE_OPENAI_ENDPOINT=""
AZURE_OPENAI_CHAT_COMPLETION_MODEL=""
AZURE_OPENAI_CHAT_MODEL=""
AZURE_OPENAI_RESPONSES_MODEL=""
# Mem0
MEM0_API_KEY=""
# Copilot Studio
+3 -3
View File
@@ -15,7 +15,7 @@ python/
├── pyproject.toml # Root package (agent-framework)
├── packages/
│ ├── core/ # agent-framework-core (main package)
│ ├── azure-ai/ # agent-framework-azure-ai
│ ├── foundry/ # agent-framework-foundry
│ ├── anthropic/ # agent-framework-anthropic
│ └── ... # Other connector packages
```
@@ -76,9 +76,9 @@ uv run poe add-dependency-and-validate-bounds --package core --dependency "<depe
Provider folders in core use `__getattr__` to lazy load from connector packages:
```python
# In agent_framework/azure/__init__.py
# In agent_framework/foundry/__init__.py
_IMPORTS: dict[str, tuple[str, str]] = {
"AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"FoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
}
def __getattr__(name: str) -> Any:
+1 -1
View File
@@ -124,7 +124,7 @@ The merge CI workflow (`python-merge-tests.yml`) splits integration tests into p
- **Azure OpenAI integration** — runs when `packages/core/agent_framework/azure/` or core changes
- **Misc integration** — Anthropic, Ollama, MCP tests; runs when their packages or core change
- **Functions integration** — Azure Functions + Durable Task; runs when their packages or core change
- **Azure AI integration** — runs when `packages/azure-ai/` or core changes
- **Foundry integration** — runs when `packages/foundry/` or core changes
Core infrastructure changes (e.g., `_agents.py`, `_types.py`) trigger all integration test jobs. Scheduled and manual runs always execute all jobs.
+4 -3
View File
@@ -40,7 +40,7 @@ python/
│ ├── core/ # agent-framework-core (main package)
│ │ ├── agent_framework/ # Public API exports
│ │ └── tests/
│ ├── azure-ai/ # agent-framework-azure-ai
│ ├── foundry/ # agent-framework-foundry
│ ├── anthropic/ # agent-framework-anthropic
│ ├── ollama/ # agent-framework-ollama
│ └── ... # Other provider packages
@@ -52,7 +52,7 @@ python/
### Package Relationships
- `agent-framework-core` contains core abstractions and OpenAI/Azure OpenAI built-in
- Provider packages (`azure-ai`, `anthropic`, etc.) extend core with specific integrations
- Provider packages (`foundry`, `anthropic`, etc.) extend core with specific integrations
- Core uses lazy loading via `__getattr__` in provider folders (e.g., `agent_framework/azure/`)
## Package Documentation
@@ -68,8 +68,9 @@ python/
- [ollama](packages/ollama/AGENTS.md) - Local Ollama inference
### Azure Integrations
- [azure-ai](packages/azure-ai/AGENTS.md) - Azure AI Foundry agents
- [foundry](packages/foundry/README.md) - Microsoft Foundry chat, agent, memory, and embedding integrations
- [azure-ai-search](packages/azure-ai-search/AGENTS.md) - Azure AI Search RAG
- [azure-cosmos](packages/azure-cosmos/AGENTS.md) - Azure Cosmos DB-backed history provider
- [azurefunctions](packages/azurefunctions/AGENTS.md) - Azure Functions hosting
### Protocols & UI
+9 -8
View File
@@ -325,14 +325,15 @@ python/
│ │ ├── mem0/ # Lazy loads from agent-framework-mem0
│ │ └── redis/ # Lazy loads from agent-framework-redis
│ │
│ ├── azure-ai/ # agent-framework-azure-ai
│ ├── foundry/ # agent-framework-foundry
│ │ ├── pyproject.toml
│ │ ├── tests/
│ │ └── agent_framework_azure_ai/
│ │ └── agent_framework_foundry/
│ │ ├── __init__.py # Public exports
│ │ ├── _chat_client.py # AzureAIClient implementation
│ │ ├── _client.py # AzureAIAgentClient implementation
│ │ ├── _shared.py # AzureAISettings and shared utilities
│ │ ├── _chat_client.py # FoundryChatClient implementation
│ │ ├── _agent.py # FoundryAgent implementation
│ │ ├── _embedding_client.py # FoundryEmbeddingClient implementation
│ │ ├── _memory_provider.py # Foundry memory implementation
│ │ └── py.typed # PEP 561 marker
│ ├── anthropic/ # agent-framework-anthropic
│ ├── bedrock/ # agent-framework-bedrock
@@ -345,9 +346,9 @@ python/
Provider folders in the core package use `__getattr__` to lazy load classes from their respective connector packages. This allows users to import from a consistent location while only loading dependencies when needed:
```python
# In agent_framework/azure/__init__.py
# In agent_framework/foundry/__init__.py
_IMPORTS: dict[str, tuple[str, str]] = {
"AzureAIAgentClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"FoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
# ...
}
@@ -419,7 +420,7 @@ pip install agent-framework-core[all]
pip install agent-framework
# Install specific connector (pulls in core as dependency)
pip install agent-framework-azure-ai
pip install agent-framework-foundry
```
## Documentation
+1 -1
View File
@@ -249,7 +249,7 @@ For more advanced orchestration patterns including Sequential, Concurrent, Group
- [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
- [Foundry Integration](https://github.com/microsoft/agent-framework/tree/main/python/packages/foundry): Microsoft Foundry integration
- [Workflow Samples](samples/03-workflows): Advanced multi-agent patterns
## Agent Framework Documentation
@@ -1281,8 +1281,8 @@ class RawAnthropicClient(
)
)
case "input_json_delta":
# Skip argument deltas for MCP tools — execution is handled server-side.
if self._last_call_content_type == "mcp_tool_use":
# Skip argument deltas for MCP and server tools — execution is handled server-side.
if self._last_call_content_type in ("mcp_tool_use", "server_tool_use"):
pass
else:
call_id = self._last_call_id_name[0] if self._last_call_id_name else ""
@@ -1144,6 +1144,53 @@ def test_parse_contents_from_anthropic_input_json_delta_no_duplicate_name(
assert result[0].arguments == '"San Francisco"}'
def test_parse_contents_server_tool_use_input_json_delta_ignored(
mock_anthropic_client: MagicMock,
) -> None:
"""Regression test: input_json_delta events are ignored after a server_tool_use block.
Server-managed tools have their execution handled server-side, so streaming
input_json_delta events must not produce Content.from_function_call(name='')
entries that would cause Anthropic API 400 errors on subsequent turns.
"""
client = create_test_anthropic_client(mock_anthropic_client)
# Simulate a server_tool_use event that sets _last_call_content_type
server_tool_content = MagicMock()
server_tool_content.type = "server_tool_use"
server_tool_content.id = "srvtool_abc"
server_tool_content.name = "web_search"
server_tool_content.input = {}
result = client._parse_contents_from_anthropic([server_tool_content])
# server_tool_use falls through to function_call (not mcp_tool_use / code_execution)
assert len(result) == 1
assert result[0].type == "function_call"
assert client._last_call_content_type == "server_tool_use" # type: ignore[attr-defined]
# input_json_delta events after server_tool_use must be silently ignored
delta_content = MagicMock()
delta_content.type = "input_json_delta"
delta_content.partial_json = '{"query": "latest news"}'
result = client._parse_contents_from_anthropic([delta_content])
assert result == [], (
"input_json_delta after server_tool_use should produce no content, "
"but got: %r" % result
)
# A second delta must also be ignored
delta_content_2 = MagicMock()
delta_content_2.type = "input_json_delta"
delta_content_2.partial_json = '{"extra": true}'
result = client._parse_contents_from_anthropic([delta_content_2])
assert result == [], (
"subsequent input_json_delta after server_tool_use should also be ignored, "
"but got: %r" % result
)
# Stream Processing Tests
-30
View File
@@ -1,30 +0,0 @@
# Azure AI Package (agent-framework-azure-ai)
Integration with Azure AI inference embeddings plus shared Azure authentication helpers.
## Main Classes
- **`AzureAIInferenceEmbeddingClient`** - Full-featured Azure AI inference embeddings client
- **`RawAzureAIInferenceEmbeddingClient`** - Raw embeddings client without middleware layers
- **`AzureAIInferenceEmbeddingOptions`** / **`AzureAIInferenceEmbeddingSettings`** - Embedding options and settings
- **`AzureAISettings`** - Shared Azure AI project settings TypedDict
- **`AzureCredentialTypes`** / **`AzureTokenProvider`** - Shared Azure authentication helpers
## Usage
```python
from agent_framework_azure_ai import AzureAIInferenceEmbeddingClient
client = AzureAIInferenceEmbeddingClient(
endpoint="https://<resource>.inference.ai.azure.com",
api_key="...",
model="text-embedding-3-large",
)
result = await client.get_embeddings(["Hello"])
```
## Import Path
```python
from agent_framework_azure_ai import AzureAIInferenceEmbeddingClient
```
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) Microsoft Corporation.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
-26
View File
@@ -1,26 +0,0 @@
# Get Started with Microsoft Agent Framework Azure AI
Please install this package via pip:
```bash
pip install agent-framework-azure-ai --pre
```
## Foundry Memory Context Provider
The Foundry Memory context provider enables semantic memory capabilities for your agents using Azure AI Foundry Memory Store. It automatically:
- Retrieves static (user profile) memories on first run
- Searches for contextual memories based on conversation
- Updates the memory store with new conversation messages
### Basic Usage Example
See the [Foundry Memory example](../../samples/02-agents/context_providers/azure_ai_foundry_memory.py) which demonstrates:
- Creating a memory store using Azure AI Projects client
- Setting up an agent with FoundryMemoryProvider
- Teaching the agent user preferences
- Retrieving information using remembered context across conversations
- Automatic memory updates with configurable delays
and see the [README](https://github.com/microsoft/agent-framework/tree/main/python/README.md) for more information.
@@ -1,28 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
import importlib.metadata
from ._embedding_client import (
AzureAIInferenceEmbeddingClient,
AzureAIInferenceEmbeddingOptions,
AzureAIInferenceEmbeddingSettings,
RawAzureAIInferenceEmbeddingClient,
)
from ._entra_id_authentication import AzureCredentialTypes, AzureTokenProvider
from ._shared import AzureAISettings
try:
__version__ = importlib.metadata.version(__name__)
except importlib.metadata.PackageNotFoundError:
__version__ = "0.0.0"
__all__ = [
"AzureAIInferenceEmbeddingClient",
"AzureAIInferenceEmbeddingOptions",
"AzureAIInferenceEmbeddingSettings",
"AzureAISettings",
"AzureCredentialTypes",
"AzureTokenProvider",
"RawAzureAIInferenceEmbeddingClient",
"__version__",
]
@@ -1,67 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable
from typing import Union
from agent_framework.exceptions import ChatClientInvalidAuthException
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
logger: logging.Logger = logging.getLogger(__name__)
AzureTokenProvider = Callable[[], Union[str, Awaitable[str]]]
"""A callable that returns a bearer token string, either synchronously or asynchronously."""
AzureCredentialTypes = Union[TokenCredential, AsyncTokenCredential]
"""Union of Azure credential types.
Accepts:
- ``TokenCredential`` — synchronous Azure credential (e.g. ``DefaultAzureCredential()``)
- ``AsyncTokenCredential`` — asynchronous Azure credential (e.g. ``azure.identity.aio.DefaultAzureCredential()``)
"""
def resolve_credential_to_token_provider(
credential: AzureCredentialTypes | AzureTokenProvider,
token_endpoint: str | None,
) -> AzureTokenProvider:
"""Convert an Azure credential or token provider into an ``ad_token_provider`` callable.
If the credential is already a callable token provider, it is returned as-is
(``token_endpoint`` is not required in this case).
If it is a ``TokenCredential`` or ``AsyncTokenCredential``, it is wrapped using
``azure.identity.get_bearer_token_provider`` (sync or async variant) which
handles token caching and automatic refresh.
Args:
credential: An Azure credential or token provider callable.
token_endpoint: The token scope/endpoint
(e.g. ``"https://cognitiveservices.azure.com/.default"``).
Required when ``credential`` is a ``TokenCredential`` or ``AsyncTokenCredential``.
Returns:
A callable that returns a bearer token string (sync or async).
Raises:
ServiceInvalidAuthError: If the token endpoint is empty when needed for credential wrapping.
"""
# Already a token provider callable (not a credential object) — use directly
if callable(credential) and not isinstance(credential, (TokenCredential, AsyncTokenCredential)):
return credential
if not token_endpoint:
raise ChatClientInvalidAuthException(
"A token endpoint must be provided either in settings, as an environment variable, or as an argument."
)
if isinstance(credential, AsyncTokenCredential):
from azure.identity.aio import get_bearer_token_provider as get_async_bearer_token_provider
return get_async_bearer_token_provider(credential, token_endpoint)
from azure.identity import get_bearer_token_provider
return get_bearer_token_provider(credential, token_endpoint) # type: ignore[arg-type]
@@ -1,48 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from __future__ import annotations
import sys
if sys.version_info >= (3, 11):
from typing import TypedDict # pragma: no cover
else:
from typing_extensions import TypedDict # type: ignore # pragma: no cover
class AzureAISettings(TypedDict, total=False):
"""Azure AI Project settings.
Settings are resolved in this order: explicit keyword arguments, values from an
explicitly provided .env file, then environment variables with the prefix
'AZURE_AI_'. If settings are missing after resolution, validation will fail.
Keyword Args:
project_endpoint: The Azure AI Project endpoint URL.
Can be set via environment variable AZURE_AI_PROJECT_ENDPOINT.
model: The name of the model to use.
Can be set via environment variable AZURE_AI_MODEL.
env_file_path: If provided, the .env settings are read from this file path location.
env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
Examples:
.. code-block:: python
from agent_framework.azure import AzureAISettings
# Using environment variables
# Set AZURE_AI_PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com
# Set AZURE_AI_MODEL=gpt-4
settings = AzureAISettings()
# Or passing parameters directly
settings = AzureAISettings(
project_endpoint="https://your-project.cognitiveservices.azure.com", model="gpt-4"
)
# Or loading from a .env file
settings = AzureAISettings(env_file_path="path/to/.env")
"""
project_endpoint: str | None
model: str | None
-109
View File
@@ -1,109 +0,0 @@
[project]
name = "agent-framework-azure-ai"
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.0rc6"
license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework"
urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true"
urls.issues = "https://github.com/microsoft/agent-framework/issues"
classifiers = [
"License :: OSI Approved :: MIT License",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
"agent-framework-core>=1.0.0rc6",
"agent-framework-openai>=1.0.0rc6",
"azure-ai-projects>=2.0.0,<3.0",
"azure-ai-agents>=1.2.0b5,<1.2.0b6",
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
"azure-identity>=1,<2",
"aiohttp>=3.7.0,<4",
]
[tool.uv]
prerelease = "if-necessary-or-explicit"
environments = [
"sys_platform == 'darwin'",
"sys_platform == 'linux'",
"sys_platform == 'win32'"
]
[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 = []
timeout = 120
markers = [
"integration: marks tests as integration tests that require external services",
]
[tool.ruff]
extend = "../../pyproject.toml"
[tool.coverage.run]
omit = [
"**/__init__.py"
]
[tool.pyright]
extends = "../../pyproject.toml"
include = ["agent_framework_azure_ai"]
[tool.mypy]
plugins = ['pydantic.mypy']
strict = true
python_version = "3.10"
ignore_missing_imports = true
disallow_untyped_defs = true
no_implicit_optional = true
check_untyped_defs = true
warn_return_any = true
show_error_codes = true
warn_unused_ignores = false
disallow_incomplete_defs = true
disallow_untyped_decorators = true
[tool.bandit]
targets = ["agent_framework_azure_ai"]
exclude_dirs = ["tests"]
[tool.poe]
executor.type = "uv"
include = "../../shared_tasks.toml"
[tool.poe.tasks.mypy]
help = "Run MyPy for this package."
cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_azure_ai"
[tool.poe.tasks.test]
help = "Run the default unit test suite for this package."
cmd = 'pytest -m "not integration" --cov=agent_framework_azure_ai --cov-report=term-missing:skip-covered tests'
[tool.poe.tasks.integration-tests]
help = "Run the package integration test suite."
cmd = """
pytest --import-mode=importlib
-n logical --dist worksteal
tests
"""
[build-system]
requires = ["flit-core >= 3.11,<4.0"]
build-backend = "flit_core.buildapi"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

@@ -1,61 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from unittest.mock import MagicMock, patch
import pytest
from agent_framework.exceptions import ChatClientInvalidAuthException
from azure.core.credentials import TokenCredential
from azure.core.credentials_async import AsyncTokenCredential
from agent_framework_azure_ai._entra_id_authentication import (
resolve_credential_to_token_provider,
)
TOKEN_ENDPOINT = "https://cognitiveservices.azure.com/.default"
def test_resolve_sync_credential_returns_provider() -> None:
"""Test that a sync TokenCredential is resolved via azure.identity.get_bearer_token_provider."""
mock_credential = MagicMock(spec=TokenCredential)
mock_provider = MagicMock(return_value="token-string")
with patch("azure.identity.get_bearer_token_provider", return_value=mock_provider) as mock_gbtp:
result = resolve_credential_to_token_provider(mock_credential, TOKEN_ENDPOINT)
mock_gbtp.assert_called_once_with(mock_credential, TOKEN_ENDPOINT)
assert result is mock_provider
def test_resolve_async_credential_returns_provider() -> None:
"""Test that an AsyncTokenCredential is resolved via azure.identity.aio.get_bearer_token_provider."""
mock_credential = MagicMock(spec=AsyncTokenCredential)
mock_provider = MagicMock(return_value="token-string")
with patch("azure.identity.aio.get_bearer_token_provider", return_value=mock_provider) as mock_gbtp:
result = resolve_credential_to_token_provider(mock_credential, TOKEN_ENDPOINT)
mock_gbtp.assert_called_once_with(mock_credential, TOKEN_ENDPOINT)
assert result is mock_provider
def test_resolve_callable_provider_passthrough() -> None:
"""Test that a callable token provider is returned as-is, without needing token_endpoint."""
my_provider = lambda: "my-token" # noqa: E731
# Works with token_endpoint
assert resolve_credential_to_token_provider(my_provider, TOKEN_ENDPOINT) is my_provider
# Also works without token_endpoint
assert resolve_credential_to_token_provider(my_provider, None) is my_provider
assert resolve_credential_to_token_provider(my_provider, "") is my_provider
def test_resolve_missing_endpoint_raises() -> None:
"""Test that missing token endpoint raises ChatClientInvalidAuthException."""
mock_credential = MagicMock(spec=TokenCredential)
with pytest.raises(ChatClientInvalidAuthException, match="A token endpoint must be provided"):
resolve_credential_to_token_provider(mock_credential, "")
with pytest.raises(ChatClientInvalidAuthException, match="A token endpoint must be provided"):
resolve_credential_to_token_provider(mock_credential, None) # type: ignore[arg-type]
@@ -1,78 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from pytest import fixture
@fixture
def exclude_list(request: Any) -> list[str]:
"""Fixture that returns a list of environment variables to exclude."""
return request.param if hasattr(request, "param") else []
@fixture
def override_env_param_dict(request: Any) -> dict[str, str]:
"""Fixture that returns a dict of environment variables to override."""
return request.param if hasattr(request, "param") else {}
@fixture()
def azure_ai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore
"""Fixture to set environment variables for AzureAISettings."""
if exclude_list is None:
exclude_list = []
if override_env_param_dict is None:
override_env_param_dict = {}
env_vars = {
"AZURE_AI_PROJECT_ENDPOINT": "https://test-project.cognitiveservices.azure.com/",
"AZURE_AI_MODEL": "test-gpt-4o",
}
env_vars.update(override_env_param_dict) # type: ignore
for key, value in env_vars.items():
if key in exclude_list:
monkeypatch.delenv(key, raising=False) # type: ignore
continue
monkeypatch.setenv(key, value) # type: ignore
return env_vars
@fixture
def mock_agents_client() -> MagicMock:
"""Fixture that provides a mock AgentsClient."""
mock_client = MagicMock()
# Mock agents property
mock_client.create_agent = AsyncMock()
mock_client.delete_agent = AsyncMock()
# Mock agent creation response
mock_agent = MagicMock()
mock_agent.id = "test-agent-id"
mock_client.create_agent.return_value = mock_agent
# Mock threads property
mock_client.threads = MagicMock()
mock_client.threads.create = AsyncMock()
mock_client.messages.create = AsyncMock()
# Mock runs property
mock_client.runs = MagicMock()
mock_client.runs.list = AsyncMock()
mock_client.runs.cancel = AsyncMock()
mock_client.runs.stream = AsyncMock()
mock_client.runs.submit_tool_outputs_stream = AsyncMock()
return mock_client
@fixture
def mock_azure_credential() -> MagicMock:
"""Fixture that provides a mock AsyncTokenCredential."""
return MagicMock()
@@ -1,76 +0,0 @@
%PDF-1.7
%����
1 0 obj
<</Type/Catalog/Pages 2 0 R/Lang(en) /StructTreeRoot 22 0 R/MarkInfo<</Marked true>>/Metadata 132 0 R/ViewerPreferences 133 0 R>>
endobj
2 0 obj
<</Type/Pages/Count 1/Kids[ 4 0 R] >>
endobj
3 0 obj
<</Author(Test Author) /Creator(Test Creator) /Title(Employee Directory) >>
endobj
4 0 obj
<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]/Resources<</Font<</F1 5 0 R>>>>/Contents 6 0 R>>
endobj
5 0 obj
<</Type/Font/Subtype/Type1/BaseFont/Times-Roman>>
endobj
6 0 obj
<</Length 200>>
stream
BT
/F1 12 Tf
50 750 Td
(Employee Directory) Tj
0 -30 Td
(Name: John Smith) Tj
0 -15 Td
(Department: Engineering) Tj
0 -15 Td
(Age: 28) Tj
0 -30 Td
(Name: Alice Johnson) Tj
0 -15 Td
(Department: Sales) Tj
0 -15 Td
(Age: 24) Tj
0 -30 Td
(Name: Bob Wilson) Tj
0 -15 Td
(Department: Marketing) Tj
0 -15 Td
(Age: 35) Tj
ET
endstream
endobj
22 0 obj
<</Type/StructTreeRoot>>
endobj
132 0 obj
<</Type/Metadata/Subtype/XML>>
endobj
133 0 obj
<</DisplayDocTitle true>>
endobj
xref
0 10
0000000000 65535 f
0000000015 00000 n
0000000152 00000 n
0000000209 00000 n
0000000300 00000 n
0000000420 00000 n
0000000490 00000 n
0000000000 65535 f
0000000000 65535 f
0000000000 65535 f
22 1
0000000740 00000 n
132 2
0000000780 00000 n
0000000820 00000 n
trailer
<</Size 134/Root 1 0 R/Info 3 0 R>>
startxref
860
%%EOF
+3 -1
View File
@@ -9,7 +9,7 @@ Azure Cosmos DB history provider integration for Agent Framework.
## Usage
```python
from agent_framework_azure_cosmos import CosmosHistoryProvider
from agent_framework.azure import CosmosHistoryProvider
provider = CosmosHistoryProvider(
endpoint="https://<account>.documents.azure.com:443/",
@@ -24,5 +24,7 @@ Container name is configured on the provider. `session_id` is used as the partit
## Import Path
```python
from agent_framework.azure import CosmosHistoryProvider
# or directly:
from agent_framework_azure_cosmos import CosmosHistoryProvider
```
+11 -2
View File
@@ -14,7 +14,7 @@ The Azure Cosmos DB integration provides `CosmosHistoryProvider` for persistent
```python
from azure.identity.aio import DefaultAzureCredential
from agent_framework_azure_cosmos import CosmosHistoryProvider
from agent_framework.azure import CosmosHistoryProvider
provider = CosmosHistoryProvider(
endpoint="https://<account>.documents.azure.com:443/",
@@ -35,4 +35,13 @@ Container naming behavior:
- Container name is configured on the provider (`container_name` or `AZURE_COSMOS_CONTAINER_NAME`)
- `session_id` is used as the Cosmos partition key for reads/writes
See `samples/cosmos_history_provider.py` for a runnable package-local example.
See the [conversation samples](../../samples/02-agents/conversations/) for runnable examples, including
[`cosmos_history_provider.py`](../../samples/02-agents/conversations/cosmos_history_provider.py).
## Import Paths
```python
from agent_framework.azure import CosmosHistoryProvider
# or directly:
from agent_framework_azure_cosmos import CosmosHistoryProvider
```
@@ -1,20 +0,0 @@
# Azure Cosmos DB Package Samples
This folder contains samples for `agent-framework-azure-cosmos`.
| File | Description |
| --- | --- |
| [`cosmos_history_provider.py`](cosmos_history_provider.py) | Demonstrates an Agent using `CosmosHistoryProvider` with `FoundryChatClient` (configured against an Azure AI Foundry project endpoint), provider-configured container name, and `session_id` partitioning. |
## Prerequisites
- `AZURE_COSMOS_ENDPOINT`
- `AZURE_COSMOS_DATABASE_NAME`
- `AZURE_COSMOS_CONTAINER_NAME`
- `AZURE_COSMOS_KEY` (or equivalent credential flow)
## Run
```bash
uv run --directory packages/azure-cosmos python samples/cosmos_history_provider.py
```
@@ -1,3 +0,0 @@
# Copyright (c) Microsoft. All rights reserved.
"""Samples for the Azure Cosmos history provider package."""
+1 -1
View File
@@ -34,8 +34,8 @@ FOUNDRY_PROJECT_ENDPOINT=...
FOUNDRY_MODEL=...
...
OPENAI_API_KEY=sk-...
OPENAI_CHAT_COMPLETION_MODEL=...
OPENAI_CHAT_MODEL=...
OPENAI_RESPONSES_MODEL=...
...
AZURE_OPENAI_API_KEY=...
AZURE_OPENAI_ENDPOINT=...
@@ -53,6 +53,7 @@ from typing import (
runtime_checkable,
)
from ._feature_stage import ExperimentalFeature, experimental
from ._tools import FunctionTool
from ._types import AgentResponse, Message
@@ -64,6 +65,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
@experimental(feature_id=ExperimentalFeature.EVALS)
class EvalNotPassedError(Exception):
"""Raised when evaluation results contain failures."""
@@ -71,6 +73,7 @@ class EvalNotPassedError(Exception):
# region Core types
@experimental(feature_id=ExperimentalFeature.EVALS)
@runtime_checkable
class ConversationSplitter(Protocol):
"""Strategy for splitting a conversation into (query, response) messages.
@@ -103,6 +106,7 @@ class ConversationSplitter(Protocol):
def __call__(self, conversation: list[Message]) -> tuple[list[Message], list[Message]]: ...
@experimental(feature_id=ExperimentalFeature.EVALS)
class ConversationSplit(str, Enum):
"""Built-in conversation split strategies.
@@ -131,6 +135,7 @@ class ConversationSplit(str, Enum):
return _BUILT_IN_SPLITTERS[self](conversation)
@experimental(feature_id=ExperimentalFeature.EVALS)
@dataclass
class ExpectedToolCall:
"""A tool call that an agent is expected to make.
@@ -173,6 +178,7 @@ _BUILT_IN_SPLITTERS: dict[ConversationSplit, Callable[[list[Message]], tuple[lis
}
@experimental(feature_id=ExperimentalFeature.EVALS)
class EvalItem:
"""A single item to be evaluated.
@@ -295,6 +301,7 @@ class EvalItem:
# region Score and result types
@experimental(feature_id=ExperimentalFeature.EVALS)
@dataclass
class EvalScoreResult:
"""Result from a single evaluator on a single item.
@@ -312,6 +319,7 @@ class EvalScoreResult:
sample: dict[str, Any] | None = None
@experimental(feature_id=ExperimentalFeature.EVALS)
@dataclass
class EvalItemResult:
"""Per-item result from an evaluation run.
@@ -358,6 +366,7 @@ class EvalItemResult:
return self.status == "fail"
@experimental(feature_id=ExperimentalFeature.EVALS)
class EvalResults:
"""Results from an evaluation run by a single provider.
@@ -493,6 +502,7 @@ class EvalResults:
# region Evaluator protocol
@experimental(feature_id=ExperimentalFeature.EVALS)
@runtime_checkable
class Evaluator(Protocol):
"""Protocol for evaluation providers.
@@ -543,6 +553,7 @@ class Evaluator(Protocol):
# region Converter
@experimental(feature_id=ExperimentalFeature.EVALS)
class AgentEvalConverter:
"""Converts agent-framework types to evaluation format.
@@ -846,6 +857,7 @@ def _extract_overall_query(workflow_result: WorkflowRunResult) -> str | None:
# region Local evaluation checks
@experimental(feature_id=ExperimentalFeature.EVALS)
@dataclass
class CheckResult:
"""Result of a single check on a single evaluation item.
@@ -870,6 +882,7 @@ an awaitable ``CheckResult``; they will be awaited automatically by
"""
@experimental(feature_id=ExperimentalFeature.EVALS)
def keyword_check(*keywords: str, case_sensitive: bool = False) -> EvalCheck:
"""Check that the response contains all specified keywords.
@@ -897,6 +910,7 @@ def keyword_check(*keywords: str, case_sensitive: bool = False) -> EvalCheck:
return _check
@experimental(feature_id=ExperimentalFeature.EVALS)
def tool_called_check(*tool_names: str, mode: Literal["all", "any"] = "all") -> EvalCheck:
"""Check that specific tools were called during the conversation.
@@ -979,6 +993,7 @@ def _extract_tool_calls(item: EvalItem) -> list[tuple[str, dict[str, Any] | None
return calls
@experimental(feature_id=ExperimentalFeature.EVALS)
def tool_calls_present(item: EvalItem) -> CheckResult:
"""Check that all expected tool calls were made (unordered, extras OK).
@@ -1020,6 +1035,7 @@ def tool_calls_present(item: EvalItem) -> CheckResult:
)
@experimental(feature_id=ExperimentalFeature.EVALS)
def tool_call_args_match(item: EvalItem) -> CheckResult:
"""Check that expected tool calls match on name and arguments.
@@ -1220,6 +1236,7 @@ def evaluator(fn: Callable[..., Any], /) -> EvalCheck: ...
def evaluator(*, name: str | None = None) -> Callable[[Callable[..., Any]], EvalCheck]: ...
@experimental(feature_id=ExperimentalFeature.EVALS)
def evaluator(
fn: Callable[..., Any] | None = None,
*,
@@ -1322,6 +1339,7 @@ async def _run_check(check_fn: EvalCheck, item: EvalItem) -> CheckResult:
return result
@experimental(feature_id=ExperimentalFeature.EVALS)
class LocalEvaluator:
"""Evaluation provider that runs checks locally without API calls.
@@ -1431,6 +1449,7 @@ class LocalEvaluator:
# region Public orchestration functions
@experimental(feature_id=ExperimentalFeature.EVALS)
async def evaluate_agent(
*,
agent: SupportsAgentRun | None = None,
@@ -1634,6 +1653,7 @@ async def evaluate_agent(
return await _run_evaluators(evaluators, items, eval_name=name)
@experimental(feature_id=ExperimentalFeature.EVALS)
async def evaluate_workflow(
*,
workflow: Workflow,
@@ -46,6 +46,7 @@ class ExperimentalFeature(str, Enum):
on enum membership or attribute presence over time.
"""
EVALS = "EVALS"
SKILLS = "SKILLS"
+13 -1
View File
@@ -1796,7 +1796,19 @@ def prepend_instructions_to_messages(
if isinstance(instructions, str):
instructions = [instructions]
instruction_messages = [Message(role, [instr]) for instr in instructions]
# Skip instructions that are already present as leading messages with the
# same role and text. This prevents duplicate system messages when
# instructions are injected by multiple layers (e.g. Agent + chat client).
deduplicated: list[str] = []
for idx, instr in enumerate(instructions):
if idx < len(messages) and messages[idx].role == role and messages[idx].text == instr:
continue
deduplicated.append(instr)
if not deduplicated:
return messages
instruction_messages = [Message(role, [instr]) for instr in deduplicated]
return [*instruction_messages, *messages]
@@ -6,7 +6,7 @@ import json
import logging
import sys
import uuid
from collections.abc import AsyncIterable, Awaitable, Sequence
from collections.abc import AsyncIterable, Awaitable, Mapping, Sequence
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast, overload
@@ -152,7 +152,8 @@ class WorkflowAgent(BaseAgent):
session: AgentSession | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> ResponseStream[AgentResponseUpdate, AgentResponse]: ...
@overload
@@ -164,7 +165,8 @@ class WorkflowAgent(BaseAgent):
session: AgentSession | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AgentResponse: ...
def run(
@@ -175,7 +177,8 @@ class WorkflowAgent(BaseAgent):
session: AgentSession | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> ResponseStream[AgentResponseUpdate, AgentResponse] | Awaitable[AgentResponse]:
"""Get a response from the workflow agent.
@@ -192,8 +195,12 @@ class WorkflowAgent(BaseAgent):
checkpoint_storage: Runtime checkpoint storage. When provided with checkpoint_id,
used to load and restore the checkpoint. When provided without checkpoint_id,
enables checkpointing for this run.
**kwargs: Additional keyword arguments passed through to underlying workflow
and tool functions.
function_invocation_kwargs: Keyword arguments forwarded to tool invocations in
subagents. Either a mapping of agent name/executor id to kwargs, or a flat
mapping of kwargs for all tool invocations.
client_kwargs: Keyword arguments forwarded to chat client calls in
subagents. Either a mapping of agent name/executor id to kwargs, or a flat
mapping of kwargs for all chat client calls.
Returns:
When stream=True: An AsyncIterable[AgentResponseUpdate] for streaming updates.
@@ -208,10 +215,26 @@ class WorkflowAgent(BaseAgent):
response_id = str(uuid.uuid4())
if stream:
return ResponseStream(
self._run_stream_impl(messages, response_id, session, checkpoint_id, checkpoint_storage, **kwargs),
self._run_stream_impl(
messages,
response_id,
session,
checkpoint_id,
checkpoint_storage,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
),
finalizer=AgentResponse.from_updates,
)
return self._run_impl(messages, response_id, session, checkpoint_id, checkpoint_storage, **kwargs)
return self._run_impl(
messages,
response_id,
session,
checkpoint_id,
checkpoint_storage,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
)
async def _run_impl(
self,
@@ -220,7 +243,8 @@ class WorkflowAgent(BaseAgent):
session: AgentSession | None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AgentResponse:
"""Internal implementation of non-streaming execution.
@@ -230,8 +254,8 @@ class WorkflowAgent(BaseAgent):
session: The agent session for conversation context.
checkpoint_id: ID of checkpoint to restore from.
checkpoint_storage: Runtime checkpoint storage.
**kwargs: Additional keyword arguments passed through to the underlying
workflow and tool functions.
function_invocation_kwargs: Optional kwargs for tool invocations.
client_kwargs: Optional kwargs for chat client calls.
Returns:
An AgentResponse representing the workflow execution results.
@@ -264,7 +288,12 @@ class WorkflowAgent(BaseAgent):
output_events: list[WorkflowEvent[Any]] = []
async for event in self._run_core(
session_messages, checkpoint_id, checkpoint_storage, streaming=False, **kwargs
session_messages,
checkpoint_id,
checkpoint_storage,
streaming=False,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
if event.type == "output" or event.type == "request_info":
output_events.append(event)
@@ -285,7 +314,8 @@ class WorkflowAgent(BaseAgent):
session: AgentSession | None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AsyncIterable[AgentResponseUpdate]:
"""Internal implementation of streaming execution.
@@ -295,8 +325,8 @@ class WorkflowAgent(BaseAgent):
session: The agent session for conversation context.
checkpoint_id: ID of checkpoint to restore from.
checkpoint_storage: Runtime checkpoint storage.
**kwargs: Additional keyword arguments passed through to the underlying
workflow and tool functions.
function_invocation_kwargs: Optional kwargs for tool invocations.
client_kwargs: Optional kwargs for chat client calls.
Yields:
AgentResponseUpdate objects representing the workflow execution progress.
@@ -329,7 +359,12 @@ class WorkflowAgent(BaseAgent):
session_messages: list[Message] = session_context.get_messages(include_input=True)
all_updates: list[AgentResponseUpdate] = []
async for event in self._run_core(
session_messages, checkpoint_id, checkpoint_storage, streaming=True, **kwargs
session_messages,
checkpoint_id,
checkpoint_storage,
streaming=True,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
updates = self._convert_workflow_event_to_agent_response_updates(response_id, event)
for update in updates:
@@ -349,7 +384,8 @@ class WorkflowAgent(BaseAgent):
checkpoint_id: str | None,
checkpoint_storage: CheckpointStorage | None,
streaming: bool,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AsyncIterable[WorkflowEvent]:
"""Core implementation that yields workflow events for both streaming and non-streaming modes.
@@ -358,8 +394,8 @@ class WorkflowAgent(BaseAgent):
checkpoint_id: ID of checkpoint to restore from.
checkpoint_storage: Runtime checkpoint storage.
streaming: Whether to use streaming workflow methods.
**kwargs: Additional keyword arguments passed through to the underlying
workflow and tool functions.
function_invocation_kwargs: Optional kwargs for tool invocations.
client_kwargs: Optional kwargs for chat client calls.
Yields:
WorkflowEvent objects from the workflow execution.
@@ -371,10 +407,19 @@ class WorkflowAgent(BaseAgent):
if bool(self.pending_requests):
function_responses = self._process_pending_requests(input_messages)
if streaming:
async for event in self.workflow.run(responses=function_responses, stream=True, **kwargs):
async for event in self.workflow.run(
responses=function_responses,
stream=True,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
yield event
else:
for event in await self.workflow.run(responses=function_responses, **kwargs):
for event in await self.workflow.run(
responses=function_responses,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
yield event
elif checkpoint_id is not None:
@@ -383,14 +428,16 @@ class WorkflowAgent(BaseAgent):
stream=True,
checkpoint_id=checkpoint_id,
checkpoint_storage=checkpoint_storage,
**kwargs,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
yield event
else:
for event in await self.workflow.run(
checkpoint_id=checkpoint_id,
checkpoint_storage=checkpoint_storage,
**kwargs,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
yield event
@@ -400,14 +447,16 @@ class WorkflowAgent(BaseAgent):
message=input_messages,
stream=True,
checkpoint_storage=checkpoint_storage,
**kwargs,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
yield event
else:
for event in await self.workflow.run(
message=input_messages,
checkpoint_storage=checkpoint_storage,
**kwargs,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
yield event
@@ -2,7 +2,7 @@
import logging
import sys
from collections.abc import Awaitable, Callable, Mapping
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any, Literal, cast
@@ -14,7 +14,7 @@ from .._agents import SupportsAgentRun
from .._sessions import AgentSession
from .._types import AgentResponse, AgentResponseUpdate, Message, ResponseStream
from ._agent_utils import resolve_agent_id
from ._const import WORKFLOW_RUN_KWARGS_KEY
from ._const import GLOBAL_KWARGS_KEY, WORKFLOW_RUN_KWARGS_KEY
from ._executor import Executor, handler
from ._message_utils import normalize_messages_input
from ._request_info_mixin import response_handler
@@ -335,15 +335,17 @@ class AgentExecutor(Executor):
Returns:
The complete AgentResponse, or None if waiting for user input.
"""
run_kwargs, options = self._prepare_agent_run_args(ctx.get_state(WORKFLOW_RUN_KWARGS_KEY, {}))
function_invocation_kwargs, client_kwargs = self._prepare_agent_run_args(
ctx.get_state(WORKFLOW_RUN_KWARGS_KEY, {})
)
run_agent = cast(Callable[..., Awaitable[AgentResponse[Any]]], self._agent.run)
response = await run_agent(
self._cache,
stream=False,
session=self._session,
options=options,
**run_kwargs,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
)
await ctx.yield_output(response)
@@ -365,7 +367,9 @@ class AgentExecutor(Executor):
Returns:
The complete AgentResponse, or None if waiting for user input.
"""
run_kwargs, options = self._prepare_agent_run_args(ctx.get_state(WORKFLOW_RUN_KWARGS_KEY, {}))
function_invocation_kwargs, client_kwargs = self._prepare_agent_run_args(
ctx.get_state(WORKFLOW_RUN_KWARGS_KEY, {})
)
updates: list[AgentResponseUpdate] = []
streamed_user_input_requests: list[Content] = []
@@ -374,8 +378,8 @@ class AgentExecutor(Executor):
self._cache,
stream=True,
session=self._session,
options=options,
**run_kwargs,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
)
async for update in stream:
updates.append(update)
@@ -421,74 +425,58 @@ class AgentExecutor(Executor):
return response
# Parameters that are explicitly passed to agent.run() by AgentExecutor
# and must not appear in **run_kwargs to avoid TypeError from duplicate values.
_RESERVED_RUN_PARAMS: frozenset[str] = frozenset({"session", "stream", "messages"})
def _prepare_agent_run_args(
self,
raw_run_kwargs: dict[str, Any],
) -> tuple[dict[str, Any] | None, dict[str, Any] | None]:
"""Prepare function_invocation_kwargs and client_kwargs for agent.run().
@staticmethod
def _prepare_agent_run_args(raw_run_kwargs: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] | None]:
"""Prepare kwargs and options for agent.run(), avoiding duplicate option passing.
Extracts ``function_invocation_kwargs`` and ``client_kwargs`` from the
workflow state dict, resolving per-executor entries using ``self.id``. The
``__global__`` sentinel key (set by ``Workflow._resolve_invocation_kwargs``) denotes
global kwargs that apply to all executors. Per-executor dicts use executor IDs as
keys; this executor extracts only its own entry.
Workflow-level kwargs are propagated to tool calls through
`options.additional_function_arguments`. If workflow kwargs include an
`options` key, merge it into the final options object and remove it from
kwargs before spreading `**run_kwargs`.
Reserved parameters (session, stream, messages) that are explicitly
managed by AgentExecutor are stripped from run_kwargs to prevent
``TypeError: got multiple values for keyword argument`` collisions.
Returns:
A 2-tuple of (function_invocation_kwargs, client_kwargs).
"""
run_kwargs = dict(raw_run_kwargs)
fi_resolved = raw_run_kwargs.get("function_invocation_kwargs")
ci_resolved = raw_run_kwargs.get("client_kwargs")
# Strip reserved params that AgentExecutor passes explicitly to agent.run().
for key in AgentExecutor._RESERVED_RUN_PARAMS:
if key in run_kwargs:
logger.warning(
"Workflow kwarg '%s' is reserved by AgentExecutor and will be ignored. "
"Remove it from workflow.run() kwargs to silence this warning.",
key,
)
run_kwargs.pop(key)
function_invocation_kwargs = self._resolve_executor_kwargs(fi_resolved)
client_kwargs = self._resolve_executor_kwargs(ci_resolved)
options_from_workflow = run_kwargs.pop("options", None)
workflow_additional_args = run_kwargs.pop("additional_function_arguments", None)
return function_invocation_kwargs, client_kwargs
options: dict[str, Any] = {}
if options_from_workflow is not None:
if isinstance(options_from_workflow, Mapping):
options_from_workflow_map = cast(Mapping[str, Any], options_from_workflow)
for key, value in options_from_workflow_map.items():
options[key] = value
else:
logger.warning(
"Ignoring non-mapping workflow 'options' kwarg of type %s for AgentExecutor %s.",
type(options_from_workflow).__name__,
AgentExecutor.__name__,
)
def _resolve_executor_kwargs(self, resolved: dict[str, Any] | None) -> dict[str, Any] | None:
"""Extract this executor's kwargs from a resolved invocation kwargs dict.
existing_additional_args = options.get("additional_function_arguments")
additional_args: dict[str, Any]
if isinstance(existing_additional_args, Mapping):
existing_additional_args_map = cast(Mapping[str, Any], existing_additional_args)
additional_args = {key: value for key, value in existing_additional_args_map.items()}
Args:
resolved: The resolved dict produced by ``Workflow._resolve_invocation_kwargs``,
containing either a ``__global__`` key (global kwargs) or executor-ID keys
(per-executor kwargs). May also be ``None``.
Returns:
The kwargs for this executor, or ``None`` if not applicable.
"""
if not isinstance(resolved, dict):
return None
# Use explicit key-presence checks so that an empty per-executor dict is
# honoured (e.g. to clear kwargs) instead of falling through to global.
if self.id in resolved:
executor_kwargs = resolved[self.id]
elif GLOBAL_KWARGS_KEY in resolved:
executor_kwargs = resolved[GLOBAL_KWARGS_KEY]
else:
additional_args = {}
return None
if workflow_additional_args is not None:
if isinstance(workflow_additional_args, Mapping):
workflow_additional_args_map = cast(Mapping[str, Any], workflow_additional_args)
additional_args.update({key: value for key, value in workflow_additional_args_map.items()})
else:
logger.warning(
"Ignoring non-mapping workflow 'additional_function_arguments' kwarg of type %s for AgentExecutor %s.", # noqa: E501
type(workflow_additional_args).__name__,
AgentExecutor.__name__,
)
if not isinstance(executor_kwargs, dict):
logger.warning(
"Executor %s expected a dict for its kwargs, but got %s. Ignoring.",
self.id,
type(executor_kwargs), # type: ignore
)
if run_kwargs:
additional_args.update(run_kwargs)
return None
if additional_args:
options["additional_function_arguments"] = additional_args
return run_kwargs, options or None
return executor_kwargs # type: ignore
@@ -14,6 +14,10 @@ INTERNAL_SOURCE_PREFIX = "internal"
# to pass kwargs from workflow.run() through to agent.run() and @tool functions.
WORKFLOW_RUN_KWARGS_KEY = "_workflow_run_kwargs"
# Sentinel key used in resolved invocation kwargs dicts to denote global kwargs
# that apply to all executors (as opposed to per-executor keyed entries).
GLOBAL_KWARGS_KEY = "__global__"
def INTERNAL_SOURCE_ID(executor_id: str) -> str:
"""Generate an internal source ID for a given executor."""
@@ -10,14 +10,14 @@ import json
import logging
import types
import uuid
from collections.abc import AsyncIterable, Awaitable, Callable, Sequence
from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, Sequence
from typing import Any, Literal, overload
from .._types import ResponseStream
from ..observability import OtelAttr, capture_exception, create_workflow_span
from ._agent import WorkflowAgent
from ._checkpoint import CheckpointStorage
from ._const import DEFAULT_MAX_ITERATIONS, WORKFLOW_RUN_KWARGS_KEY
from ._const import DEFAULT_MAX_ITERATIONS, GLOBAL_KWARGS_KEY, WORKFLOW_RUN_KWARGS_KEY
from ._edge import (
EdgeGroup,
FanOutEdgeGroup,
@@ -180,7 +180,6 @@ class Workflow(DictConvertible):
description: str | None = None,
max_iterations: int = DEFAULT_MAX_ITERATIONS,
output_executors: list[str] | None = None,
**kwargs: Any,
):
"""Initialize the workflow with a list of edges.
@@ -198,7 +197,6 @@ class Workflow(DictConvertible):
WorkflowBuilder, this will be the description of the builder.
output_executors: Optional list of executor IDs whose outputs will be considered workflow outputs.
If None or empty, all executor outputs are treated as workflow outputs.
kwargs: Additional keyword arguments. Unused in this implementation.
"""
self.edge_groups = list(edge_groups)
self.executors = dict(executors)
@@ -300,7 +298,8 @@ class Workflow(DictConvertible):
initial_executor_fn: Callable[[], Awaitable[None]] | None = None,
reset_context: bool = True,
streaming: bool = False,
run_kwargs: dict[str, Any] | None = None,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AsyncIterable[WorkflowEvent]:
"""Private method to run workflow with proper tracing.
@@ -311,7 +310,10 @@ class Workflow(DictConvertible):
initial_executor_fn: Optional function to execute initial executor
reset_context: Whether to reset the context for a new run
streaming: Whether to enable streaming mode for agents
run_kwargs: Optional kwargs to store in State for agent invocations
function_invocation_kwargs: Optional kwargs to store in State for function
invocations in subagents
client_kwargs: Optional kwargs to store in State for chat client
invocations in subagents
Yields:
WorkflowEvent: The events generated during the workflow execution.
@@ -350,8 +352,17 @@ class Workflow(DictConvertible):
# Only overwrite when new kwargs are explicitly provided or state was
# just cleared (fresh run). On continuation (reset_context=False) with
# no new kwargs, preserve the kwargs from the original run.
if run_kwargs is not None:
self._state.set(WORKFLOW_RUN_KWARGS_KEY, run_kwargs)
if function_invocation_kwargs is not None or client_kwargs is not None:
combined_kwargs: dict[str, Any] = {}
if function_invocation_kwargs is not None:
combined_kwargs["function_invocation_kwargs"] = self._resolve_invocation_kwargs(
function_invocation_kwargs, "function_invocation_kwargs"
)
if client_kwargs is not None:
combined_kwargs["client_kwargs"] = self._resolve_invocation_kwargs(
client_kwargs, "client_kwargs"
)
self._state.set(WORKFLOW_RUN_KWARGS_KEY, combined_kwargs)
elif reset_context:
self._state.set(WORKFLOW_RUN_KWARGS_KEY, {})
self._state.commit() # Commit immediately so kwargs are available
@@ -459,10 +470,11 @@ class Workflow(DictConvertible):
message: Any | None = None,
*,
stream: Literal[True],
responses: dict[str, Any] | None = None,
responses: Mapping[str, Any] | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
) -> ResponseStream[WorkflowEvent, WorkflowRunResult]: ...
@overload
@@ -471,11 +483,12 @@ class Workflow(DictConvertible):
message: Any | None = None,
*,
stream: Literal[False] = ...,
responses: dict[str, Any] | None = None,
responses: Mapping[str, Any] | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
include_status_events: bool = False,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Any] | None = None,
) -> Awaitable[WorkflowRunResult]: ...
def run(
@@ -483,11 +496,12 @@ class Workflow(DictConvertible):
message: Any | None = None,
*,
stream: bool = False,
responses: dict[str, Any] | None = None,
responses: Mapping[str, Any] | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
include_status_events: bool = False,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> ResponseStream[WorkflowEvent, WorkflowRunResult] | Awaitable[WorkflowRunResult]:
"""Run the workflow, optionally streaming events.
@@ -509,7 +523,12 @@ class Workflow(DictConvertible):
(restore then send responses).
checkpoint_storage: Runtime checkpoint storage.
include_status_events: Whether to include status events (non-streaming only).
**kwargs: Additional keyword arguments to pass through to agent invocations.
function_invocation_kwargs: Keyword arguments forwarded to tool invocations in
subagents. Either a mapping for agent name or agent executor id to kwargs,
or a flat mapping of kwargs for all tool invocations.
client_kwargs: Keyword arguments forwarded to chat client calls in
subagents. Either a mapping for agent name or agent executor id to kwargs,
or a flat mapping of kwargs for all chat client calls.
Returns:
When stream=True: A ResponseStream[WorkflowEvent, WorkflowRunResult] for
@@ -530,7 +549,8 @@ class Workflow(DictConvertible):
checkpoint_id=checkpoint_id,
checkpoint_storage=checkpoint_storage,
streaming=stream,
**kwargs,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
),
finalizer=functools.partial(self._finalize_events, include_status_events=include_status_events),
cleanup_hooks=[
@@ -546,11 +566,12 @@ class Workflow(DictConvertible):
self,
message: Any | None = None,
*,
responses: dict[str, Any] | None = None,
responses: Mapping[str, Any] | None = None,
checkpoint_id: str | None = None,
checkpoint_storage: CheckpointStorage | None = None,
streaming: bool = False,
**kwargs: Any,
function_invocation_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
client_kwargs: Mapping[str, Mapping[str, Any]] | Mapping[str, Any] | None = None,
) -> AsyncIterable[WorkflowEvent]:
"""Single core execution path for both streaming and non-streaming modes.
@@ -569,11 +590,8 @@ class Workflow(DictConvertible):
initial_executor_fn=initial_executor_fn,
reset_context=reset_context,
streaming=streaming,
# Empty **kwargs (no caller-provided kwargs) is collapsed to None so that
# continuation calls without explicit kwargs preserve the original run's kwargs.
# A non-empty kwargs dict (even one with empty values like {"key": {}})
# is passed through and will overwrite stored kwargs.
run_kwargs=kwargs if kwargs else None,
function_invocation_kwargs=function_invocation_kwargs,
client_kwargs=client_kwargs,
):
if event.type == "output" and not self._should_yield_output_event(event):
continue
@@ -624,7 +642,7 @@ class Workflow(DictConvertible):
@staticmethod
def _validate_run_params(
message: Any | None,
responses: dict[str, Any] | None,
responses: Mapping[str, Any] | None,
checkpoint_id: str | None,
) -> None:
"""Validate parameter combinations for run().
@@ -650,7 +668,7 @@ class Workflow(DictConvertible):
def _resolve_execution_mode(
self,
message: Any | None,
responses: dict[str, Any] | None,
responses: Mapping[str, Any] | None,
checkpoint_id: str | None,
checkpoint_storage: CheckpointStorage | None,
) -> tuple[Callable[[], Awaitable[None]], bool]:
@@ -680,7 +698,7 @@ class Workflow(DictConvertible):
self,
checkpoint_id: str,
checkpoint_storage: CheckpointStorage | None,
responses: dict[str, Any],
responses: Mapping[str, Any],
) -> None:
"""Restore from a checkpoint then send responses to pending requests.
@@ -700,7 +718,7 @@ class Workflow(DictConvertible):
await self._runner.restore_from_checkpoint(checkpoint_id, checkpoint_storage)
await self._send_responses_internal(responses)
async def _send_responses_internal(self, responses: dict[str, Any]) -> None:
async def _send_responses_internal(self, responses: Mapping[str, Any]) -> None:
"""Internal method to validate and send responses to the executors."""
pending_requests = await self._runner_context.get_pending_request_info_events()
if not pending_requests:
@@ -739,6 +757,44 @@ class Workflow(DictConvertible):
raise ValueError(f"Executor with ID {executor_id} not found.")
return self.executors[executor_id]
def _resolve_invocation_kwargs(
self,
kwargs: Mapping[str, Any],
param_name: str,
) -> dict[str, Any]:
"""Resolve invocation kwargs into a normalized per-executor or global format.
Detects whether the provided kwargs dict uses per-executor targeting by checking
if any top-level key matches a known executor ID in the workflow. If at least one
key matches, all entries are treated as per-executor. Otherwise the dict is treated
as global kwargs that apply to every executor.
Args:
kwargs: The raw invocation kwargs from the caller.
param_name: The parameter name (for logging), e.g. ``"function_invocation_kwargs"``.
Returns:
A dict with either:
- ``{"__global__": <original dict>}`` for global kwargs, or
- The original dict unchanged for per-executor kwargs.
"""
executor_ids = set(self.executors.keys())
matched_ids = kwargs.keys() & executor_ids
if matched_ids:
logger.info(
"Detected per-executor %s: executor ID(s) %s found in keys. "
"All entries will be treated as per-executor.",
param_name,
matched_ids,
)
return dict(kwargs)
logger.info(
"No executor IDs found in %s keys; treating as global kwargs for all executors.",
param_name,
)
return {GLOBAL_KWARGS_KEY: dict(kwargs)}
def _should_yield_output_event(self, event: WorkflowEvent[Any]) -> bool:
"""Determine if an output event should be yielded as a workflow output.
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
from ._workflow import Workflow
from ._checkpoint_encoding import decode_checkpoint_value
from ._const import WORKFLOW_RUN_KWARGS_KEY
from ._const import GLOBAL_KWARGS_KEY, WORKFLOW_RUN_KWARGS_KEY
from ._events import (
WorkflowEvent,
WorkflowRunState,
@@ -387,8 +387,28 @@ class WorkflowExecutor(Executor):
# Get kwargs from parent workflow's State to propagate to subworkflow
parent_kwargs: dict[str, Any] = ctx.get_state(WORKFLOW_RUN_KWARGS_KEY, {})
# Extract invocation kwargs recognised by Workflow.run()
# The state stores resolved format (with __global__ wrapper for global kwargs).
# Unwrap __global__ before passing to the subworkflow so it gets re-resolved
# against the subworkflow's own executor IDs.
fi_kwargs: dict[str, Any] | None = None
ci_kwargs: dict[str, Any] | None = None
for key in ("function_invocation_kwargs", "client_kwargs"):
resolved = parent_kwargs.get(key)
if isinstance(resolved, dict):
# Unwrap global sentinel; pass per-executor dicts as-is
unwrapped: dict[str, Any] = resolved.get(GLOBAL_KWARGS_KEY, resolved) # type: ignore
if key == "function_invocation_kwargs":
fi_kwargs = unwrapped # type: ignore
else:
ci_kwargs = unwrapped # type: ignore
# Run the sub-workflow and collect all events, passing parent kwargs
result = await self.workflow.run(input_data, **parent_kwargs)
result = await self.workflow.run(
input_data,
function_invocation_kwargs=fi_kwargs, # type: ignore
client_kwargs=ci_kwargs, # type: ignore
)
logger.debug(
f"WorkflowExecutor {self.id} sub-workflow {self.workflow.id} "
@@ -14,13 +14,7 @@ _IMPORTS: dict[str, tuple[str, str]] = {
"AgentResponseCallbackProtocol": ("agent_framework_durabletask", "agent-framework-durabletask"),
"AzureAISearchContextProvider": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"),
"AzureAISearchSettings": ("agent_framework_azure_ai_search", "agent-framework-azure-ai-search"),
"AzureAISettings": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"AzureAIInferenceEmbeddingClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"AzureAIInferenceEmbeddingOptions": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"AzureAIInferenceEmbeddingSettings": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"RawAzureAIInferenceEmbeddingClient": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"AzureCredentialTypes": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"AzureTokenProvider": ("agent_framework_azure_ai", "agent-framework-azure-ai"),
"CosmosHistoryProvider": ("agent_framework_azure_cosmos", "agent-framework-azure-cosmos"),
"DurableAIAgent": ("agent_framework_durabletask", "agent-framework-durabletask"),
"DurableAIAgentClient": ("agent_framework_durabletask", "agent-framework-durabletask"),
"DurableAIAgentOrchestrationContext": ("agent_framework_durabletask", "agent-framework-durabletask"),
@@ -3,19 +3,11 @@
# Type stubs for the agent_framework.azure lazy-loading namespace.
# Install the relevant packages for full type support.
from agent_framework_azure_ai import (
AzureAIInferenceEmbeddingClient,
AzureAIInferenceEmbeddingOptions,
AzureAIInferenceEmbeddingSettings,
AzureAISettings,
AzureCredentialTypes,
AzureTokenProvider,
RawAzureAIInferenceEmbeddingClient,
)
from agent_framework_azure_ai_search import (
AzureAISearchContextProvider,
AzureAISearchSettings,
)
from agent_framework_azure_cosmos import CosmosHistoryProvider
from agent_framework_azurefunctions import AgentFunctionApp
from agent_framework_durabletask import (
AgentCallbackContext,
@@ -30,17 +22,11 @@ __all__ = [
"AgentCallbackContext",
"AgentFunctionApp",
"AgentResponseCallbackProtocol",
"AzureAIInferenceEmbeddingClient",
"AzureAIInferenceEmbeddingOptions",
"AzureAIInferenceEmbeddingSettings",
"AzureAISearchContextProvider",
"AzureAISearchSettings",
"AzureAISettings",
"AzureCredentialTypes",
"AzureTokenProvider",
"CosmosHistoryProvider",
"DurableAIAgent",
"DurableAIAgentClient",
"DurableAIAgentOrchestrationContext",
"DurableAIAgentWorker",
"RawAzureAIInferenceEmbeddingClient",
]
@@ -16,6 +16,9 @@ _IMPORTS: dict[str, tuple[str, str]] = {
"FoundryAgent": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryChatOptions": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryEmbeddingClient": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryEmbeddingOptions": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryEmbeddingSettings": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryEvals": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryMemoryProvider": ("agent_framework_foundry", "agent-framework-foundry"),
"FoundryLocalChatOptions": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
@@ -25,6 +28,7 @@ _IMPORTS: dict[str, tuple[str, str]] = {
"RawFoundryAgent": ("agent_framework_foundry", "agent-framework-foundry"),
"RawFoundryAgentChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
"RawFoundryChatClient": ("agent_framework_foundry", "agent-framework-foundry"),
"RawFoundryEmbeddingClient": ("agent_framework_foundry", "agent-framework-foundry"),
"evaluate_foundry_target": ("agent_framework_foundry", "agent-framework-foundry"),
"evaluate_traces": ("agent_framework_foundry", "agent-framework-foundry"),
}
@@ -8,11 +8,15 @@ from agent_framework_foundry import (
FoundryAgent,
FoundryChatClient,
FoundryChatOptions,
FoundryEmbeddingClient,
FoundryEmbeddingOptions,
FoundryEmbeddingSettings,
FoundryEvals,
FoundryMemoryProvider,
RawFoundryAgent,
RawFoundryAgentChatClient,
RawFoundryChatClient,
RawFoundryEmbeddingClient,
evaluate_foundry_target,
evaluate_traces,
)
@@ -27,6 +31,9 @@ __all__ = [
"FoundryAgent",
"FoundryChatClient",
"FoundryChatOptions",
"FoundryEmbeddingClient",
"FoundryEmbeddingOptions",
"FoundryEmbeddingSettings",
"FoundryEvals",
"FoundryLocalChatOptions",
"FoundryLocalClient",
@@ -36,6 +43,7 @@ __all__ = [
"RawFoundryAgent",
"RawFoundryAgentChatClient",
"RawFoundryChatClient",
"RawFoundryEmbeddingClient",
"evaluate_foundry_target",
"evaluate_traces",
]
@@ -5,7 +5,6 @@
This module lazily re-exports objects from:
- ``agent-framework-copilotstudio``
- ``agent-framework-purview``
- ``agent-framework-foundry-local``
Supported classes:
- CopilotStudioAgent
@@ -20,9 +19,6 @@ Supported classes:
- PurviewRequestError
- PurviewServiceError
- CacheProvider
- FoundryLocalChatOptions
- FoundryLocalClient
- FoundryLocalSettings
"""
@@ -43,9 +39,6 @@ _IMPORTS: dict[str, tuple[str, str]] = {
"PurviewRequestError": ("agent_framework_purview", "agent-framework-purview"),
"PurviewServiceError": ("agent_framework_purview", "agent-framework-purview"),
"CacheProvider": ("agent_framework_purview", "agent-framework-purview"),
"FoundryLocalChatOptions": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
"FoundryLocalClient": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
"FoundryLocalSettings": ("agent_framework_foundry_local", "agent-framework-foundry-local"),
}
@@ -4,11 +4,6 @@ from agent_framework_copilotstudio import (
CopilotStudioAgent,
acquire_token,
)
from agent_framework_foundry_local import (
FoundryLocalChatOptions,
FoundryLocalClient,
FoundryLocalSettings,
)
from agent_framework_purview import (
CacheProvider,
PurviewAppLocation,
@@ -26,9 +21,6 @@ from agent_framework_purview import (
__all__ = [
"CacheProvider",
"CopilotStudioAgent",
"FoundryLocalChatOptions",
"FoundryLocalClient",
"FoundryLocalSettings",
"PurviewAppLocation",
"PurviewAuthenticationError",
"PurviewChatPolicyMiddleware",
+1 -1
View File
@@ -35,10 +35,10 @@ all = [
"agent-framework-a2a",
"agent-framework-ag-ui",
"agent-framework-azure-ai-search",
"agent-framework-azure-cosmos",
"agent-framework-anthropic",
"agent-framework-openai",
"agent-framework-claude",
"agent-framework-azure-ai",
"agent-framework-azurefunctions",
"agent-framework-bedrock",
"agent-framework-chatkit",
@@ -0,0 +1,10 @@
# Copyright (c) Microsoft. All rights reserved.
from agent_framework_azure_cosmos import CosmosHistoryProvider
import agent_framework.azure as azure
def test_azure_namespace_exposes_cosmos_history_provider() -> None:
assert azure.CosmosHistoryProvider is CosmosHistoryProvider
assert "CosmosHistoryProvider" in dir(azure)
@@ -4068,3 +4068,87 @@ def test_oauth_consent_request_serialization_roundtrip():
# endregion
# region prepend_instructions_to_messages tests
def test_prepend_instructions_basic():
"""Test that instructions are prepended as system message."""
from agent_framework._types import prepend_instructions_to_messages
messages = [Message("user", ["Hello"])]
result = prepend_instructions_to_messages(messages, "You are helpful.")
assert len(result) == 2
assert result[0].role == "system"
assert result[0].text == "You are helpful."
assert result[1].role == "user"
def test_prepend_instructions_none():
"""Test that None instructions returns messages unchanged."""
from agent_framework._types import prepend_instructions_to_messages
messages = [Message("user", ["Hello"])]
result = prepend_instructions_to_messages(messages, None)
assert result is messages
def test_prepend_instructions_skips_duplicate():
"""Test that duplicate system instructions are not prepended again."""
from agent_framework._types import prepend_instructions_to_messages
messages = [
Message("system", ["You are helpful."]),
Message("user", ["Hello"]),
]
result = prepend_instructions_to_messages(messages, "You are helpful.")
assert len(result) == 2
assert result[0].role == "system"
assert result[0].text == "You are helpful."
assert result[1].role == "user"
def test_prepend_instructions_skips_duplicate_list():
"""Test deduplication with a list of instructions."""
from agent_framework._types import prepend_instructions_to_messages
messages = [
Message("system", ["First instruction"]),
Message("system", ["Second instruction"]),
Message("user", ["Hello"]),
]
result = prepend_instructions_to_messages(messages, ["First instruction", "Second instruction"])
assert len(result) == 3
assert result[0].text == "First instruction"
assert result[1].text == "Second instruction"
assert result[2].text == "Hello"
def test_prepend_instructions_adds_when_different():
"""Test that different instructions are still prepended."""
from agent_framework._types import prepend_instructions_to_messages
messages = [
Message("system", ["Old instruction"]),
Message("user", ["Hello"]),
]
result = prepend_instructions_to_messages(messages, "New instruction")
assert len(result) == 3
assert result[0].role == "system"
assert result[0].text == "New instruction"
assert result[1].text == "Old instruction"
assert result[2].text == "Hello"
def test_prepend_instructions_custom_role():
"""Test prepending with a custom role."""
from agent_framework._types import prepend_instructions_to_messages
messages = [Message("user", ["Hello"])]
result = prepend_instructions_to_messages(messages, "Be concise.", role="developer")
assert len(result) == 2
assert result[0].role == "developer"
# endregion
@@ -1,8 +1,7 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from collections.abc import AsyncIterable, Awaitable
from typing import TYPE_CHECKING, Any, Literal, overload
from typing import Any, Literal, overload
import pytest
@@ -22,9 +21,7 @@ from agent_framework import (
)
from agent_framework._workflows._agent_executor import AgentExecutorResponse
from agent_framework._workflows._checkpoint import InMemoryCheckpointStorage
if TYPE_CHECKING:
from _pytest.logging import LogCaptureFixture
from agent_framework._workflows._const import GLOBAL_KWARGS_KEY
class _CountingAgent(BaseAgent):
@@ -309,87 +306,28 @@ async def test_agent_executor_save_and_restore_state_directly() -> None:
assert restored_session.session_id == session.session_id
async def test_agent_executor_run_with_session_kwarg_does_not_raise() -> None:
"""Passing session= via workflow.run() should not cause a duplicate-keyword TypeError (#4295)."""
agent = _CountingAgent(id="session_kwarg_agent", name="SessionKwargAgent")
executor = AgentExecutor(agent, id="session_kwarg_exec")
workflow = WorkflowBuilder(start_executor=executor).build()
async def test_prepare_agent_run_args_extracts_invocation_kwargs() -> None:
"""_prepare_agent_run_args extracts function_invocation_kwargs and client_kwargs."""
agent = _CountingAgent(id="test_agent", name="TestAgent")
executor = AgentExecutor(agent, id="test_exec")
# This previously raised: TypeError: run() got multiple values for keyword argument 'session'
result = await workflow.run("hello", session="user-supplied-value")
assert result is not None
assert agent.call_count == 1
async def test_agent_executor_run_streaming_with_stream_kwarg_does_not_raise() -> None:
"""Passing stream= via workflow.run() kwargs should not cause a duplicate-keyword TypeError."""
agent = _CountingAgent(id="stream_kwarg_agent", name="StreamKwargAgent")
executor = AgentExecutor(agent, id="stream_kwarg_exec")
workflow = WorkflowBuilder(start_executor=executor).build()
# stream=True at workflow level triggers streaming mode (returns async iterable)
events: list[WorkflowEvent] = []
async for event in workflow.run("hello", stream=True):
events.append(event)
assert len(events) > 0
assert agent.call_count == 1
@pytest.mark.parametrize("reserved_kwarg", ["session", "stream", "messages"])
async def test_prepare_agent_run_args_strips_reserved_kwargs(reserved_kwarg: str, caplog: "LogCaptureFixture") -> None:
"""_prepare_agent_run_args must remove reserved kwargs and log a warning."""
raw: dict[str, Any] = {
reserved_kwarg: "should-be-stripped",
"custom_key": "keep-me",
"function_invocation_kwargs": {"__global__": {"key": "fi_val"}},
"client_kwargs": {"__global__": {"key": "ci_val"}},
}
with caplog.at_level(logging.WARNING):
run_kwargs, options = AgentExecutor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
assert reserved_kwarg not in run_kwargs
assert "custom_key" in run_kwargs
assert options is not None
assert options["additional_function_arguments"]["custom_key"] == "keep-me"
assert any(reserved_kwarg in record.message for record in caplog.records)
fi_kwargs, ci_kwargs = executor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
assert fi_kwargs == {"key": "fi_val"}
assert ci_kwargs == {"key": "ci_val"}
async def test_prepare_agent_run_args_preserves_non_reserved_kwargs() -> None:
"""Non-reserved workflow kwargs should pass through unchanged."""
raw: dict[str, Any] = {"custom_param": "value", "another": 42}
run_kwargs, _options = AgentExecutor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
assert run_kwargs["custom_param"] == "value"
assert run_kwargs["another"] == 42
async def test_prepare_agent_run_args_returns_none_when_no_kwargs() -> None:
"""_prepare_agent_run_args returns None for both when raw dict has no invocation kwargs."""
agent = _CountingAgent(id="test_agent", name="TestAgent")
executor = AgentExecutor(agent, id="test_exec")
async def test_prepare_agent_run_args_strips_all_reserved_kwargs_at_once(
caplog: "LogCaptureFixture",
) -> None:
"""All reserved kwargs should be stripped when supplied together, each emitting a warning."""
raw: dict[str, Any] = {"session": "x", "stream": True, "messages": [], "custom": 1}
with caplog.at_level(logging.WARNING):
run_kwargs, options = AgentExecutor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
assert "session" not in run_kwargs
assert "stream" not in run_kwargs
assert "messages" not in run_kwargs
assert run_kwargs["custom"] == 1
assert options is not None
assert options["additional_function_arguments"]["custom"] == 1
warned_keys = {r.message.split("'")[1] for r in caplog.records if "reserved" in r.message.lower()}
assert warned_keys == {"session", "stream", "messages"}
async def test_agent_executor_run_with_messages_kwarg_does_not_raise() -> None:
"""Passing messages= via workflow.run() kwargs should not cause a duplicate-keyword TypeError."""
agent = _CountingAgent(id="messages_kwarg_agent", name="MessagesKwargAgent")
executor = AgentExecutor(agent, id="messages_kwarg_exec")
workflow = WorkflowBuilder(start_executor=executor).build()
result = await workflow.run("hello", messages=["stale"])
assert result is not None
assert agent.call_count == 1
fi_kwargs, ci_kwargs = executor._prepare_agent_run_args({}) # pyright: ignore[reportPrivateUsage]
assert fi_kwargs is None
assert ci_kwargs is None
class _NonCopyableRaw:
@@ -638,3 +576,126 @@ async def test_checkpoint_restore_works_without_context_mode_in_state() -> None:
assert cache[0].text == "cached msg"
# context_mode should remain as configured in the constructor, not changed by restore
assert executor._context_mode == "last_agent" # pyright: ignore[reportPrivateUsage]
# ---------------------------------------------------------------------------
# Per-executor kwargs resolution tests
# ---------------------------------------------------------------------------
async def test_resolve_executor_kwargs_returns_global_kwargs() -> None:
"""_resolve_executor_kwargs with the global kwargs key returns the global kwargs."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_a")
resolved = {GLOBAL_KWARGS_KEY: {"tool_param": "value"}}
result = executor._resolve_executor_kwargs(resolved) # pyright: ignore[reportPrivateUsage]
assert result == {"tool_param": "value"}
async def test_resolve_executor_kwargs_returns_per_executor_kwargs() -> None:
"""_resolve_executor_kwargs with matching executor ID returns that executor's kwargs."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_a")
resolved = {"exec_a": {"my_param": 42}, "exec_b": {"other_param": 99}}
result = executor._resolve_executor_kwargs(resolved) # pyright: ignore[reportPrivateUsage]
assert result == {"my_param": 42}
async def test_resolve_executor_kwargs_returns_none_for_unmatched_per_executor() -> None:
"""_resolve_executor_kwargs returns None when per-executor dict has no matching ID."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_c")
resolved = {"exec_a": {"my_param": 42}, "exec_b": {"other_param": 99}}
result = executor._resolve_executor_kwargs(resolved) # pyright: ignore[reportPrivateUsage]
assert result is None
async def test_resolve_executor_kwargs_returns_none_for_none_input() -> None:
"""_resolve_executor_kwargs returns None when input is None."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_a")
result = executor._resolve_executor_kwargs(None) # pyright: ignore[reportPrivateUsage]
assert result is None
async def test_resolve_executor_kwargs_prefers_executor_id_over_global() -> None:
"""_resolve_executor_kwargs prefers executor-specific entry over __global__."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_a")
# Dict has both a per-executor entry and a global entry
resolved = {"exec_a": {"specific": True}, GLOBAL_KWARGS_KEY: {"global": True}}
result = executor._resolve_executor_kwargs(resolved) # pyright: ignore[reportPrivateUsage]
assert result == {"specific": True}
async def test_prepare_agent_run_args_extracts_function_invocation_kwargs() -> None:
"""_prepare_agent_run_args extracts function_invocation_kwargs from the state dict."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_a")
raw: dict[str, Any] = {
"function_invocation_kwargs": {GLOBAL_KWARGS_KEY: {"tool_key": "tool_val"}},
}
fi_kwargs, client_kwargs = executor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
assert fi_kwargs == {"tool_key": "tool_val"}
assert client_kwargs is None
async def test_prepare_agent_run_args_extracts_client_kwargs() -> None:
"""_prepare_agent_run_args extracts client_kwargs from the state dict."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_a")
raw: dict[str, Any] = {
"client_kwargs": {GLOBAL_KWARGS_KEY: {"model": "gpt-4"}},
}
fi_kwargs, client_kwargs = executor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
assert fi_kwargs is None
assert client_kwargs == {"model": "gpt-4"}
async def test_prepare_agent_run_args_per_executor_resolution() -> None:
"""_prepare_agent_run_args resolves per-executor function_invocation_kwargs using self.id."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_a")
raw: dict[str, Any] = {
"function_invocation_kwargs": {
"exec_a": {"my_tool_key": "my_val"},
"exec_b": {"other_tool_key": "other_val"},
},
}
fi_kwargs, _ = executor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
assert fi_kwargs == {"my_tool_key": "my_val"}
async def test_prepare_agent_run_args_per_executor_no_match() -> None:
"""_prepare_agent_run_args returns None for function_invocation_kwargs when executor ID not found."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_c")
raw: dict[str, Any] = {
"function_invocation_kwargs": {
"exec_a": {"my_tool_key": "my_val"},
"exec_b": {"other_tool_key": "other_val"},
},
}
fi_kwargs, _ = executor._prepare_agent_run_args(raw) # pyright: ignore[reportPrivateUsage]
assert fi_kwargs is None
async def test_resolve_executor_kwargs_empty_per_executor_does_not_fallback_to_global() -> None:
"""An explicit empty per-executor dict should not fall through to global kwargs."""
agent = _CountingAgent(id="a", name="A")
executor = AgentExecutor(agent, id="exec_a")
# Per-executor entry for exec_a is empty, but global has values.
# The empty dict should be honoured (no fallback to global).
resolved = {"exec_a": {}, GLOBAL_KWARGS_KEY: {"global_key": "global_val"}}
result = executor._resolve_executor_kwargs(resolved) # pyright: ignore[reportPrivateUsage]
assert result == {}
File diff suppressed because it is too large Load Diff
@@ -481,7 +481,7 @@ services:
environment:
# OpenAI
- OPENAI_API_KEY=\${OPENAI_API_KEY}
- OPENAI_CHAT_MODEL=\${OPENAI_CHAT_MODEL:-gpt-4o-mini}
- OPENAI_CHAT_COMPLETION_MODEL=\${OPENAI_CHAT_COMPLETION_MODEL:-gpt-4o-mini}
# Or Azure OpenAI
- AZURE_OPENAI_API_KEY=\${AZURE_OPENAI_API_KEY}
- AZURE_OPENAI_ENDPOINT=\${AZURE_OPENAI_ENDPOINT}
@@ -514,7 +514,7 @@ az acr build --registry myregistry \\
--target-port 8080 \\
--ingress 'external' \\
--registry-server myregistry.azurecr.io \\
--env-vars OPENAI_API_KEY=secretref:openai-key OPENAI_CHAT_MODEL=gpt-4o-mini`})]
--env-vars OPENAI_API_KEY=secretref:openai-key OPENAI_CHAT_COMPLETION_MODEL=gpt-4o-mini`})]
}), o.jsxs("div", {
className: "border-l-2 border-primary pl-3", children: [o.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [o.jsx("div", { className: "w-5 h-5 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-bold", children: "5" }), o.jsx("h5", { className: "font-medium text-sm", children: "Get Application URL" })] }), o.jsx("pre", {
className: "bg-muted p-2 rounded text-xs overflow-x-auto border mt-2", children: `az containerapp show --name ${r.toLowerCase()}-app \\
+1 -1
View File
@@ -33,7 +33,7 @@ Then edit `.env` and add your API keys:
```bash
# For OpenAI (minimum required)
OPENAI_API_KEY="your-api-key-here"
OPENAI_CHAT_MODEL="gpt-4o-mini"
OPENAI_CHAT_COMPLETION_MODEL="gpt-4o-mini"
# Or for Azure OpenAI
AZURE_OPENAI_ENDPOINT="your-endpoint"
@@ -243,7 +243,7 @@ services:
environment:
# OpenAI
- OPENAI_API_KEY=\${OPENAI_API_KEY}
- OPENAI_CHAT_MODEL=\${OPENAI_CHAT_MODEL:-gpt-4o-mini}
- OPENAI_CHAT_COMPLETION_MODEL=\${OPENAI_CHAT_COMPLETION_MODEL:-gpt-4o-mini}
# Or Azure OpenAI
- AZURE_OPENAI_API_KEY=\${AZURE_OPENAI_API_KEY}
- AZURE_OPENAI_ENDPOINT=\${AZURE_OPENAI_ENDPOINT}
@@ -802,7 +802,7 @@ az acr build --registry myregistry \\
--target-port 8080 \\
--ingress 'external' \\
--registry-server myregistry.azurecr.io \\
--env-vars OPENAI_API_KEY=secretref:openai-key OPENAI_CHAT_MODEL=gpt-4o-mini`}
--env-vars OPENAI_API_KEY=secretref:openai-key OPENAI_CHAT_COMPLETION_MODEL=gpt-4o-mini`}
</pre>
</div>
+1 -1
View File
@@ -1,3 +1,3 @@
# Agent Framework Foundry
This package contains the cloud Azure AI Foundry integrations for Microsoft Agent Framework, including Foundry chat clients, preconfigured Foundry agents, and Foundry memory providers.
This package contains the Microsoft Foundry integrations for Microsoft Agent Framework, including Foundry chat clients, preconfigured Foundry agents, Foundry embedding clients, and Foundry memory providers.
@@ -4,6 +4,12 @@ import importlib.metadata
from ._agent import FoundryAgent, RawFoundryAgent, RawFoundryAgentChatClient
from ._chat_client import FoundryChatClient, FoundryChatOptions, RawFoundryChatClient
from ._embedding_client import (
FoundryEmbeddingClient,
FoundryEmbeddingOptions,
FoundryEmbeddingSettings,
RawFoundryEmbeddingClient,
)
from ._foundry_evals import (
FoundryEvals,
evaluate_foundry_target,
@@ -20,11 +26,15 @@ __all__ = [
"FoundryAgent",
"FoundryChatClient",
"FoundryChatOptions",
"FoundryEmbeddingClient",
"FoundryEmbeddingOptions",
"FoundryEmbeddingSettings",
"FoundryEvals",
"FoundryMemoryProvider",
"RawFoundryAgent",
"RawFoundryAgentChatClient",
"RawFoundryChatClient",
"RawFoundryEmbeddingClient",
"__version__",
"evaluate_foundry_target",
"evaluate_traces",
@@ -28,22 +28,22 @@ else:
from typing_extensions import TypeVar # type: ignore # pragma: no cover
logger = logging.getLogger("agent_framework.azure_ai")
logger = logging.getLogger("agent_framework.foundry")
_IMAGE_MEDIA_PREFIXES = ("image/",)
class AzureAIInferenceEmbeddingOptions(EmbeddingGenerationOptions, total=False):
"""Azure AI Inference-specific embedding options.
class FoundryEmbeddingOptions(EmbeddingGenerationOptions, total=False):
"""Foundry inference-specific embedding options.
Extends EmbeddingGenerationOptions with Azure AI Inference-specific fields.
Extends ``EmbeddingGenerationOptions`` with Foundry inference-specific fields.
Examples:
.. code-block:: python
from agent_framework_azure_ai import AzureAIInferenceEmbeddingOptions
from agent_framework_foundry import FoundryEmbeddingOptions
options: AzureAIInferenceEmbeddingOptions = {
options: FoundryEmbeddingOptions = {
"model": "text-embedding-3-small",
"dimensions": 1536,
"input_type": "document",
@@ -68,28 +68,28 @@ class AzureAIInferenceEmbeddingOptions(EmbeddingGenerationOptions, total=False):
"""Additional model-specific parameters passed directly to the API."""
AzureAIInferenceEmbeddingOptionsT = TypeVar(
"AzureAIInferenceEmbeddingOptionsT",
FoundryEmbeddingOptionsT = TypeVar(
"FoundryEmbeddingOptionsT",
bound=TypedDict, # type: ignore[valid-type]
default="AzureAIInferenceEmbeddingOptions",
default="FoundryEmbeddingOptions",
covariant=True,
)
class AzureAIInferenceEmbeddingSettings(TypedDict, total=False):
"""Azure AI Inference embedding settings."""
class FoundryEmbeddingSettings(TypedDict, total=False):
"""Foundry inference embedding settings."""
endpoint: str | None
api_key: str | None
models_endpoint: str | None
models_api_key: str | None
embedding_model: str | None
image_embedding_model: str | None
class RawAzureAIInferenceEmbeddingClient(
BaseEmbeddingClient[Content | str, list[float], AzureAIInferenceEmbeddingOptionsT],
Generic[AzureAIInferenceEmbeddingOptionsT],
class RawFoundryEmbeddingClient(
BaseEmbeddingClient[Content | str, list[float], FoundryEmbeddingOptionsT],
Generic[FoundryEmbeddingOptionsT],
):
"""Raw Azure AI Inference embedding client without telemetry.
"""Raw Foundry 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
@@ -98,14 +98,14 @@ class RawAzureAIInferenceEmbeddingClient(
Keyword Args:
model: The text embedding model (e.g. "text-embedding-3-small").
Can also be set via environment variable AZURE_AI_INFERENCE_EMBEDDING_MODEL.
Can also be set via environment variable FOUNDRY_EMBEDDING_MODEL.
image_model: The image embedding model (e.g. "Cohere-embed-v3-english").
Can also be set via environment variable AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL.
Can also be set via environment variable FOUNDRY_IMAGE_EMBEDDING_MODEL.
Falls back to ``model`` if not provided.
endpoint: The Azure AI Inference endpoint URL.
Can also be set via environment variable AZURE_AI_INFERENCE_ENDPOINT.
endpoint: The Foundry inference endpoint URL.
Can also be set via environment variable FOUNDRY_MODELS_ENDPOINT.
api_key: API key for authentication.
Can also be set via environment variable AZURE_AI_INFERENCE_API_KEY.
Can also be set via environment variable FOUNDRY_MODELS_API_KEY.
text_client: Optional pre-configured ``EmbeddingsClient``.
image_client: Optional pre-configured ``ImageEmbeddingsClient``.
credential: Optional ``AzureKeyCredential`` or token credential. If not provided,
@@ -128,13 +128,13 @@ class RawAzureAIInferenceEmbeddingClient(
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize a raw Azure AI Inference embedding client."""
"""Initialize a raw Foundry embedding client."""
settings = load_settings(
AzureAIInferenceEmbeddingSettings,
env_prefix="AZURE_AI_INFERENCE_",
required_fields=["endpoint", "embedding_model"],
endpoint=endpoint,
api_key=api_key,
FoundryEmbeddingSettings,
env_prefix="FOUNDRY_",
required_fields=["models_endpoint", "embedding_model"],
models_endpoint=endpoint,
models_api_key=api_key,
embedding_model=model,
image_embedding_model=image_model,
env_file_path=env_file_path,
@@ -143,10 +143,10 @@ class RawAzureAIInferenceEmbeddingClient(
self.model = settings["embedding_model"] # type: ignore[reportTypedDictNotRequiredAccess]
self.image_model: str = settings.get("image_embedding_model") or self.model # type: ignore[assignment]
resolved_endpoint = settings["endpoint"] # type: ignore[reportTypedDictNotRequiredAccess]
resolved_endpoint = settings["models_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 settings.get("models_api_key"):
credential = AzureKeyCredential(settings["models_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.")
@@ -169,7 +169,7 @@ class RawAzureAIInferenceEmbeddingClient(
with suppress(Exception):
await self._image_client.close()
async def __aenter__(self) -> RawAzureAIInferenceEmbeddingClient[AzureAIInferenceEmbeddingOptionsT]:
async def __aenter__(self) -> RawFoundryEmbeddingClient[FoundryEmbeddingOptionsT]:
"""Enter the async context manager."""
return self
@@ -185,8 +185,8 @@ class RawAzureAIInferenceEmbeddingClient(
self,
values: Sequence[Content | str],
*,
options: AzureAIInferenceEmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[list[float], AzureAIInferenceEmbeddingOptionsT]:
options: FoundryEmbeddingOptionsT | None = None,
) -> GeneratedEmbeddings[list[float], FoundryEmbeddingOptionsT]:
"""Generate embeddings for text and/or image inputs.
Text inputs (``str`` or ``Content`` with ``type="text"``) are sent to the
@@ -310,12 +310,12 @@ class RawAzureAIInferenceEmbeddingClient(
) # type: ignore[reportReturnType]
class AzureAIInferenceEmbeddingClient(
EmbeddingTelemetryLayer[Content | str, list[float], AzureAIInferenceEmbeddingOptionsT],
RawAzureAIInferenceEmbeddingClient[AzureAIInferenceEmbeddingOptionsT],
Generic[AzureAIInferenceEmbeddingOptionsT],
class FoundryEmbeddingClient(
EmbeddingTelemetryLayer[Content | str, list[float], FoundryEmbeddingOptionsT],
RawFoundryEmbeddingClient[FoundryEmbeddingOptionsT],
Generic[FoundryEmbeddingOptionsT],
):
"""Azure AI Inference embedding client with telemetry support.
"""Foundry 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
@@ -323,14 +323,14 @@ class AzureAIInferenceEmbeddingClient(
Keyword Args:
model: The text embedding model (e.g. "text-embedding-3-small").
Can also be set via environment variable AZURE_AI_INFERENCE_EMBEDDING_MODEL.
Can also be set via environment variable FOUNDRY_EMBEDDING_MODEL.
image_model: The image embedding model
(e.g. "Cohere-embed-v3-english"). Can also be set via environment variable
AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL. Falls back to ``model``.
endpoint: The Azure AI Inference endpoint URL.
Can also be set via environment variable AZURE_AI_INFERENCE_ENDPOINT.
FOUNDRY_IMAGE_EMBEDDING_MODEL. Falls back to ``model``.
endpoint: The Foundry inference endpoint URL.
Can also be set via environment variable FOUNDRY_MODELS_ENDPOINT.
api_key: API key for authentication.
Can also be set via environment variable AZURE_AI_INFERENCE_API_KEY.
Can also be set via environment variable FOUNDRY_MODELS_API_KEY.
text_client: Optional pre-configured ``EmbeddingsClient``.
image_client: Optional pre-configured ``ImageEmbeddingsClient``.
credential: Optional ``AzureKeyCredential`` or token credential.
@@ -341,14 +341,14 @@ class AzureAIInferenceEmbeddingClient(
Examples:
.. code-block:: python
from agent_framework_azure_ai import AzureAIInferenceEmbeddingClient
from agent_framework_foundry import FoundryEmbeddingClient
# 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=text-embedding-3-small
# Set AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL=Cohere-embed-v3-english
client = AzureAIInferenceEmbeddingClient()
# Set FOUNDRY_MODELS_ENDPOINT=https://your-endpoint.inference.ai.azure.com
# Set FOUNDRY_MODELS_API_KEY=your-key
# Set FOUNDRY_EMBEDDING_MODEL=text-embedding-3-small
# Set FOUNDRY_IMAGE_EMBEDDING_MODEL=Cohere-embed-v3-english
client = FoundryEmbeddingClient()
# Text embeddings
result = await client.get_embeddings(["Hello, world!"])
@@ -380,7 +380,7 @@ class AzureAIInferenceEmbeddingClient(
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an Azure AI Inference embedding client."""
"""Initialize a Foundry embedding client."""
super().__init__(
model=model,
image_model=image_model,
@@ -40,6 +40,7 @@ from agent_framework._evaluation import (
EvalResults,
EvalScoreResult,
)
from agent_framework._feature_stage import ExperimentalFeature, experimental
from openai import AsyncOpenAI
from ._chat_client import FoundryChatClient
@@ -491,6 +492,7 @@ async def _evaluate_via_responses_impl(
# ---------------------------------------------------------------------------
@experimental(feature_id=ExperimentalFeature.EVALS)
class FoundryEvals:
"""Evaluation provider backed by Microsoft Foundry.
@@ -727,6 +729,7 @@ class FoundryEvals:
# ---------------------------------------------------------------------------
@experimental(feature_id=ExperimentalFeature.EVALS)
async def evaluate_traces(
*,
evaluators: Sequence[str] | None = None,
@@ -817,6 +820,7 @@ async def evaluate_traces(
return await _poll_eval_run(oai_client, eval_obj.id, run.id, poll_interval, timeout)
@experimental(feature_id=ExperimentalFeature.EVALS)
async def evaluate_foundry_target(
*,
target: dict[str, Any],
+1
View File
@@ -25,6 +25,7 @@ classifiers = [
dependencies = [
"agent-framework-core>=1.0.0rc6",
"agent-framework-openai>=1.0.0rc6",
"azure-ai-inference>=1.0.0b9,<1.0.0b10",
"azure-ai-projects>=2.0.0,<3.0",
]
@@ -10,10 +10,10 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from agent_framework import Content
from agent_framework_azure_ai import (
AzureAIInferenceEmbeddingClient,
AzureAIInferenceEmbeddingOptions,
RawAzureAIInferenceEmbeddingClient,
from agent_framework_foundry import (
FoundryEmbeddingClient,
FoundryEmbeddingOptions,
RawFoundryEmbeddingClient,
)
@@ -57,9 +57,9 @@ def mock_image_client() -> AsyncMock:
@pytest.fixture
def raw_client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> RawAzureAIInferenceEmbeddingClient[Any]:
"""Create a RawAzureAIInferenceEmbeddingClient with mocked SDK clients."""
return RawAzureAIInferenceEmbeddingClient(
def raw_client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> RawFoundryEmbeddingClient[Any]:
"""Create a RawFoundryEmbeddingClient with mocked SDK clients."""
return RawFoundryEmbeddingClient(
model="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
@@ -69,9 +69,9 @@ def raw_client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> Raw
@pytest.fixture
def client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> AzureAIInferenceEmbeddingClient[Any]:
"""Create an AzureAIInferenceEmbeddingClient with mocked SDK clients."""
return AzureAIInferenceEmbeddingClient(
def client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> FoundryEmbeddingClient[Any]:
"""Create a FoundryEmbeddingClient with mocked SDK clients."""
return FoundryEmbeddingClient(
model="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
@@ -80,11 +80,11 @@ def client(mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> AzureAI
)
class TestRawAzureAIInferenceEmbeddingClient:
"""Tests for the raw Azure AI Inference embedding client."""
class TestRawFoundryEmbeddingClient:
"""Tests for the raw Foundry embedding client."""
async def test_text_embeddings(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
self, raw_client: RawFoundryEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""Text inputs are dispatched to the text client."""
result = await raw_client.get_embeddings(["hello", "world"])
@@ -94,7 +94,7 @@ class TestRawAzureAIInferenceEmbeddingClient:
assert call_kwargs.kwargs["model"] == "test-model"
async def test_text_content_embeddings(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
self, raw_client: RawFoundryEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""Content.from_text() inputs are dispatched to the text client."""
text_content = Content.from_text("hello")
@@ -105,7 +105,7 @@ class TestRawAzureAIInferenceEmbeddingClient:
assert call_kwargs.kwargs["input"] == ["hello"]
async def test_image_content_embeddings(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_image_client: AsyncMock
self, raw_client: RawFoundryEmbeddingClient[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")
@@ -119,7 +119,7 @@ class TestRawAzureAIInferenceEmbeddingClient:
async def test_mixed_text_and_image(
self,
raw_client: RawAzureAIInferenceEmbeddingClient[Any],
raw_client: RawFoundryEmbeddingClient[Any],
mock_text_client: AsyncMock,
mock_image_client: AsyncMock,
) -> None:
@@ -138,16 +138,16 @@ class TestRawAzureAIInferenceEmbeddingClient:
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:
async def test_empty_input(self, raw_client: RawFoundryEmbeddingClient[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
self, raw_client: RawFoundryEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""Options are passed through to the SDK."""
options: AzureAIInferenceEmbeddingOptions = {
options: FoundryEmbeddingOptions = {
"dimensions": 512,
"input_type": "document",
"encoding_format": "float",
@@ -160,23 +160,23 @@ class TestRawAzureAIInferenceEmbeddingClient:
assert call_kwargs.kwargs["encoding_format"] == "float"
async def test_model_override_in_options(
self, raw_client: RawAzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
self, raw_client: RawFoundryEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
"""model in options overrides the default."""
options: AzureAIInferenceEmbeddingOptions = {"model": "custom-model"}
options: FoundryEmbeddingOptions = {"model": "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:
async def test_unsupported_content_type_raises(self, raw_client: RawFoundryEmbeddingClient[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
self, raw_client: RawFoundryEmbeddingClient[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)
@@ -184,7 +184,7 @@ class TestRawAzureAIInferenceEmbeddingClient:
assert result.usage is not None
assert result.usage["input_token_count"] == 42
def test_service_url(self, raw_client: RawAzureAIInferenceEmbeddingClient[Any]) -> None:
def test_service_url(self, raw_client: RawFoundryEmbeddingClient[Any]) -> None:
"""service_url returns the configured endpoint."""
assert raw_client.service_url() == "https://test.inference.ai.azure.com"
@@ -194,15 +194,15 @@ class TestRawAzureAIInferenceEmbeddingClient:
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": "env-model",
"FOUNDRY_MODELS_ENDPOINT": "https://env.inference.ai.azure.com",
"FOUNDRY_MODELS_API_KEY": "env-key",
"FOUNDRY_EMBEDDING_MODEL": "env-model",
},
),
patch("agent_framework_azure_ai._embedding_client.EmbeddingsClient"),
patch("agent_framework_azure_ai._embedding_client.ImageEmbeddingsClient"),
patch("agent_framework_foundry._embedding_client.EmbeddingsClient"),
patch("agent_framework_foundry._embedding_client.ImageEmbeddingsClient"),
):
client = RawAzureAIInferenceEmbeddingClient()
client = RawFoundryEmbeddingClient()
assert client.model == "env-model"
assert client.image_model == "env-model" # falls back to model
@@ -212,22 +212,22 @@ class TestRawAzureAIInferenceEmbeddingClient:
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": "text-model",
"AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL": "image-model",
"FOUNDRY_MODELS_ENDPOINT": "https://env.inference.ai.azure.com",
"FOUNDRY_MODELS_API_KEY": "env-key",
"FOUNDRY_EMBEDDING_MODEL": "text-model",
"FOUNDRY_IMAGE_EMBEDDING_MODEL": "image-model",
},
),
patch("agent_framework_azure_ai._embedding_client.EmbeddingsClient"),
patch("agent_framework_azure_ai._embedding_client.ImageEmbeddingsClient"),
patch("agent_framework_foundry._embedding_client.EmbeddingsClient"),
patch("agent_framework_foundry._embedding_client.ImageEmbeddingsClient"),
):
client = RawAzureAIInferenceEmbeddingClient()
client = RawFoundryEmbeddingClient()
assert client.model == "text-model"
assert client.image_model == "image-model"
def test_image_model_explicit(self, mock_text_client: AsyncMock, mock_image_client: AsyncMock) -> None:
"""image_model can be set explicitly."""
client = RawAzureAIInferenceEmbeddingClient(
client = RawFoundryEmbeddingClient(
model="text-model",
image_model="image-model",
endpoint="https://test.inference.ai.azure.com",
@@ -242,7 +242,7 @@ class TestRawAzureAIInferenceEmbeddingClient:
self, mock_text_client: AsyncMock, mock_image_client: AsyncMock
) -> None:
"""image_model is passed to the image client embed call."""
client = RawAzureAIInferenceEmbeddingClient(
client = RawFoundryEmbeddingClient(
model="text-model",
image_model="image-model",
endpoint="https://test.inference.ai.azure.com",
@@ -256,12 +256,10 @@ class TestRawAzureAIInferenceEmbeddingClient:
assert call_kwargs.kwargs["model"] == "image-model"
class TestAzureAIInferenceEmbeddingClient:
"""Tests for the telemetry-enabled Azure AI Inference embedding client."""
class TestFoundryEmbeddingClient:
"""Tests for the telemetry-enabled Foundry embedding client."""
async def test_text_embeddings(
self, client: AzureAIInferenceEmbeddingClient[Any], mock_text_client: AsyncMock
) -> None:
async def test_text_embeddings(self, client: FoundryEmbeddingClient[Any], mock_text_client: AsyncMock) -> None:
"""Text embeddings work through the telemetry layer."""
result = await client.get_embeddings(["hello"])
assert len(result) == 1
@@ -269,11 +267,11 @@ class TestAzureAIInferenceEmbeddingClient:
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"
assert FoundryEmbeddingClient.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(
client = FoundryEmbeddingClient(
model="test-model",
endpoint="https://test.inference.ai.azure.com",
api_key="test-key",
@@ -284,32 +282,32 @@ class TestAzureAIInferenceEmbeddingClient:
assert client.otel_provider_name == "custom-provider"
_SKIP_REASON = "Azure AI Inference integration tests disabled"
_SKIP_REASON = "Foundry inference integration tests disabled"
def _integration_tests_enabled() -> bool:
def _foundry_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")
os.environ.get("FOUNDRY_MODELS_ENDPOINT")
and os.environ.get("FOUNDRY_MODELS_API_KEY")
and os.environ.get("FOUNDRY_EMBEDDING_MODEL")
)
skip_if_azure_ai_inference_integration_tests_disabled = pytest.mark.skipif(
not _integration_tests_enabled(),
skip_if_foundry_inference_integration_tests_disabled = pytest.mark.skipif(
not _foundry_integration_tests_enabled(),
reason=_SKIP_REASON,
)
class TestAzureAIInferenceEmbeddingIntegration:
"""Integration tests requiring a live Azure AI Inference endpoint."""
class TestFoundryEmbeddingIntegration:
"""Integration tests requiring a live Foundry inference endpoint."""
@pytest.mark.flaky
@pytest.mark.integration
@skip_if_azure_ai_inference_integration_tests_disabled
@skip_if_foundry_inference_integration_tests_disabled
async def test_text_embedding_live(self) -> None:
"""Generate text embeddings against a live endpoint."""
client = AzureAIInferenceEmbeddingClient()
client = FoundryEmbeddingClient()
result = await client.get_embeddings(["Hello, world!"])
assert len(result) == 1
assert len(result[0].vector) > 0
@@ -17,8 +17,10 @@ from agent_framework import (
BaseAgent,
Content,
ContextProvider,
HistoryProvider,
Message,
ResponseStream,
SessionContext,
normalize_messages,
)
from agent_framework._settings import load_settings
@@ -352,13 +354,25 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
AgentException: If the request fails.
"""
if stream:
ctx_holder: dict[str, Any] = {}
async def _after_run_hook(response: AgentResponse) -> None:
session_context = ctx_holder.get("session_context")
sess = ctx_holder.get("session")
if session_context is not None and sess is not None:
session_context._response = response
try:
await self._run_after_providers(session=sess, context=session_context)
except Exception:
logger.exception("Error running after_run providers in streaming result hook")
def _finalize(updates: Sequence[AgentResponseUpdate]) -> AgentResponse:
return AgentResponse.from_updates(updates)
return ResponseStream(
self._stream_updates(messages=messages, session=session, options=options),
self._stream_updates(messages=messages, session=session, options=options, _ctx_holder=ctx_holder),
finalizer=_finalize,
result_hooks=[_after_run_hook],
)
return self._run_impl(messages=messages, session=session, options=options)
@@ -377,11 +391,22 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
session = self.create_session()
opts: dict[str, Any] = dict(options) if options else {}
timeout = opts.pop("timeout", None) or self._settings.get("timeout") or DEFAULT_TIMEOUT_SECONDS
timeout = opts.get("timeout") or self._settings.get("timeout") or DEFAULT_TIMEOUT_SECONDS
copilot_session = await self._get_or_create_session(session, streaming=False, runtime_options=opts)
input_messages = normalize_messages(messages)
prompt = "\n".join([message.text for message in input_messages])
session_context = await self._run_before_providers(session=session, input_messages=input_messages, options=opts)
# NOTE: session is created after providers run so that future provider-contributed
# tools/config could be folded into runtime_options before session creation.
copilot_session = await self._get_or_create_session(session, streaming=False, runtime_options=opts)
# Build the prompt from the full set of messages in the session context,
# so that any context/history provider-injected messages are included.
context_messages = session_context.get_messages(include_input=True)
prompt = "\n".join([message.text for message in context_messages])
if session_context.instructions:
prompt = "\n".join(session_context.instructions) + "\n" + prompt
message_options = cast(MessageOptions, {"prompt": prompt})
try:
@@ -408,7 +433,10 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
)
response_id = message_id
return AgentResponse(messages=response_messages, response_id=response_id)
response = AgentResponse(messages=response_messages, response_id=response_id)
session_context._response = response # type: ignore[assignment]
await self._run_after_providers(session=session, context=session_context)
return response
async def _stream_updates(
self,
@@ -416,6 +444,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
*,
session: AgentSession | None = None,
options: OptionsT | None = None,
_ctx_holder: dict[str, Any] | None = None,
) -> AsyncIterable[AgentResponseUpdate]:
"""Internal method to stream updates from GitHub Copilot.
@@ -425,6 +454,9 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
Keyword Args:
session: The conversation session associated with the message(s).
options: Runtime options (model, timeout, etc.).
_ctx_holder: Internal dict populated with session_context and session
so that the caller (via a ResponseStream result_hook) can run
after_run providers without duplicating the updates buffer.
Yields:
AgentResponseUpdate items.
@@ -440,9 +472,23 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
opts: dict[str, Any] = dict(options) if options else {}
copilot_session = await self._get_or_create_session(session, streaming=True, runtime_options=opts)
input_messages = normalize_messages(messages)
prompt = "\n".join([message.text for message in input_messages])
session_context = await self._run_before_providers(session=session, input_messages=input_messages, options=opts)
# NOTE: session is created after providers run so that future provider-contributed
# tools/config could be folded into runtime_options before session creation.
copilot_session = await self._get_or_create_session(session, streaming=True, runtime_options=opts)
if _ctx_holder is not None:
_ctx_holder["session_context"] = session_context
_ctx_holder["session"] = session
# Build the prompt from the full session context so provider-injected messages are included.
context_messages = session_context.get_messages(include_input=True)
prompt = "\n".join([message.text for message in context_messages])
if session_context.instructions:
prompt = "\n".join(session_context.instructions) + "\n" + prompt
message_options = cast(MessageOptions, {"prompt": prompt})
queue: asyncio.Queue[AgentResponseUpdate | Exception | None] = asyncio.Queue()
@@ -513,6 +559,46 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
finally:
unsubscribe()
async def _run_before_providers(
self,
*,
session: AgentSession,
input_messages: list[Message],
options: dict[str, Any],
) -> SessionContext:
"""Run before_run on all context providers and return the session context.
Creates a SessionContext and invokes ``before_run`` on each provider in
forward order. ``HistoryProvider`` instances with
``load_messages=False`` are skipped.
Keyword Args:
session: The conversation session.
input_messages: The normalized input messages.
options: Runtime options dict.
Returns:
The SessionContext with provider context populated.
"""
session_context = SessionContext(
session_id=session.session_id,
service_session_id=session.service_session_id,
input_messages=input_messages,
options=options,
)
for provider in self.context_providers:
if isinstance(provider, HistoryProvider) and not provider.load_messages:
continue
await provider.before_run(
agent=self, # type: ignore[arg-type]
session=session,
context=session_context,
state=session.state.setdefault(provider.source_id, {}),
)
return session_context
@staticmethod
def _prepare_system_message(
instructions: str | None,
@@ -17,6 +17,8 @@ from agent_framework import (
AgentResponseUpdate,
AgentSession,
Content,
ContextProvider,
HistoryProvider,
Message,
)
from agent_framework.exceptions import AgentException
@@ -1367,3 +1369,532 @@ class TestGitHubCopilotAgentPermissions:
call_args = mock_client.create_session.call_args
config = call_args[0][0]
assert "on_permission_request" not in config
class SpyContextProvider(ContextProvider):
"""A context provider that records whether its hooks are called."""
def __init__(self) -> None:
super().__init__(source_id="spy-provider")
self.before_run_called = False
self.after_run_called = False
self.before_run_context: Any = None
self.after_run_context: Any = None
async def before_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
self.before_run_called = True
self.before_run_context = context
context.instructions.append("Injected by spy provider")
async def after_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
self.after_run_called = True
self.after_run_context = context
class TestGitHubCopilotAgentContextProviders:
"""Test cases for context provider integration."""
async def test_before_run_called_on_run(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that before_run is called on context providers during run()."""
mock_session.send_and_wait.return_value = assistant_message_event
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
await agent.run("Hello", session=session)
assert spy.before_run_called
async def test_after_run_called_on_run(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that after_run is called on context providers after run()."""
mock_session.send_and_wait.return_value = assistant_message_event
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
await agent.run("Hello", session=session)
assert spy.after_run_called
async def test_provider_instructions_included_in_prompt(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that instructions added by context providers are included in the prompt."""
mock_session.send_and_wait.return_value = assistant_message_event
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
await agent.run("Hello", session=session)
sent_prompt = mock_session.send_and_wait.call_args[0][0]["prompt"]
assert "Injected by spy provider" in sent_prompt
async def test_after_run_receives_response(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that after_run context contains the agent response."""
mock_session.send_and_wait.return_value = assistant_message_event
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
await agent.run("Hello", session=session)
assert spy.after_run_context is not None
assert spy.after_run_context.response is not None
async def test_before_run_called_on_streaming(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_delta_event: SessionEvent,
session_idle_event: SessionEvent,
) -> None:
"""Test that before_run is called on context providers during streaming."""
events = [assistant_delta_event, session_idle_event]
def mock_on(handler: Any) -> Any:
for event in events:
handler(event)
return lambda: None
mock_session.on = mock_on
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
async for _ in agent.run("Hello", stream=True, session=session):
pass
assert spy.before_run_called
async def test_after_run_called_on_streaming(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_delta_event: SessionEvent,
session_idle_event: SessionEvent,
) -> None:
"""Test that after_run is called on context providers after streaming."""
events = [assistant_delta_event, session_idle_event]
def mock_on(handler: Any) -> Any:
for event in events:
handler(event)
return lambda: None
mock_session.on = mock_on
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
async for _ in agent.run("Hello", stream=True, session=session):
pass
assert spy.after_run_called
async def test_provider_instructions_included_in_streaming_prompt(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_delta_event: SessionEvent,
session_idle_event: SessionEvent,
) -> None:
"""Test that instructions from context providers are included in the streaming prompt."""
events = [assistant_delta_event, session_idle_event]
def mock_on(handler: Any) -> Any:
for event in events:
handler(event)
return lambda: None
mock_session.on = mock_on
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
async for _ in agent.run("Hello", stream=True, session=session):
pass
sent_prompt = mock_session.send.call_args[0][0]["prompt"]
assert "Injected by spy provider" in sent_prompt
async def test_context_preserved_across_runs(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that provider state is preserved across multiple runs with the same session."""
mock_session.send_and_wait.return_value = assistant_message_event
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
await agent.run("Hello", session=session)
assert spy.before_run_called
spy.before_run_called = False
await agent.run("Hello again", session=session)
assert spy.before_run_called
async def test_context_messages_included_in_prompt(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that context messages added by providers via extend_messages are included in the prompt."""
mock_session.send_and_wait.return_value = assistant_message_event
class MessageInjectingProvider(ContextProvider):
def __init__(self) -> None:
super().__init__(source_id="msg-injector")
async def before_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
context.extend_messages(self, [Message(role="user", contents=[Content.from_text("History message")])])
async def after_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
pass
provider = MessageInjectingProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[provider])
session = agent.create_session()
await agent.run("Hello", session=session)
sent_prompt = mock_session.send_and_wait.call_args[0][0]["prompt"]
assert "History message" in sent_prompt
assert "Hello" in sent_prompt
async def test_context_messages_included_in_streaming_prompt(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_delta_event: SessionEvent,
session_idle_event: SessionEvent,
) -> None:
"""Test that context messages added by providers are included in the streaming prompt."""
events = [assistant_delta_event, session_idle_event]
def mock_on(handler: Any) -> Any:
for event in events:
handler(event)
return lambda: None
mock_session.on = mock_on
class MessageInjectingProvider(ContextProvider):
def __init__(self) -> None:
super().__init__(source_id="msg-injector")
async def before_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
context.extend_messages(self, [Message(role="user", contents=[Content.from_text("History message")])])
async def after_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
pass
provider = MessageInjectingProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[provider])
session = agent.create_session()
async for _ in agent.run("Hello", stream=True, session=session):
pass
sent_prompt = mock_session.send.call_args[0][0]["prompt"]
assert "History message" in sent_prompt
assert "Hello" in sent_prompt
async def test_after_run_not_called_on_error(
self,
mock_client: MagicMock,
mock_session: MagicMock,
) -> None:
"""Test that after_run is NOT called when send_and_wait raises."""
mock_session.send_and_wait.side_effect = Exception("Request failed")
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
with pytest.raises(AgentException):
await agent.run("Hello", session=session)
assert spy.before_run_called
assert not spy.after_run_called
async def test_after_run_not_called_on_streaming_error(
self,
mock_client: MagicMock,
mock_session: MagicMock,
session_error_event: SessionEvent,
) -> None:
"""Test that after_run is NOT called when streaming encounters an error."""
events = [session_error_event]
def mock_on(handler: Any) -> Any:
for event in events:
handler(event)
return lambda: None
mock_session.on = mock_on
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
with pytest.raises(AgentException):
async for _ in agent.run("Hello", stream=True, session=session):
pass
assert spy.before_run_called
assert not spy.after_run_called
async def test_multiple_providers_ordering(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that before_run is called in forward order and after_run in reverse order."""
mock_session.send_and_wait.return_value = assistant_message_event
call_order: list[str] = []
class OrderedProvider(ContextProvider):
def __init__(self, name: str) -> None:
super().__init__(source_id=name)
self.name = name
async def before_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
call_order.append(f"before:{self.name}")
async def after_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
call_order.append(f"after:{self.name}")
providers = [OrderedProvider("A"), OrderedProvider("B"), OrderedProvider("C")]
agent = GitHubCopilotAgent(client=mock_client, context_providers=providers)
session = agent.create_session()
await agent.run("Hello", session=session)
assert call_order == ["before:A", "before:B", "before:C", "after:C", "after:B", "after:A"]
async def test_history_provider_skip_when_load_messages_false(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that HistoryProvider with load_messages=False is skipped in before_run."""
mock_session.send_and_wait.return_value = assistant_message_event
class StubHistoryProvider(HistoryProvider):
def __init__(self, *, load_messages: bool = True) -> None:
super().__init__(source_id="stub-history", load_messages=load_messages)
self.before_run_called = False
async def before_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
self.before_run_called = True
async def after_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
self.after_run_called = True
async def get_messages(self, *, session_id: str, **kwargs: Any) -> list[Message]:
return []
async def save_messages(self, *, session_id: str, messages: list[Message], **kwargs: Any) -> None:
pass
skipped_provider = StubHistoryProvider(load_messages=False)
active_provider = StubHistoryProvider(load_messages=True)
# Use unique source_ids
skipped_provider._source_id = "skipped-history"
active_provider._source_id = "active-history"
agent = GitHubCopilotAgent(client=mock_client, context_providers=[skipped_provider, active_provider])
session = agent.create_session()
await agent.run("Hello", session=session)
assert not skipped_provider.before_run_called
assert active_provider.before_run_called
# after_run should still be called even when load_messages=False
assert skipped_provider.after_run_called
assert active_provider.after_run_called
async def test_streaming_after_run_response_has_updates(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_delta_event: SessionEvent,
session_idle_event: SessionEvent,
) -> None:
"""Test that streaming after_run context.response contains the aggregated updates."""
events = [assistant_delta_event, session_idle_event]
def mock_on(handler: Any) -> Any:
for event in events:
handler(event)
return lambda: None
mock_session.on = mock_on
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
async for _ in agent.run("Hello", stream=True, session=session):
pass
assert spy.after_run_context is not None
assert spy.after_run_context.response is not None
assert len(spy.after_run_context.response.messages) > 0
assert spy.after_run_context.response.messages[0].text == "Hello"
async def test_streaming_after_run_sets_empty_response_on_no_updates(
self,
mock_client: MagicMock,
mock_session: MagicMock,
session_idle_event: SessionEvent,
) -> None:
"""Test that streaming after_run sets an empty response when no updates are yielded."""
events = [session_idle_event]
def mock_on(handler: Any) -> Any:
for event in events:
handler(event)
return lambda: None
mock_session.on = mock_on
spy = SpyContextProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[spy])
session = agent.create_session()
async for _ in agent.run("Hello", stream=True, session=session):
pass
assert spy.after_run_called
assert spy.after_run_context.response is not None
assert len(spy.after_run_context.response.messages) == 0
async def test_timeout_preserved_in_session_context_options(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Test that timeout is preserved in session context options for providers."""
mock_session.send_and_wait.return_value = assistant_message_event
observed_options: dict[str, Any] = {}
class OptionsObserverProvider(ContextProvider):
def __init__(self) -> None:
super().__init__(source_id="options-observer")
async def before_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
observed_options.update(context.options)
async def after_run(
self,
*,
agent: Any,
session: AgentSession,
context: Any,
state: dict[str, Any],
) -> None:
pass
provider = OptionsObserverProvider()
agent = GitHubCopilotAgent(client=mock_client, context_providers=[provider])
session = agent.create_session()
await agent.run("Hello", session=session, options={"timeout": 120})
assert observed_options.get("timeout") == 120
@@ -7,7 +7,7 @@ configured for GAIA benchmark tasks using the OpenAI Responses API.
Required Environment Variables:
OPENAI_API_KEY: Your OpenAI API key
OPENAI_RESPONSES_MODEL: Model to use with Responses API (e.g., gpt-4o, gpt-4o-mini)
OPENAI_CHAT_MODEL: Model to use with Responses API (e.g., gpt-4o, gpt-4o-mini)
Optional Environment Variables:
OPENAI_BASE_URL: Custom API base URL if using a proxy or compatible service
@@ -19,7 +19,7 @@ Authentication:
Example:
export OPENAI_API_KEY="sk-..."
export OPENAI_RESPONSES_MODEL="gpt-4o"
export OPENAI_CHAT_MODEL="gpt-4o"
"""
from collections.abc import AsyncIterator
+15 -8
View File
@@ -36,16 +36,20 @@ These variables are used when the client is configured for OpenAI:
| `OPENAI_ORG_ID` | OpenAI organization ID |
| `OPENAI_BASE_URL` | Custom OpenAI-compatible base URL |
| `OPENAI_MODEL` | Generic fallback model |
| `OPENAI_RESPONSES_MODEL` | Preferred model for `OpenAIChatClient` |
| `OPENAI_CHAT_MODEL` | Preferred model for `OpenAIChatCompletionClient` |
| `OPENAI_CHAT_MODEL` | Preferred model for `OpenAIChatClient` |
| `OPENAI_CHAT_COMPLETION_MODEL` | Preferred model for `OpenAIChatCompletionClient` |
| `OPENAI_EMBEDDING_MODEL` | Preferred model for `OpenAIEmbeddingClient` |
Model lookup order:
- `OpenAIChatClient`: `OPENAI_RESPONSES_MODEL` -> `OPENAI_MODEL`
- `OpenAIChatCompletionClient`: `OPENAI_CHAT_MODEL` -> `OPENAI_MODEL`
- `OpenAIChatClient`: `OPENAI_CHAT_MODEL` -> `OPENAI_MODEL`
- `OpenAIChatCompletionClient`: `OPENAI_CHAT_COMPLETION_MODEL` -> `OPENAI_MODEL`
- `OpenAIEmbeddingClient`: `OPENAI_EMBEDDING_MODEL` -> `OPENAI_MODEL`
These model variables are only consulted when you do not pass `model=` directly. In other words,
`OpenAIChatClient(model="...")` ignores `OPENAI_CHAT_MODEL`, and
`OpenAIChatCompletionClient(model="...")` ignores `OPENAI_CHAT_COMPLETION_MODEL`.
### Azure OpenAI
These variables are used when the client is configured for Azure OpenAI:
@@ -57,16 +61,19 @@ These variables are used when the client is configured for Azure OpenAI:
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key |
| `AZURE_OPENAI_API_VERSION` | Azure OpenAI API version |
| `AZURE_OPENAI_MODEL` | Generic fallback deployment |
| `AZURE_OPENAI_RESPONSES_MODEL` | Preferred deployment for `OpenAIChatClient` |
| `AZURE_OPENAI_CHAT_MODEL` | Preferred deployment for `OpenAIChatCompletionClient` |
| `AZURE_OPENAI_CHAT_MODEL` | Preferred deployment for `OpenAIChatClient` |
| `AZURE_OPENAI_CHAT_COMPLETION_MODEL` | Preferred deployment for `OpenAIChatCompletionClient` |
| `AZURE_OPENAI_EMBEDDING_MODEL` | Preferred deployment for `OpenAIEmbeddingClient` |
Deployment lookup order:
- `OpenAIChatClient`: `AZURE_OPENAI_RESPONSES_MODEL` -> `AZURE_OPENAI_MODEL`
- `OpenAIChatCompletionClient`: `AZURE_OPENAI_CHAT_MODEL` -> `AZURE_OPENAI_MODEL`
- `OpenAIChatClient`: `AZURE_OPENAI_CHAT_MODEL` -> `AZURE_OPENAI_MODEL`
- `OpenAIChatCompletionClient`: `AZURE_OPENAI_CHAT_COMPLETION_MODEL` -> `AZURE_OPENAI_MODEL`
- `OpenAIEmbeddingClient`: `AZURE_OPENAI_EMBEDDING_MODEL` -> `AZURE_OPENAI_MODEL`
For Azure routing, the same rule applies: the client-specific deployment variable is checked first,
then the generic `AZURE_OPENAI_MODEL` fallback. Passing `model=` overrides both environment variables.
When both OpenAI and Azure environment variables are present, the generic clients prefer OpenAI
when `OPENAI_API_KEY` is configured. To use Azure explicitly, pass `azure_endpoint` or
`credential`.
@@ -289,7 +289,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_RESPONSES_MODEL`` and then ``OPENAI_MODEL``.
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL``.
api_key: API key. When not provided explicitly, the constructor reads
``OPENAI_API_KEY``. A callable API key is also supported.
org_id: OpenAI organization ID. When not provided explicitly, the constructor reads
@@ -331,7 +331,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``AZURE_OPENAI_RESPONSES_MODEL`` and then
reads ``AZURE_OPENAI_CHAT_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
@@ -380,8 +380,8 @@ class RawOpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_RESPONSES_MODEL`` and then ``OPENAI_MODEL`` for OpenAI,
or ``AZURE_OPENAI_RESPONSES_MODEL`` and then ``AZURE_OPENAI_MODEL`` for Azure.
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL`` for OpenAI,
or ``AZURE_OPENAI_CHAT_MODEL`` and then ``AZURE_OPENAI_MODEL`` for Azure.
api_key: API key override. For OpenAI this maps to ``OPENAI_API_KEY``.
For Azure this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
auth. A callable token provider is also accepted for backwards compatibility,
@@ -418,10 +418,10 @@ class RawOpenAIChatClient( # type: ignore[misc]
2. Explicit OpenAI API key or ``OPENAI_API_KEY``
3. Azure environment fallback
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_RESPONSES_MODEL``,
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_CHAT_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure routing
reads ``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_RESPONSES_MODEL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
"""
settings, client, use_azure_client = load_openai_service_settings(
@@ -437,8 +437,8 @@ class RawOpenAIChatClient( # type: ignore[misc]
client=async_client,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
openai_model_fields=("responses_model", "model"),
azure_model_fields=("responses_model", "model"),
openai_model_fields=("chat_model", "model"),
azure_model_fields=("chat_model", "model"),
responses_mode=True,
)
@@ -2501,7 +2501,7 @@ class OpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_RESPONSES_MODEL`` and then ``OPENAI_MODEL``.
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL``.
api_key: API key. When not provided explicitly, the constructor reads
``OPENAI_API_KEY``. A callable API key is also supported.
org_id: OpenAI organization ID. When not provided explicitly, the constructor reads
@@ -2547,7 +2547,7 @@ class OpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``AZURE_OPENAI_RESPONSES_MODEL`` and then
reads ``AZURE_OPENAI_CHAT_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
@@ -2600,8 +2600,8 @@ class OpenAIChatClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_RESPONSES_MODEL`` and then ``OPENAI_MODEL`` for OpenAI
routing, or ``AZURE_OPENAI_RESPONSES_MODEL`` and then
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL`` for OpenAI
routing, or ``AZURE_OPENAI_CHAT_MODEL`` and then
``AZURE_OPENAI_MODEL`` for Azure routing.
api_key: API key override. For OpenAI routing this maps to ``OPENAI_API_KEY``.
For Azure routing this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
@@ -2641,10 +2641,10 @@ class OpenAIChatClient( # type: ignore[misc]
2. Explicit OpenAI API key or ``OPENAI_API_KEY``
3. Azure environment fallback
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_RESPONSES_MODEL``,
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_CHAT_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure routing
reads ``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_RESPONSES_MODEL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
Examples:
@@ -203,7 +203,7 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL``.
reads ``OPENAI_CHAT_COMPLETION_MODEL`` and then ``OPENAI_MODEL``.
api_key: API key. When not provided explicitly, the constructor reads
``OPENAI_API_KEY``. A callable API key is also supported.
org_id: OpenAI organization ID. When not provided explicitly, the constructor reads
@@ -245,7 +245,7 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``AZURE_OPENAI_CHAT_MODEL`` and then
reads ``AZURE_OPENAI_CHAT_COMPLETION_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
@@ -294,8 +294,8 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL`` for OpenAI routing,
or ``AZURE_OPENAI_CHAT_MODEL`` and then ``AZURE_OPENAI_MODEL`` for Azure routing.
reads ``OPENAI_CHAT_COMPLETION_MODEL`` and then ``OPENAI_MODEL`` for OpenAI routing,
or ``AZURE_OPENAI_CHAT_COMPLETION_MODEL`` and then ``AZURE_OPENAI_MODEL`` for Azure routing.
api_key: API key override. For OpenAI routing this maps to ``OPENAI_API_KEY``.
For Azure routing this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
auth. A callable token provider is also accepted for backwards compatibility,
@@ -332,10 +332,10 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
2. Explicit OpenAI API key or ``OPENAI_API_KEY``
3. Azure environment fallback
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_CHAT_MODEL``,
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_CHAT_COMPLETION_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure routing
reads ``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_MODEL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_COMPLETION_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
"""
settings, client, use_azure_client = load_openai_service_settings(
@@ -351,8 +351,8 @@ class RawOpenAIChatCompletionClient( # type: ignore[misc]
client=async_client,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
openai_model_fields=("chat_model", "model"),
azure_model_fields=("chat_model", "model"),
openai_model_fields=("chat_completion_model", "model"),
azure_model_fields=("chat_completion_model", "model"),
)
self.client = client
@@ -1048,7 +1048,7 @@ class OpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL``.
reads ``OPENAI_CHAT_COMPLETION_MODEL`` and then ``OPENAI_MODEL``.
api_key: API key. When not provided explicitly, the constructor reads
``OPENAI_API_KEY``. A callable API key is also supported.
org_id: OpenAI organization ID. When not provided explicitly, the constructor reads
@@ -1088,7 +1088,7 @@ class OpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``AZURE_OPENAI_CHAT_MODEL`` and then
reads ``AZURE_OPENAI_CHAT_COMPLETION_MODEL`` and then
``AZURE_OPENAI_MODEL``.
azure_endpoint: Azure resource endpoint. When not provided explicitly, the constructor
reads ``AZURE_OPENAI_ENDPOINT``.
@@ -1135,8 +1135,8 @@ class OpenAIChatCompletionClient( # type: ignore[misc]
Keyword Args:
model: Model identifier to use for the request. When not provided, the constructor
reads ``OPENAI_CHAT_MODEL`` and then ``OPENAI_MODEL`` for OpenAI routing,
or ``AZURE_OPENAI_CHAT_MODEL`` and then
reads ``OPENAI_CHAT_COMPLETION_MODEL`` and then ``OPENAI_MODEL`` for OpenAI routing,
or ``AZURE_OPENAI_CHAT_COMPLETION_MODEL`` and then
``AZURE_OPENAI_MODEL`` for Azure routing.
api_key: API key override. For OpenAI routing this maps to ``OPENAI_API_KEY``.
For Azure routing this can be used instead of ``AZURE_OPENAI_API_KEY`` for key
@@ -1173,10 +1173,10 @@ class OpenAIChatCompletionClient( # type: ignore[misc]
2. Explicit OpenAI API key or ``OPENAI_API_KEY``
3. Azure environment fallback
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_CHAT_MODEL``,
OpenAI routing reads ``OPENAI_API_KEY``, ``OPENAI_CHAT_COMPLETION_MODEL``,
``OPENAI_MODEL``, ``OPENAI_ORG_ID``, and ``OPENAI_BASE_URL``. Azure routing
reads ``AZURE_OPENAI_ENDPOINT``, ``AZURE_OPENAI_BASE_URL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_MODEL``,
``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_CHAT_COMPLETION_MODEL``,
``AZURE_OPENAI_MODEL``, and ``AZURE_OPENAI_API_VERSION``.
Examples:
@@ -67,10 +67,10 @@ class OpenAISettings(TypedDict, total=False):
Can be set via environment variable OPENAI_MODEL.
embedding_model: The OpenAI embedding model to use, for example, text-embedding-3-small.
Can be set via environment variable OPENAI_EMBEDDING_MODEL.
chat_model: The OpenAI chat-completions model to prefer before OPENAI_MODEL.
chat_model: The OpenAIChatClient model to prefer before OPENAI_MODEL.
Can be set via environment variable OPENAI_CHAT_MODEL.
responses_model: The OpenAI responses model to prefer before OPENAI_MODEL.
Can be set via environment variable OPENAI_RESPONSES_MODEL.
chat_completion_model: The OpenAIChatCompletionClient model to prefer before OPENAI_MODEL.
Can be set via environment variable OPENAI_CHAT_COMPLETION_MODEL.
Examples:
.. code-block:: python
@@ -95,7 +95,7 @@ class OpenAISettings(TypedDict, total=False):
model: str | None
embedding_model: str | None
chat_model: str | None
responses_model: str | None
chat_completion_model: str | None
class AzureOpenAISettings(TypedDict, total=False):
@@ -107,24 +107,24 @@ class AzureOpenAISettings(TypedDict, total=False):
model: str | None
embedding_model: str | None
chat_model: str | None
responses_model: str | None
chat_completion_model: str | None
api_version: str | None
OpenAIModelSettingName = Literal["model", "embedding_model", "chat_model", "responses_model"]
OpenAIModelSettingName = Literal["model", "embedding_model", "chat_model", "chat_completion_model"]
OPENAI_MODEL_ENV_VARS: dict[OpenAIModelSettingName, str] = {
"model": "OPENAI_MODEL",
"embedding_model": "OPENAI_EMBEDDING_MODEL",
"chat_model": "OPENAI_CHAT_MODEL",
"responses_model": "OPENAI_RESPONSES_MODEL",
"chat_completion_model": "OPENAI_CHAT_COMPLETION_MODEL",
}
AZURE_MODEL_ENV_VARS: dict[OpenAIModelSettingName, str] = {
"model": "AZURE_OPENAI_MODEL",
"embedding_model": "AZURE_OPENAI_EMBEDDING_MODEL",
"chat_model": "AZURE_OPENAI_CHAT_MODEL",
"responses_model": "AZURE_OPENAI_RESPONSES_MODEL",
"chat_completion_model": "AZURE_OPENAI_CHAT_COMPLETION_MODEL",
}
@@ -43,15 +43,15 @@ def openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): #
"OPENAI_ORG_ID",
"OPENAI_MODEL",
"OPENAI_EMBEDDING_MODEL",
"OPENAI_CHAT_COMPLETION_MODEL",
"OPENAI_CHAT_MODEL",
"OPENAI_RESPONSES_MODEL",
"OPENAI_API_VERSION",
"OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_CHAT_COMPLETION_MODEL",
"AZURE_OPENAI_CHAT_MODEL",
"AZURE_OPENAI_RESPONSES_MODEL",
"AZURE_OPENAI_EMBEDDING_MODEL",
"AZURE_OPENAI_MODEL",
"AZURE_OPENAI_API_VERSION",
@@ -92,15 +92,15 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic
"OPENAI_ORG_ID",
"OPENAI_MODEL",
"OPENAI_EMBEDDING_MODEL",
"OPENAI_CHAT_COMPLETION_MODEL",
"OPENAI_CHAT_MODEL",
"OPENAI_RESPONSES_MODEL",
"OPENAI_API_VERSION",
"OPENAI_BASE_URL",
"AZURE_OPENAI_ENDPOINT",
"AZURE_OPENAI_BASE_URL",
"AZURE_OPENAI_API_KEY",
"AZURE_OPENAI_CHAT_COMPLETION_MODEL",
"AZURE_OPENAI_CHAT_MODEL",
"AZURE_OPENAI_RESPONSES_MODEL",
"AZURE_OPENAI_EMBEDDING_MODEL",
"AZURE_OPENAI_MODEL",
"AZURE_OPENAI_API_VERSION",
@@ -109,8 +109,8 @@ def azure_openai_unit_test_env(monkeypatch, exclude_list, override_env_param_dic
env_vars = {
"AZURE_OPENAI_ENDPOINT": "https://test-endpoint.openai.azure.com",
"AZURE_OPENAI_CHAT_MODEL": "test_chat_deployment",
"AZURE_OPENAI_RESPONSES_MODEL": "test_responses_deployment",
"AZURE_OPENAI_CHAT_COMPLETION_MODEL": "test_chat_deployment",
"AZURE_OPENAI_CHAT_MODEL": "test_responses_deployment",
"AZURE_OPENAI_EMBEDDING_MODEL": "test_embedding_deployment",
"AZURE_OPENAI_MODEL": "test_deployment",
"AZURE_OPENAI_API_KEY": "test_api_key",
@@ -150,12 +150,12 @@ def test_openai_chat_client_tool_methods_return_dict() -> None:
assert web_tool.get("type") == "web_search"
def test_init_prefers_openai_responses_model(monkeypatch, openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_RESPONSES_MODEL", "test_responses_model")
def test_init_prefers_openai_chat_model(monkeypatch, openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_CHAT_MODEL", "test_chat_model")
openai_responses_client = OpenAIChatClient()
assert openai_responses_client.model == "test_responses_model"
assert openai_responses_client.model == "test_chat_model"
def test_init_validation_fail() -> None:
@@ -23,7 +23,7 @@ pytestmark = pytest.mark.azure
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.openai.azure.com")
or (os.getenv("AZURE_OPENAI_RESPONSES_MODEL", "") == "" and os.getenv("AZURE_OPENAI_MODEL", "") == ""),
or (os.getenv("AZURE_OPENAI_CHAT_MODEL", "") == "" and os.getenv("AZURE_OPENAI_MODEL", "") == ""),
reason="No real Azure OpenAI endpoint or responses deployment provided; skipping integration tests.",
)
@@ -35,7 +35,7 @@ def _with_azure_openai_debug() -> Any:
try:
return await func(*args, **kwargs)
except Exception as exc:
model = os.getenv("AZURE_OPENAI_RESPONSES_MODEL") or os.getenv("AZURE_OPENAI_MODEL", "<unset>")
model = os.getenv("AZURE_OPENAI_CHAT_MODEL") or os.getenv("AZURE_OPENAI_MODEL", "<unset>")
api_version = os.getenv("AZURE_OPENAI_API_VERSION") or "preview"
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT", "<unset>")
debug_message = f"Azure OpenAI debug: endpoint={endpoint}, model={model}, api_version={api_version}"
@@ -96,7 +96,7 @@ async def get_weather(location: str) -> str:
def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatClient(credential=AzureCliCredential())
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_MODEL"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert isinstance(client, SupportsChatGetResponse)
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
@@ -106,7 +106,7 @@ def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) ->
def test_init_auto_detects_azure_env(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatClient()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_MODEL"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -141,7 +141,7 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
client = OpenAIChatClient(credential=lambda: "token")
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_MODEL"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -149,7 +149,7 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
def test_init_falls_back_to_generic_azure_deployment_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
client = OpenAIChatClient()
@@ -160,9 +160,9 @@ def test_init_falls_back_to_generic_azure_deployment_env(
def test_init_does_not_fall_back_to_openai_responses_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.setenv("OPENAI_RESPONSES_MODEL", "test_responses_model")
monkeypatch.setenv("OPENAI_CHAT_MODEL", "test_responses_model")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIChatClient()
@@ -171,9 +171,9 @@ def test_init_does_not_fall_back_to_openai_responses_model_for_azure_env(
def test_init_does_not_fall_back_to_openai_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_RESPONSES_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.delenv("OPENAI_RESPONSES_MODEL", raising=False)
monkeypatch.delenv("OPENAI_CHAT_MODEL", raising=False)
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
@@ -203,7 +203,7 @@ def test_init_with_credential_wraps_async_token_credential(
def test_init_uses_default_azure_api_version(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatClient(credential=AzureCliCredential())
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_RESPONSES_MODEL"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert client.api_version is not None
@@ -79,7 +79,7 @@ def test_supports_web_search_only() -> None:
def test_init_prefers_openai_chat_model(monkeypatch, openai_unit_test_env: dict[str, str]) -> None:
monkeypatch.setenv("OPENAI_CHAT_MODEL", "test_chat_model")
monkeypatch.setenv("OPENAI_CHAT_COMPLETION_MODEL", "test_chat_model")
open_ai_chat_completion = OpenAIChatCompletionClient()
@@ -1233,6 +1233,32 @@ def test_prepare_options_with_instructions(
assert prepared_options["messages"][0]["content"] == "You are a helpful assistant."
def test_prepare_options_with_instructions_no_duplicate(
openai_unit_test_env: dict[str, str],
) -> None:
"""Test that duplicate system message from instructions is not added again.
Regression test for https://github.com/microsoft/agent-framework/issues/5049
"""
client = OpenAIChatCompletionClient()
# Simulate messages that already contain the system instruction
messages = [
Message(role="system", text="You are a helpful assistant."),
Message(role="user", text="Hello"),
]
options = {"instructions": "You are a helpful assistant."}
prepared_options = client._prepare_options(messages, options)
# Should NOT duplicate the system message
assert "messages" in prepared_options
assert len(prepared_options["messages"]) == 2
assert prepared_options["messages"][0]["role"] == "system"
assert prepared_options["messages"][0]["content"] == "You are a helpful assistant."
assert prepared_options["messages"][1]["role"] == "user"
def test_prepare_message_with_author_name(openai_unit_test_env: dict[str, str]) -> None:
"""Test that author_name is included in prepared message."""
client = OpenAIChatCompletionClient()
@@ -29,7 +29,7 @@ pytestmark = pytest.mark.azure
skip_if_azure_openai_integration_tests_disabled = pytest.mark.skipif(
os.getenv("AZURE_OPENAI_ENDPOINT", "") in ("", "https://test-endpoint.openai.azure.com")
or (os.getenv("AZURE_OPENAI_CHAT_MODEL", "") == "" and os.getenv("AZURE_OPENAI_MODEL", "") == ""),
or (os.getenv("AZURE_OPENAI_CHAT_COMPLETION_MODEL", "") == "" and os.getenv("AZURE_OPENAI_MODEL", "") == ""),
reason="No real Azure OpenAI endpoint or chat deployment provided; skipping integration tests.",
)
@@ -41,7 +41,7 @@ def _with_azure_openai_debug() -> Any:
try:
return await func(*args, **kwargs)
except Exception as exc:
model = os.getenv("AZURE_OPENAI_CHAT_MODEL") or os.getenv("AZURE_OPENAI_MODEL", "<unset>")
model = os.getenv("AZURE_OPENAI_CHAT_COMPLETION_MODEL") or os.getenv("AZURE_OPENAI_MODEL", "<unset>")
api_version = os.getenv("AZURE_OPENAI_API_VERSION", "<unset>")
endpoint = os.getenv("AZURE_OPENAI_ENDPOINT", "<unset>")
debug_message = f"Azure OpenAI debug: endpoint={endpoint}, model={model}, api_version={api_version}"
@@ -78,7 +78,7 @@ async def get_weather(location: str) -> str:
def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatCompletionClient(azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"))
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_COMPLETION_MODEL"]
assert isinstance(client, SupportsChatGetResponse)
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.OTEL_PROVIDER_NAME == "azure.ai.openai"
@@ -89,7 +89,7 @@ def test_init_with_azure_endpoint(azure_openai_unit_test_env: dict[str, str]) ->
def test_init_auto_detects_azure_env(azure_openai_unit_test_env: dict[str, str]) -> None:
client = OpenAIChatCompletionClient()
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_COMPLETION_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -111,7 +111,7 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
client = OpenAIChatCompletionClient(credential=lambda: "token")
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_MODEL"]
assert client.model == azure_openai_unit_test_env["AZURE_OPENAI_CHAT_COMPLETION_MODEL"]
assert isinstance(client.client, AsyncAzureOpenAI)
assert client.azure_endpoint == azure_openai_unit_test_env["AZURE_OPENAI_ENDPOINT"]
@@ -119,7 +119,7 @@ def test_explicit_credential_wins_over_openai_api_key(monkeypatch, azure_openai_
def test_init_falls_back_to_generic_azure_deployment_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_COMPLETION_MODEL", raising=False)
client = OpenAIChatCompletionClient()
@@ -130,9 +130,9 @@ def test_init_falls_back_to_generic_azure_deployment_env(
def test_init_does_not_fall_back_to_openai_chat_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_COMPLETION_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.setenv("OPENAI_CHAT_MODEL", "test_chat_model")
monkeypatch.setenv("OPENAI_CHAT_COMPLETION_MODEL", "test_chat_model")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
OpenAIChatCompletionClient()
@@ -141,9 +141,9 @@ def test_init_does_not_fall_back_to_openai_chat_model_for_azure_env(
def test_init_does_not_fall_back_to_openai_model_for_azure_env(
monkeypatch, azure_openai_unit_test_env: dict[str, str]
) -> None:
monkeypatch.delenv("AZURE_OPENAI_CHAT_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_CHAT_COMPLETION_MODEL", raising=False)
monkeypatch.delenv("AZURE_OPENAI_MODEL", raising=False)
monkeypatch.delenv("OPENAI_CHAT_MODEL", raising=False)
monkeypatch.delenv("OPENAI_CHAT_COMPLETION_MODEL", raising=False)
monkeypatch.setenv("OPENAI_MODEL", "gpt-5")
with pytest.raises(SettingNotFoundError, match="Azure OpenAI client requires a model"):
-2
View File
@@ -70,7 +70,6 @@ agent-framework-ag-ui = { workspace = true }
agent-framework-azure-ai-search = { workspace = true }
agent-framework-azure-cosmos = { workspace = true }
agent-framework-anthropic = { workspace = true }
agent-framework-azure-ai = { workspace = true }
agent-framework-azurefunctions = { workspace = true }
agent-framework-bedrock = { workspace = true }
agent-framework-chatkit = { workspace = true }
@@ -187,7 +186,6 @@ executionEnvironments = [
{ root = "packages/ag-ui/tests", reportPrivateUsage = "none" },
{ root = "packages/anthropic/tests", reportPrivateUsage = "none" },
{ root = "packages/azure-ai-search/tests", reportPrivateUsage = "none" },
{ root = "packages/azure-ai/tests", reportPrivateUsage = "none" },
{ root = "packages/azure-cosmos/tests", reportPrivateUsage = "none" },
{ root = "packages/azurefunctions/tests", reportPrivateUsage = "none" },
{ root = "packages/bedrock/tests", reportPrivateUsage = "none" },
@@ -61,8 +61,8 @@ Depending on the selected client, set the appropriate environment variables:
**For OpenAI clients:**
- `OPENAI_API_KEY`: Your OpenAI API key
- `OPENAI_CHAT_MODEL`: The OpenAI model for `openai_chat_completion`
- `OPENAI_RESPONSES_MODEL`: The OpenAI model for `openai_responses`
- `OPENAI_CHAT_COMPLETION_MODEL`: The OpenAI model for `openai_chat_completion`
- `OPENAI_CHAT_MODEL`: The OpenAI model for `openai_responses`
**For Anthropic client (`anthropic`):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key
@@ -8,6 +8,7 @@ These samples demonstrate different approaches to managing conversation history
|------|-------------|
| [`suspend_resume_session.py`](suspend_resume_session.py) | Suspend and resume conversation sessions, comparing service-managed sessions (Azure AI Foundry) with in-memory sessions (OpenAI). |
| [`custom_history_provider.py`](custom_history_provider.py) | Implement a custom history provider by extending `BaseHistoryProvider`, enabling conversation persistence in your preferred storage backend. |
| [`cosmos_history_provider.py`](cosmos_history_provider.py) | Use Azure Cosmos DB as a history provider for durable conversation storage with `CosmosHistoryProvider`. |
| [`redis_history_provider.py`](redis_history_provider.py) | Use Redis as a history provider for persistent conversation history storage across sessions. |
## Prerequisites
@@ -21,6 +22,14 @@ These samples demonstrate different approaches to managing conversation history
**For `custom_history_provider.py`:**
- `OPENAI_API_KEY`: Your OpenAI API key
**For `cosmos_history_provider.py`:**
- `FOUNDRY_PROJECT_ENDPOINT`: Your Azure AI Foundry project endpoint
- `FOUNDRY_MODEL`: The Foundry model deployment name
- `AZURE_COSMOS_ENDPOINT`: Your Azure Cosmos DB account endpoint
- `AZURE_COSMOS_DATABASE_NAME`: The database that stores conversation history
- `AZURE_COSMOS_CONTAINER_NAME`: The container that stores conversation history
- Either `AZURE_COSMOS_KEY` or Azure CLI authentication (`az login`)
**For `redis_history_provider.py`:**
- `OPENAI_API_KEY`: Your OpenAI API key
- A running Redis server — default URL is `redis://localhost:6379`
@@ -1,20 +1,19 @@
# Copyright (c) Microsoft. All rights reserved.
# ruff: noqa: T201
import asyncio
import os
from agent_framework import Agent
from agent_framework.azure import CosmosHistoryProvider
from agent_framework.foundry import FoundryChatClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
from agent_framework_azure_cosmos import CosmosHistoryProvider
# Load environment variables from .env file.
load_dotenv()
"""
This sample demonstrates CosmosHistoryProvider as an agent context provider.
This sample demonstrates CosmosHistoryProvider as an agent history provider.
Key components:
- FoundryChatClient configured with an Azure AI project endpoint
@@ -54,39 +53,38 @@ async def main() -> None:
)
return
# 1. Create an Azure credential and Foundry chat client using project endpoint auth.
async with AzureCliCredential() as credential:
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
)
# 1. Create an Azure credential and a CosmosHistoryProvider for agent context
async with (
AzureCliCredential() as credential,
CosmosHistoryProvider(
endpoint=cosmos_endpoint,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
credential=cosmos_key or credential,
) as history_provider,
# 2. Create an agent that uses Cosmos for persisted conversation history.
Agent(
client=FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=credential,
),
name="CosmosHistoryAgent",
instructions="You are a helpful assistant that remembers prior turns.",
context_providers=[history_provider],
default_options={"store": False},
) as agent,
):
# 3. Create a session (session_id is used as the partition key).
session = agent.create_session()
# 2. Create an agent that uses the history provider as a context provider.
async with (
CosmosHistoryProvider(
endpoint=cosmos_endpoint,
database_name=cosmos_database_name,
container_name=cosmos_container_name,
credential=cosmos_key or credential,
) as history_provider,
client.as_agent(
name="CosmosHistoryAgent",
instructions="You are a helpful assistant that remembers prior turns.",
context_providers=[history_provider],
default_options={"store": False},
) as agent,
):
# 3. Create a session (session_id is used as the partition key).
session = agent.create_session()
# 4. Run a multi-turn conversation; history is persisted by CosmosHistoryProvider.
response1 = await agent.run("My name is Ada and I enjoy distributed systems.", session=session)
print(f"Assistant: {response1.text}")
# 4. Run a multi-turn conversation; history is persisted by CosmosHistoryProvider.
response1 = await agent.run("My name is Ada and I enjoy distributed systems.", session=session)
print(f"Assistant: {response1.text}")
response2 = await agent.run("What do you remember about me?", session=session)
print(f"Assistant: {response2.text}")
print(f"Container: {history_provider.container_name}")
response2 = await agent.run("What do you remember about me?", session=session)
print(f"Assistant: {response2.text}")
print(f"Container: {history_provider.container_name}")
if __name__ == "__main__":
+1 -1
View File
@@ -8,7 +8,7 @@ FOUNDRY_MODEL=gpt-4o
# Azure OpenAI workflow sample
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_RESPONSES_MODEL=gpt-4o
AZURE_OPENAI_CHAT_MODEL=gpt-4o
# Optional fallback env name also supported by workflow_with_agents/workflow.py:
AZURE_OPENAI_MODEL=gpt-4o
# Optional if you need to override the default API version:
+2 -2
View File
@@ -94,7 +94,7 @@ workflow_name/
| Sample | What it demonstrates | Required keys / auth |
| ------ | -------------------- | -------------------- |
| [**workflow_declarative/**](workflow_declarative/) | A YAML-defined workflow loaded through `WorkflowFactory`, with nested age-based branching and no model client code. | None |
| [**workflow_with_agents/**](workflow_with_agents/) | A content review workflow that uses agents as executors and routes based on structured review output (`Writer -> Reviewer -> Editor/Publisher -> Summarizer`). | `AZURE_OPENAI_ENDPOINT`, plus `AZURE_OPENAI_RESPONSES_MODEL` or `AZURE_OPENAI_MODEL`; Azure CLI auth via `az login`; `AZURE_OPENAI_API_VERSION` is optional |
| [**workflow_with_agents/**](workflow_with_agents/) | A content review workflow that uses agents as executors and routes based on structured review output (`Writer -> Reviewer -> Editor/Publisher -> Summarizer`). | `AZURE_OPENAI_ENDPOINT`, plus `AZURE_OPENAI_CHAT_MODEL` or `AZURE_OPENAI_MODEL`; Azure CLI auth via `az login`; `AZURE_OPENAI_API_VERSION` is optional |
| [**workflow_spam/**](workflow_spam/) | A multi-step spam detection workflow with human-in-the-loop approval, branching for spam vs. legitimate messages, and a final reporting step. | None |
| [**workflow_fanout/**](workflow_fanout/) | A larger fan-out/fan-in data processing workflow with parallel validation, multiple transformations, QA, aggregation, and demo failure toggles. | None |
@@ -130,7 +130,7 @@ export FOUNDRY_MODEL="gpt-4o"
# Azure OpenAI workflow_with_agents sample
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com"
export AZURE_OPENAI_RESPONSES_MODEL="gpt-4o"
export AZURE_OPENAI_CHAT_MODEL="gpt-4o"
export AZURE_OPENAI_MODEL="gpt-4o"
az login
@@ -2,7 +2,7 @@
# This sample uses Azure CLI auth, so run `az login` before starting DevUI.
AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
AZURE_OPENAI_RESPONSES_MODEL=gpt-4o
AZURE_OPENAI_CHAT_MODEL=gpt-4o
# Optional fallback env name also supported by the client:
# AZURE_OPENAI_MODEL=gpt-4o
# Optional if you need to override the default API version:
@@ -65,7 +65,7 @@ def is_approved(message: Any) -> bool:
# Create Azure OpenAI Responses chat client
client = OpenAIChatClient(
model=os.environ.get("AZURE_OPENAI_RESPONSES_MODEL") or os.environ.get("AZURE_OPENAI_MODEL"),
model=os.environ.get("AZURE_OPENAI_CHAT_MODEL") or os.environ.get("AZURE_OPENAI_MODEL"),
azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"),
api_version=os.environ.get("AZURE_OPENAI_API_VERSION"),
credential=AzureCliCredential(),
@@ -1,10 +1,10 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "agent-framework-azure-ai",
# "agent-framework-foundry",
# ]
# ///
# Run with: uv run samples/02-agents/embeddings/azure_ai_inference_embeddings.py
# Run with: uv run samples/02-agents/embeddings/foundry_embeddings.py
# Copyright (c) Microsoft. All rights reserved.
@@ -12,28 +12,29 @@ import asyncio
import pathlib
from agent_framework import Content
from agent_framework.azure import AzureAIInferenceEmbeddingClient
from agent_framework.foundry import FoundryEmbeddingClient
from dotenv import load_dotenv
load_dotenv()
"""Azure AI Inference Image Embedding Example
"""Microsoft Foundry 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.
Foundry embedding client with the Cohere-embed-v3-english model.
Images are passed as ``Content`` objects created with ``Content.from_data()``.
Prerequisites:
Deploy an embedding model in Azure AI Inference that supports image inputs, such as Cohere-embed-v3-english.
Deploy an embedding model to a Foundry-hosted inference endpoint that supports image inputs,
such as Cohere-embed-v3-english.
The details page for that model, has a target URI and a Key, which should be set in environment variables or a .env
file as follows, the target URI should append the `/models` path:
- AZURE_AI_INFERENCE_ENDPOINT: Your Azure AI model inference endpoint URL, for instance:
- FOUNDRY_MODELS_ENDPOINT: Your Foundry models endpoint URL, for instance:
https://<apim-instance>.azure-api.net/<foundry-instance>/models
- AZURE_AI_INFERENCE_API_KEY: Your API key
- AZURE_AI_INFERENCE_EMBEDDING_MODEL: The text embedding model name
- FOUNDRY_MODELS_API_KEY: Your API key
- FOUNDRY_EMBEDDING_MODEL: The text embedding model name
(e.g. "text-embedding-3-small")
- AZURE_AI_INFERENCE_IMAGE_EMBEDDING_MODEL: The image embedding model name
- FOUNDRY_IMAGE_EMBEDDING_MODEL: The image embedding model name
(e.g. "Cohere-embed-v3-english")
"""
@@ -41,8 +42,8 @@ SAMPLE_IMAGE_PATH = pathlib.Path(__file__).parent.parent.parent / "shared" / "sa
async def main() -> None:
"""Generate image embeddings with Azure AI Inference."""
async with AzureAIInferenceEmbeddingClient() as client:
"""Generate image embeddings with Foundry."""
async with FoundryEmbeddingClient() 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")
+1 -1
View File
@@ -17,7 +17,7 @@ The Model Context Protocol (MCP) is an open standard for connecting AI agents to
## Prerequisites
- `OPENAI_API_KEY` environment variable
- `OPENAI_RESPONSES_MODEL` environment variable
- `OPENAI_CHAT_MODEL` environment variable
Run `mcp_api_key_auth.py` with the MCP API key as the first command-line argument.
@@ -25,7 +25,7 @@ The new usage tracking sample uses `OpenAIChatClient`, so set the usual OpenAI r
```bash
export OPENAI_API_KEY="your-openai-api-key"
export OPENAI_RESPONSES_MODEL="gpt-4.1-mini"
export OPENAI_CHAT_MODEL="gpt-4.1-mini"
```
Then run:
@@ -40,8 +40,8 @@ ENABLE_SENSITIVE_DATA=true
# OpenAI specific variables
# ==========================
OPENAI_API_KEY="..."
OPENAI_RESPONSES_MODEL="gpt-4o-2024-08-06"
OPENAI_CHAT_MODEL="gpt-4o-2024-08-06"
OPENAI_CHAT_COMPLETION_MODEL="gpt-4o-2024-08-06"
# Azure AI Foundry specific variables
# ====================================
@@ -20,7 +20,7 @@ This sample demonstrates using Anthropic with:
async def main() -> None:
"""Example of streaming response (get results as they are generated)."""
client = AnthropicClient[AnthropicChatOptions]()
client = AnthropicClient[AnthropicChatOptions](model_id="claude-sonnet-4-5-20250929")
# Create MCP tool configuration using instance method
mcp_tool = client.get_mcp_tool(
@@ -12,6 +12,9 @@ Supported MCP server types:
- "http": Remote HTTP server
- "sse": Remote SSE (Server-Sent Events) server
Environment variables:
- ANTHROPIC_API_KEY: Your Anthropic API key
SECURITY NOTE: MCP servers can expose powerful capabilities. Only configure
servers you trust. Use permission handlers to control what actions are allowed.
"""
@@ -15,6 +15,9 @@ Available built-in tools:
- "Glob": Search for files by pattern
- "Grep": Search file contents
Environment variables:
- ANTHROPIC_API_KEY: Your Anthropic API key
SECURITY NOTE: Only enable permissions that are necessary for your use case.
More permissions mean more potential for unintended actions.
"""
@@ -24,6 +27,10 @@ from typing import Any
from agent_framework.anthropic import ClaudeAgent
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def prompt_permission(
@@ -6,6 +6,9 @@ Claude Agent with Session Management
This sample demonstrates session management with ClaudeAgent, showing
persistent conversation capabilities. Sessions are automatically persisted
by the Claude Code CLI.
Environment variables:
- ANTHROPIC_API_KEY: Your Anthropic API key
"""
import asyncio
@@ -14,8 +17,12 @@ from typing import Annotated
from agent_framework import tool
from agent_framework.anthropic import ClaudeAgent
from dotenv import load_dotenv
from pydantic import Field
# Load environment variables from .env file
load_dotenv()
@tool
def get_weather(
@@ -7,6 +7,9 @@ This sample demonstrates how to enable shell command execution with ClaudeAgent.
By providing a permission handler via `can_use_tool`, the agent can execute
shell commands to perform tasks like listing files, running scripts, or executing system commands.
Environment variables:
- ANTHROPIC_API_KEY: Your Anthropic API key
SECURITY NOTE: Only enable shell permissions when you trust the agent's actions.
Shell commands have full access to your system within the permissions of the running process.
"""
@@ -16,6 +19,10 @@ from typing import Any
from agent_framework.anthropic import ClaudeAgent
from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def prompt_permission(
@@ -13,11 +13,18 @@ Available built-in tools:
- "Edit": Edit existing files
- "Glob": Search for files by pattern
- "Grep": Search file contents
Environment variables:
- ANTHROPIC_API_KEY: Your Anthropic API key
"""
import asyncio
from agent_framework.anthropic import ClaudeAgent
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
@@ -10,6 +10,9 @@ Available web tools:
- "WebFetch": Fetch content from URLs
- "WebSearch": Search the web
Environment variables:
- ANTHROPIC_API_KEY: Your Anthropic API key
SECURITY NOTE: Only enable URL permissions when you trust the agent's actions.
URL fetching allows the agent to access any URL accessible from your network.
"""
@@ -17,6 +20,10 @@ URL fetching allows the agent to access any URL accessible from your network.
import asyncio
from agent_framework.anthropic import ClaudeAgent
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
@@ -21,6 +21,10 @@ This sample demonstrates using Anthropic with:
You can also set additonal_chat_options with "additional_beta_flags" per request.
- Creating an agent with the Code Interpreter tool and a Skill.
- Catching and downloading generated files from the agent.
Environment variables:
- ANTHROPIC_API_KEY: Your Anthropic API key
- ANTHROPIC_CHAT_MODEL_ID: The Anthropic model to use, such as "claude-sonnet-4-5-20250929"
"""
@@ -76,19 +76,19 @@ async def example_with_session_persistence_in_memory() -> None:
# First conversation
query1 = "What's the weather like in Tokyo?"
print(f"User: {query1}")
result1 = await agent.run(query1, session=session, store=False)
result1 = await agent.run(query1, session=session, options={"store": False})
print(f"Agent: {result1.text}")
# Second conversation using the same session - maintains context
query2 = "How about London?"
print(f"\nUser: {query2}")
result2 = await agent.run(query2, session=session, store=False)
result2 = await agent.run(query2, session=session, options={"store": False})
print(f"Agent: {result2.text}")
# Third conversation - agent should remember both previous cities
query3 = "Which of the cities I asked about has better weather?"
print(f"\nUser: {query3}")
result3 = await agent.run(query3, session=session, store=False)
result3 = await agent.run(query3, session=session, options={"store": False})
print(f"Agent: {result3.text}")
print("Note: The agent remembers context from previous messages in the same session.\n")
@@ -1,9 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework.foundry import FoundryAgent
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""
Foundry Agent — Connect to a pre-configured agent in Microsoft Foundry
@@ -15,15 +17,18 @@ tools are all configured on the service — you just connect and run.
Environment variables:
FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint
FOUNDRY_AGENT_NAME — Name of the agent in Foundry
FOUNDRY_AGENT_VERSION — Version of the agent (for PromptAgents)
FOUNDRY_AGENT_VERSION — Version of the agent (optional, for PromptAgents)
"""
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-prompt-agent",
agent_version="1.0",
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
agent_name=os.environ["FOUNDRY_AGENT_NAME"],
agent_version=os.environ.get("FOUNDRY_AGENT_VERSION"),
credential=AzureCliCredential(),
)
@@ -1,10 +1,12 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework import Agent
from agent_framework.foundry import FoundryAgent, RawFoundryAgentChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""
Foundry Agent — Custom client configuration
@@ -18,16 +20,19 @@ This sample demonstrates three ways to customize the FoundryAgent client layer:
Environment variables:
FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint
FOUNDRY_AGENT_NAME — Name of the agent in Foundry
FOUNDRY_AGENT_VERSION — Version of the agent
FOUNDRY_AGENT_VERSION — Version of the agent (optional, for PromptAgents)
"""
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
# Option 1: Default — full middleware on both agent and client
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-agent",
agent_version="1.0",
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
agent_name=os.environ["FOUNDRY_AGENT_NAME"],
agent_version=os.environ.get("FOUNDRY_AGENT_VERSION"),
credential=AzureCliCredential(),
)
result = await agent.run("Hello from the default setup!")
@@ -35,9 +40,9 @@ async def main() -> None:
# Option 2: Raw client — no client-level middleware (agent middleware still active)
agent_raw_client = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-agent",
agent_version="1.0",
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
agent_name=os.environ["FOUNDRY_AGENT_NAME"],
agent_version=os.environ.get("FOUNDRY_AGENT_VERSION"),
credential=AzureCliCredential(),
client_type=RawFoundryAgentChatClient,
)
@@ -47,9 +52,9 @@ async def main() -> None:
# Option 3: Composition — use Agent(client=...) directly
# this will not run the checks that the `FoundryAgent` does on things like tools.
client = RawFoundryAgentChatClient(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-agent",
agent_version="1.0",
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
agent_name=os.environ["FOUNDRY_AGENT_NAME"],
agent_version=os.environ.get("FOUNDRY_AGENT_VERSION"),
credential=AzureCliCredential(),
)
agent_composed = Agent(client=client)
@@ -1,9 +1,11 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from agent_framework.foundry import FoundryAgent
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""
Foundry Agent — Connect to a HostedAgent (no version needed)
@@ -16,12 +18,15 @@ Environment variables:
FOUNDRY_AGENT_NAME — Name of the hosted agent
"""
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
# HostedAgents don't need agent_version
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-hosted-agent",
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
agent_name=os.environ["FOUNDRY_AGENT_NAME"],
credential=AzureCliCredential(),
)
@@ -5,6 +5,7 @@ import os
from agent_framework.foundry import FoundryAgent
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
"""
Foundry Agent with Environment Variables
@@ -18,6 +19,9 @@ Environment variables:
FOUNDRY_AGENT_VERSION — Version of the agent (optional, for PromptAgents)
"""
# Load environment variables from .env file
load_dotenv()
async def main() -> None:
agent = FoundryAgent(
@@ -1,11 +1,13 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import os
from typing import Annotated
from agent_framework import tool
from agent_framework.foundry import FoundryAgent
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from pydantic import Field
"""
@@ -24,6 +26,9 @@ Environment variables:
FOUNDRY_AGENT_VERSION — Version of the agent
"""
# Load environment variables from .env file
load_dotenv()
@tool(approval_mode="never_require")
def get_weather(
@@ -35,9 +40,9 @@ def get_weather(
async def main() -> None:
agent = FoundryAgent(
project_endpoint="https://your-project.services.ai.azure.com",
agent_name="my-weather-agent",
agent_version="1.0",
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
agent_name=os.environ["FOUNDRY_AGENT_NAME"],
agent_version=os.environ["FOUNDRY_AGENT_VERSION"],
credential=AzureCliCredential(),
tools=[get_weather],
)
@@ -8,22 +8,26 @@ from agent_framework import Agent
from agent_framework.foundry import FoundryChatClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from openai import AsyncAzureOpenAI
# Load environment variables from .env file
load_dotenv()
from openai import AsyncOpenAI
"""
Foundry Chat Client with Code Interpreter and Files Example
This sample demonstrates using get_code_interpreter_tool() with Responses on Foundry
for Python code execution and data analysis with uploaded files.
Environment variables:
FOUNDRY_PROJECT_ENDPOINT — Azure AI Foundry project endpoint
FOUNDRY_MODEL — Model deployment name (e.g. "gpt-4o")
"""
# Load environment variables from .env file
load_dotenv()
# Helper functions
async def create_sample_file_and_upload(openai_client: AsyncAzureOpenAI) -> tuple[str, str]:
async def create_sample_file_and_upload(openai_client: AsyncOpenAI) -> tuple[str, str]:
"""Create a sample CSV file and upload it for Foundry code interpreter use."""
csv_data = """name,department,salary,years_experience
Alice Johnson,Engineering,95000,5
@@ -51,7 +55,7 @@ Frank Wilson,Engineering,88000,6
return temp_file_path, uploaded_file.id
async def cleanup_files(openai_client: AsyncAzureOpenAI, temp_file_path: str, file_id: str) -> None:
async def cleanup_files(openai_client: AsyncOpenAI, temp_file_path: str, file_id: str) -> None:
"""Clean up both local temporary file and uploaded file."""
# Clean up: delete the uploaded file
await openai_client.files.delete(file_id)
@@ -68,36 +72,33 @@ async def main() -> None:
# Initialize the underlying OpenAI client for file operations
credential = AzureCliCredential()
async def get_token():
token = credential.get_token("https://cognitiveservices.azure.com/.default")
return token.token
openai_client = AsyncAzureOpenAI(
azure_ad_token_provider=get_token,
api_version="2024-05-01-preview",
# Create FoundryChatClient first, then reuse its project client for file operations
client = FoundryChatClient(
credential=credential,
project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
model=os.environ["FOUNDRY_MODEL"],
)
openai_client = client.project_client.get_openai_client()
temp_file_path, file_id = await create_sample_file_and_upload(openai_client)
# Create agent using FoundryChatClient
client = FoundryChatClient(credential=credential)
try:
# Create code interpreter tool with file access
code_interpreter_tool = client.get_code_interpreter_tool(file_ids=[file_id])
# Create code interpreter tool with file access
code_interpreter_tool = client.get_code_interpreter_tool(file_ids=[file_id])
agent = Agent(
client=client,
instructions="You are a helpful assistant that can analyze data files using Python code.",
tools=[code_interpreter_tool],
)
agent = Agent(
client=client,
instructions="You are a helpful assistant that can analyze data files using Python code.",
tools=[code_interpreter_tool],
)
# Test the code interpreter with the uploaded file
query = "Analyze the employee data in the uploaded CSV file. Calculate average salary by department."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
await cleanup_files(openai_client, temp_file_path, file_id)
# Test the code interpreter with the uploaded file
query = "Analyze the employee data in the uploaded CSV file. Calculate average salary by department."
print(f"User: {query}")
result = await agent.run(query)
print(f"Agent: {result.text}")
finally:
await cleanup_files(openai_client, temp_file_path, file_id)
if __name__ == "__main__":

Some files were not shown because too many files have changed in this diff Show More