mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e26c3580fb | ||
|
|
bbae6401b1 | ||
|
|
a58f55876c | ||
|
|
922b85485d | ||
|
|
30920e1f9d | ||
|
|
95fd5ec658 | ||
|
|
47d82911c0 | ||
|
|
339e76d51f | ||
|
|
62595b233f | ||
|
|
fd253c0b0e | ||
|
|
7e8e9e3074 | ||
|
|
7607acd009 | ||
|
|
3d87cec304 |
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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."""
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 \\
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
+51
-51
@@ -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],
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
+55
-57
@@ -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
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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`
|
||||
|
||||
+33
-35
@@ -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__":
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+13
-12
@@ -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")
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
+7
@@ -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],
|
||||
)
|
||||
|
||||
+31
-30
@@ -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
Reference in New Issue
Block a user