Merge branch 'main' into feat/durable_task

This commit is contained in:
Shyju Krishnankutty
2026-03-09 12:01:42 -07:00
committed by GitHub
35 changed files with 644 additions and 1307 deletions
+18
View File
@@ -8,6 +8,10 @@ inputs:
os: os:
description: The operating system to set up description: The operating system to set up
required: true required: true
exclude-packages:
description: Space-separated list of packages to exclude from uv sync
required: false
default: ''
runs: runs:
using: "composite" using: "composite"
@@ -19,6 +23,20 @@ runs:
enable-cache: true enable-cache: true
cache-suffix: ${{ inputs.os }}-${{ inputs.python-version }} cache-suffix: ${{ inputs.os }}-${{ inputs.python-version }}
cache-dependency-glob: "**/uv.lock" cache-dependency-glob: "**/uv.lock"
- name: Exclude incompatible workspace packages
if: ${{ inputs.exclude-packages != '' }}
shell: bash
run: |
for pkg in ${{ inputs.exclude-packages }}; do
for f in python/packages/*/pyproject.toml; do
if grep -q "name = \"$pkg\"" "$f"; then
pkg_dir=$(dirname "$f" | sed 's|python/||')
echo "Excluding workspace package: $pkg ($pkg_dir)"
sed -i.bak '/\[tool\.uv\.workspace\]/a\exclude = ["'"$pkg_dir"'"]' python/pyproject.toml
sed -i.bak '/'"$pkg"' = { workspace = true }/d' python/pyproject.toml
fi
done
done
- name: Install the project - name: Install the project
shell: bash shell: bash
run: | run: |
+4 -4
View File
@@ -18,7 +18,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.10"] python-version: ["3.11"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
defaults: defaults:
@@ -55,7 +55,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.10"] python-version: ["3.11"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
defaults: defaults:
@@ -84,7 +84,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.10"] python-version: ["3.11"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
defaults: defaults:
@@ -117,7 +117,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.10"] python-version: ["3.11"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
continue-on-error: true continue-on-error: true
defaults: defaults:
@@ -170,7 +170,7 @@ jobs:
environment: integration environment: integration
timeout-minutes: 60 timeout-minutes: 60
env: env:
UV_PYTHON: "3.10" UV_PYTHON: "3.11"
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
+1
View File
@@ -67,6 +67,7 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
os: ${{ runner.os }} os: ${{ runner.os }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
env: env:
# Configure a constant location for the uv cache # Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache UV_CACHE_DIR: /tmp/.uv-cache
+1 -1
View File
@@ -288,7 +288,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: integration environment: integration
env: env:
UV_PYTHON: "3.10" UV_PYTHON: "3.11"
OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }} OPENAI_CHAT_MODEL_ID: ${{ vars.OPENAI__CHATMODELID }}
OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }} OPENAI_RESPONSES_MODEL_ID: ${{ vars.OPENAI__RESPONSESMODELID }}
OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }} OPENAI_API_KEY: ${{ secrets.OPENAI__APIKEY }}
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
run: run:
working-directory: python working-directory: python
env: env:
UV_PYTHON: "3.10" UV_PYTHON: "3.11"
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
# Save the PR number to a file since the workflow_run event # Save the PR number to a file since the workflow_run event
+2 -1
View File
@@ -34,12 +34,13 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
os: ${{ runner.os }} os: ${{ runner.os }}
exclude-packages: ${{ matrix.python-version == '3.10' && 'agent-framework-github-copilot' || '' }}
env: env:
# Configure a constant location for the uv cache # Configure a constant location for the uv cache
UV_CACHE_DIR: /tmp/.uv-cache UV_CACHE_DIR: /tmp/.uv-cache
# Unit tests # Unit tests
- name: Run all tests - name: Run all tests
run: uv run poe all-tests run: uv run poe all-tests ${{ matrix.python-version == '3.10' && '--ignore-glob=packages/github_copilot/**' || '' }}
working-directory: ./python working-directory: ./python
# Surface failing tests # Surface failing tests
+7
View File
@@ -20,6 +20,13 @@ When making changes to a package, check if the following need updates:
- The package's `AGENTS.md` file (adding/removing/renaming public APIs, architecture changes, import path changes) - The package's `AGENTS.md` file (adding/removing/renaming public APIs, architecture changes, import path changes)
- The agent skills in `.github/skills/` if conventions, commands, or workflows change - The agent skills in `.github/skills/` if conventions, commands, or workflows change
## Pull Request Description Guidance
When preparing a PR description:
- Follow the repository PR template at `.github/pull_request_template.md` and keep its structure/headings.
- Describe the net change relative to `main` (this is implied; do not call it out explicitly as "vs main").
- Do not add ad-hoc validation sections (for example, "Validation" or "Tests run"); CI/CD and the template checklist cover validation status.
## Quick Reference ## Quick Reference
Run `uv run poe` from the `python/` directory to see available commands. See [DEV_SETUP.md](DEV_SETUP.md) for detailed usage. Run `uv run poe` from the `python/` directory to see available commands. See [DEV_SETUP.md](DEV_SETUP.md) for detailed usage.
@@ -37,9 +37,8 @@ from agent_framework.openai._responses_client import RawOpenAIResponsesClient
from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import ( from azure.ai.projects.models import (
ApproximateLocation, ApproximateLocation,
CodeInterpreterContainerAuto, AutoCodeInterpreterToolParam,
CodeInterpreterTool, CodeInterpreterTool,
FoundryFeaturesOptInKeys,
ImageGenTool, ImageGenTool,
MCPTool, MCPTool,
PromptAgentDefinition, PromptAgentDefinition,
@@ -66,7 +65,6 @@ if sys.version_info >= (3, 11):
else: else:
from typing_extensions import Self, TypedDict # type: ignore # pragma: no cover from typing_extensions import Self, TypedDict # type: ignore # pragma: no cover
logger = logging.getLogger("agent_framework.azure") logger = logging.getLogger("agent_framework.azure")
@@ -79,9 +77,6 @@ class AzureAIProjectAgentOptions(OpenAIResponsesOptions, total=False):
reasoning: Reasoning # type: ignore[misc] reasoning: Reasoning # type: ignore[misc]
"""Configuration for enabling reasoning capabilities (requires azure.ai.projects.models.Reasoning).""" """Configuration for enabling reasoning capabilities (requires azure.ai.projects.models.Reasoning)."""
foundry_features: FoundryFeaturesOptInKeys | str
"""Optional Foundry preview feature opt-in for agent version creation."""
AzureAIClientOptionsT = TypeVar( AzureAIClientOptionsT = TypeVar(
"AzureAIClientOptionsT", "AzureAIClientOptionsT",
@@ -123,6 +118,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
model_deployment_name: str | None = None, model_deployment_name: str | None = None,
credential: AzureCredentialTypes | None = None, credential: AzureCredentialTypes | None = None,
use_latest_version: bool | None = None, use_latest_version: bool | None = None,
allow_preview: bool | None = None,
env_file_path: str | None = None, env_file_path: str | None = None,
env_file_encoding: str | None = None, env_file_encoding: str | None = None,
**kwargs: Any, **kwargs: Any,
@@ -148,6 +144,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
AsyncTokenCredential, or a callable token provider. AsyncTokenCredential, or a callable token provider.
use_latest_version: Boolean flag that indicates whether to use latest agent version use_latest_version: Boolean flag that indicates whether to use latest agent version
if it exists in the service. if it exists in the service.
allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``.
env_file_path: Path to environment file for loading settings. env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file. env_file_encoding: Encoding of the environment file.
kwargs: Additional keyword arguments passed to the parent class. kwargs: Additional keyword arguments passed to the parent class.
@@ -208,11 +205,14 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
# Use provided credential # Use provided credential
if not credential: if not credential:
raise ValueError("Azure credential is required when project_client is not provided.") raise ValueError("Azure credential is required when project_client is not provided.")
project_client = AIProjectClient( project_client_kwargs: dict[str, Any] = {
endpoint=resolved_endpoint, "endpoint": resolved_endpoint,
credential=credential, # type: ignore[arg-type] "credential": credential, # type: ignore[arg-type]
user_agent=AGENT_FRAMEWORK_USER_AGENT, "user_agent": AGENT_FRAMEWORK_USER_AGENT,
) }
if allow_preview is not None:
project_client_kwargs["allow_preview"] = allow_preview
project_client = AIProjectClient(**project_client_kwargs)
should_close_client = True should_close_client = True
# Initialize parent # Initialize parent
@@ -413,8 +413,6 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
"definition": PromptAgentDefinition(**args), "definition": PromptAgentDefinition(**args),
"description": self.agent_description, "description": self.agent_description,
} }
if foundry_features := run_options.get("foundry_features"):
create_version_kwargs["foundry_features"] = foundry_features
created_agent = await self.project_client.agents.create_version(**create_version_kwargs) created_agent = await self.project_client.agents.create_version(**create_version_kwargs)
@@ -513,7 +511,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
"temperature": ("temperature",), "temperature": ("temperature",),
"top_p": ("top_p",), "top_p": ("top_p",),
"reasoning": ("reasoning",), "reasoning": ("reasoning",),
"foundry_features": ("foundry_features",), "allow_preview": ("allow_preview",),
} }
for run_keys in agent_level_option_to_run_keys.values(): for run_keys in agent_level_option_to_run_keys.values():
@@ -939,7 +937,7 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
if file_ids is None and isinstance(container, dict): if file_ids is None and isinstance(container, dict):
file_ids = container.get("file_ids") file_ids = container.get("file_ids")
resolved = resolve_file_ids(file_ids) resolved = resolve_file_ids(file_ids)
tool_container = CodeInterpreterContainerAuto(file_ids=resolved) tool_container = AutoCodeInterpreterToolParam(file_ids=resolved)
return CodeInterpreterTool(container=tool_container, **kwargs) return CodeInterpreterTool(container=tool_container, **kwargs)
@staticmethod @staticmethod
@@ -1244,6 +1242,7 @@ class AzureAIClient(
model_deployment_name: str | None = None, model_deployment_name: str | None = None,
credential: AzureCredentialTypes | None = None, credential: AzureCredentialTypes | None = None,
use_latest_version: bool | None = None, use_latest_version: bool | None = None,
allow_preview: bool | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None, middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
function_invocation_configuration: FunctionInvocationConfiguration | None = None, function_invocation_configuration: FunctionInvocationConfiguration | None = None,
env_file_path: str | None = None, env_file_path: str | None = None,
@@ -1268,6 +1267,7 @@ class AzureAIClient(
or AsyncTokenCredential. or AsyncTokenCredential.
use_latest_version: Boolean flag that indicates whether to use latest agent version use_latest_version: Boolean flag that indicates whether to use latest agent version
if it exists in the service. if it exists in the service.
allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``
middleware: Optional sequence of chat middlewares to include. middleware: Optional sequence of chat middlewares to include.
function_invocation_configuration: Optional function invocation configuration. function_invocation_configuration: Optional function invocation configuration.
env_file_path: Path to environment file for loading settings. env_file_path: Path to environment file for loading settings.
@@ -1318,6 +1318,7 @@ class AzureAIClient(
model_deployment_name=model_deployment_name, model_deployment_name=model_deployment_name,
credential=credential, credential=credential,
use_latest_version=use_latest_version, use_latest_version=use_latest_version,
allow_preview=allow_preview,
middleware=middleware, middleware=middleware,
function_invocation_configuration=function_invocation_configuration, function_invocation_configuration=function_invocation_configuration,
env_file_path=env_file_path, env_file_path=env_file_path,
@@ -18,6 +18,7 @@ from agent_framework._sessions import AgentSession, BaseContextProvider, Session
from agent_framework._settings import load_settings from agent_framework._settings import load_settings
from agent_framework.azure._entra_id_authentication import AzureCredentialTypes from agent_framework.azure._entra_id_authentication import AzureCredentialTypes
from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.aio import AIProjectClient
from openai.types.responses import ResponseInputItemParam
from ._shared import AzureAISettings from ._shared import AzureAISettings
@@ -58,6 +59,7 @@ class FoundryMemoryProvider(BaseContextProvider):
project_client: AIProjectClient | None = None, project_client: AIProjectClient | None = None,
project_endpoint: str | None = None, project_endpoint: str | None = None,
credential: AzureCredentialTypes | None = None, credential: AzureCredentialTypes | None = None,
allow_preview: bool | None = None,
memory_store_name: str, memory_store_name: str,
scope: str | None = None, scope: str | None = None,
context_prompt: str | None = None, context_prompt: str | None = None,
@@ -74,6 +76,7 @@ class FoundryMemoryProvider(BaseContextProvider):
credential: Azure credential for authentication. Accepts a TokenCredential, credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider. AsyncTokenCredential, or a callable token provider.
Required when project_client is not provided. Required when project_client is not provided.
allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``.
memory_store_name: The name of the memory store to use. memory_store_name: The name of the memory store to use.
scope: The namespace that logically groups and isolates memories (e.g., user ID). scope: The namespace that logically groups and isolates memories (e.g., user ID).
If None, `session_id` will be used. If None, `session_id` will be used.
@@ -100,11 +103,14 @@ class FoundryMemoryProvider(BaseContextProvider):
) )
if not credential: if not credential:
raise ValueError("Azure credential is required when project_client is not provided.") raise ValueError("Azure credential is required when project_client is not provided.")
project_client = AIProjectClient( project_client_kwargs: dict[str, Any] = {
endpoint=resolved_endpoint, "endpoint": resolved_endpoint,
credential=credential, # type: ignore[arg-type] "credential": credential, # type: ignore[arg-type]
user_agent=AGENT_FRAMEWORK_USER_AGENT, "user_agent": AGENT_FRAMEWORK_USER_AGENT,
) }
if allow_preview is not None:
project_client_kwargs["allow_preview"] = allow_preview
project_client = AIProjectClient(**project_client_kwargs)
if not memory_store_name: if not memory_store_name:
raise ValueError("memory_store_name is required") raise ValueError("memory_store_name is required")
@@ -169,8 +175,8 @@ class FoundryMemoryProvider(BaseContextProvider):
return return
# Convert input messages to memory search item format # Convert input messages to memory search item format
items = [ items: list[ResponseInputItemParam] = [
{"type": "text", "text": msg.text} {"type": "message", "role": "user", "content": msg.text}
for msg in context.input_messages for msg in context.input_messages
if msg and msg.text and msg.text.strip() if msg and msg.text and msg.text.strip()
] ]
@@ -224,7 +230,7 @@ class FoundryMemoryProvider(BaseContextProvider):
messages_to_store.extend(context.response.messages) messages_to_store.extend(context.response.messages)
# Filter and convert messages to memory update item format # Filter and convert messages to memory update item format
items: list[dict[str, str]] = [] items: list[ResponseInputItemParam] = []
for message in messages_to_store: for message in messages_to_store:
if message.role in {"user", "assistant", "system"} and message.text and message.text.strip(): if message.role in {"user", "assistant", "system"} and message.text and message.text.strip():
if message.role == "user": if message.role == "user":
@@ -102,6 +102,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
project_endpoint: str | None = None, project_endpoint: str | None = None,
model: str | None = None, model: str | None = None,
credential: AzureCredentialTypes | None = None, credential: AzureCredentialTypes | None = None,
allow_preview: bool | None = None,
env_file_path: str | None = None, env_file_path: str | None = None,
env_file_encoding: str | None = None, env_file_encoding: str | None = None,
) -> None: ) -> None:
@@ -117,6 +118,7 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
credential: Azure credential for authentication. Accepts a TokenCredential, credential: Azure credential for authentication. Accepts a TokenCredential,
AsyncTokenCredential, or a callable token provider. AsyncTokenCredential, or a callable token provider.
Required when project_client is not provided. Required when project_client is not provided.
allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``.
env_file_path: Path to environment file for loading settings. env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file. env_file_encoding: Encoding of the environment file.
@@ -146,11 +148,14 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
if not credential: if not credential:
raise ValueError("Azure credential is required when project_client is not provided.") raise ValueError("Azure credential is required when project_client is not provided.")
project_client = AIProjectClient( project_client_kwargs: dict[str, Any] = {
endpoint=resolved_endpoint, "endpoint": resolved_endpoint,
credential=credential, # type: ignore[arg-type] "credential": credential, # type: ignore[arg-type]
user_agent=AGENT_FRAMEWORK_USER_AGENT, "user_agent": AGENT_FRAMEWORK_USER_AGENT,
) }
if allow_preview is not None:
project_client_kwargs["allow_preview"] = allow_preview
project_client = AIProjectClient(**project_client_kwargs)
self._should_close_client = True self._should_close_client = True
self._project_client = project_client self._project_client = project_client
@@ -199,7 +204,6 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
response_format = opts.get("response_format") response_format = opts.get("response_format")
rai_config = opts.get("rai_config") rai_config = opts.get("rai_config")
reasoning = opts.get("reasoning") reasoning = opts.get("reasoning")
foundry_features = opts.get("foundry_features")
args: dict[str, Any] = {"model": resolved_model} args: dict[str, Any] = {"model": resolved_model}
@@ -246,8 +250,6 @@ class AzureAIProjectAgentProvider(Generic[OptionsCoT]):
"definition": PromptAgentDefinition(**args), "definition": PromptAgentDefinition(**args),
"description": description, "description": description,
} }
if foundry_features:
create_version_kwargs["foundry_features"] = foundry_features
created_agent = await self._project_client.agents.create_version(**create_version_kwargs) created_agent = await self._project_client.agents.create_version(**create_version_kwargs)
@@ -19,9 +19,9 @@ from azure.ai.agents.models import (
from azure.ai.projects.models import ( from azure.ai.projects.models import (
CodeInterpreterTool, CodeInterpreterTool,
MCPTool, MCPTool,
TextResponseFormatConfigurationResponseFormatJsonObject, TextResponseFormatJsonObject,
TextResponseFormatConfigurationResponseFormatText,
TextResponseFormatJsonSchema, TextResponseFormatJsonSchema,
TextResponseFormatText,
Tool, Tool,
WebSearchPreviewTool, WebSearchPreviewTool,
) )
@@ -479,11 +479,7 @@ def _prepare_mcp_tool_dict_for_azure_ai(tool_dict: dict[str, Any]) -> MCPTool:
def create_text_format_config( def create_text_format_config(
response_format: type[BaseModel] | Mapping[str, Any], response_format: type[BaseModel] | Mapping[str, Any],
) -> ( ) -> TextResponseFormatJsonSchema | TextResponseFormatJsonObject | TextResponseFormatText:
TextResponseFormatJsonSchema
| TextResponseFormatConfigurationResponseFormatJsonObject
| TextResponseFormatConfigurationResponseFormatText
):
"""Convert response_format into Azure text format configuration.""" """Convert response_format into Azure text format configuration."""
if isinstance(response_format, type) and issubclass(response_format, BaseModel): if isinstance(response_format, type) and issubclass(response_format, BaseModel):
schema = response_format.model_json_schema() schema = response_format.model_json_schema()
@@ -513,9 +509,9 @@ def create_text_format_config(
config_kwargs["description"] = format_config["description"] config_kwargs["description"] = format_config["description"]
return TextResponseFormatJsonSchema(**config_kwargs) return TextResponseFormatJsonSchema(**config_kwargs)
if format_type == "json_object": if format_type == "json_object":
return TextResponseFormatConfigurationResponseFormatJsonObject() return TextResponseFormatJsonObject()
if format_type == "text": if format_type == "text":
return TextResponseFormatConfigurationResponseFormatText() return TextResponseFormatText()
raise IntegrationInvalidRequestException("response_format must be a Pydantic model or mapping.") raise IntegrationInvalidRequestException("response_format must be a Pydantic model or mapping.")
@@ -28,7 +28,7 @@ from agent_framework.openai._responses_client import RawOpenAIResponsesClient
from azure.ai.projects.aio import AIProjectClient from azure.ai.projects.aio import AIProjectClient
from azure.ai.projects.models import ( from azure.ai.projects.models import (
ApproximateLocation, ApproximateLocation,
CodeInterpreterContainerAuto, AutoCodeInterpreterToolParam,
CodeInterpreterTool, CodeInterpreterTool,
FileSearchTool, FileSearchTool,
ImageGenTool, ImageGenTool,
@@ -1296,7 +1296,7 @@ def test_from_azure_ai_tools_mcp() -> None:
def test_from_azure_ai_tools_code_interpreter() -> None: def test_from_azure_ai_tools_code_interpreter() -> None:
"""Test from_azure_ai_tools with Code Interpreter tool.""" """Test from_azure_ai_tools with Code Interpreter tool."""
ci_tool = CodeInterpreterTool(container=CodeInterpreterContainerAuto(file_ids=["file-1"])) ci_tool = CodeInterpreterTool(container=AutoCodeInterpreterToolParam(file_ids=["file-1"]))
parsed_tools = from_azure_ai_tools([ci_tool]) parsed_tools = from_azure_ai_tools([ci_tool])
assert len(parsed_tools) == 1 assert len(parsed_tools) == 1
assert parsed_tools[0]["type"] == "code_interpreter" assert parsed_tools[0]["type"] == "code_interpreter"
@@ -86,6 +86,7 @@ class TestInit:
provider = FoundryMemoryProvider( provider = FoundryMemoryProvider(
project_endpoint="https://test.project.endpoint", project_endpoint="https://test.project.endpoint",
credential=mock_credential, # type: ignore[arg-type] credential=mock_credential, # type: ignore[arg-type]
allow_preview=True,
memory_store_name="test_store", memory_store_name="test_store",
scope="user_123", scope="user_123",
) )
@@ -93,6 +94,7 @@ class TestInit:
mock_ai_project_client.assert_called_once_with( mock_ai_project_client.assert_called_once_with(
endpoint="https://test.project.endpoint", endpoint="https://test.project.endpoint",
credential=mock_credential, credential=mock_credential,
allow_preview=True,
user_agent=AGENT_FRAMEWORK_USER_AGENT, user_agent=AGENT_FRAMEWORK_USER_AGENT,
) )
@@ -112,9 +112,7 @@ class SkillResource:
self._accepts_kwargs: bool = False self._accepts_kwargs: bool = False
if function is not None: if function is not None:
sig = inspect.signature(function) sig = inspect.signature(function)
self._accepts_kwargs = any( self._accepts_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values())
p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()
)
class Skill: class Skill:
@@ -1458,7 +1458,7 @@ def _update_conversation_id(
if conversation_id is None: if conversation_id is None:
return return
if "chat_options" in kwargs: if "chat_options" in kwargs:
kwargs["chat_options"].conversation_id = conversation_id kwargs["chat_options"]["conversation_id"] = conversation_id
else: else:
kwargs["conversation_id"] = conversation_id kwargs["conversation_id"] = conversation_id
@@ -73,6 +73,7 @@ class AzureOpenAIResponsesClient( # type: ignore[misc]
async_client: AsyncOpenAI | None = None, async_client: AsyncOpenAI | None = None,
project_client: Any | None = None, project_client: Any | None = None,
project_endpoint: str | None = None, project_endpoint: str | None = None,
allow_preview: bool | None = None,
env_file_path: str | None = None, env_file_path: str | None = None,
env_file_encoding: str | None = None, env_file_encoding: str | None = None,
instruction_role: str | None = None, instruction_role: str | None = None,
@@ -120,6 +121,7 @@ class AzureOpenAIResponsesClient( # type: ignore[misc]
project_endpoint: The Azure AI Foundry project endpoint URL. project_endpoint: The Azure AI Foundry project endpoint URL.
When provided with ``credential``, an ``AIProjectClient`` will be created When provided with ``credential``, an ``AIProjectClient`` will be created
and used to obtain the OpenAI client. Requires the ``azure-ai-projects`` package. and used to obtain the OpenAI client. Requires the ``azure-ai-projects`` package.
allow_preview: Enables preview opt-in on internally-created ``AIProjectClient``.
env_file_path: Use the environment settings file as a fallback to using env vars. env_file_path: Use the environment settings file as a fallback to using env vars.
env_file_encoding: The encoding of the environment settings file, defaults to 'utf-8'. env_file_encoding: The encoding of the environment settings file, defaults to 'utf-8'.
instruction_role: The role to use for 'instruction' messages, for example, summarization instruction_role: The role to use for 'instruction' messages, for example, summarization
@@ -189,6 +191,7 @@ class AzureOpenAIResponsesClient( # type: ignore[misc]
project_client=project_client, project_client=project_client,
project_endpoint=project_endpoint, project_endpoint=project_endpoint,
credential=credential, credential=credential,
allow_preview=allow_preview,
) )
azure_openai_settings = load_settings( azure_openai_settings = load_settings(
@@ -246,21 +249,9 @@ class AzureOpenAIResponsesClient( # type: ignore[misc]
project_client: AIProjectClient | None, project_client: AIProjectClient | None,
project_endpoint: str | None, project_endpoint: str | None,
credential: AzureCredentialTypes | AzureTokenProvider | None, credential: AzureCredentialTypes | AzureTokenProvider | None,
allow_preview: bool | None = None,
) -> AsyncOpenAI: ) -> AsyncOpenAI:
"""Create an AsyncOpenAI client from an Azure AI Foundry project. """Create an AsyncOpenAI client from an Azure AI Foundry project."""
Args:
project_client: An existing AIProjectClient to use.
project_endpoint: The Azure AI Foundry project endpoint URL.
credential: Azure credential for authentication.
Returns:
An AsyncAzureOpenAI client obtained from the project client.
Raises:
ValueError: If required parameters are missing or
the azure-ai-projects package is not installed.
"""
if project_client is not None: if project_client is not None:
return project_client.get_openai_client() return project_client.get_openai_client()
@@ -268,11 +259,14 @@ class AzureOpenAIResponsesClient( # type: ignore[misc]
raise ValueError("Azure AI project endpoint is required when project_client is not provided.") raise ValueError("Azure AI project endpoint is required when project_client is not provided.")
if not credential: if not credential:
raise ValueError("Azure credential is required when using project_endpoint without a project_client.") raise ValueError("Azure credential is required when using project_endpoint without a project_client.")
project_client = AIProjectClient( project_client_kwargs: dict[str, Any] = {
endpoint=project_endpoint, "endpoint": project_endpoint,
credential=credential, # type: ignore[arg-type] "credential": credential, # type: ignore[arg-type]
user_agent=AGENT_FRAMEWORK_USER_AGENT, "user_agent": AGENT_FRAMEWORK_USER_AGENT,
) }
if allow_preview is not None:
project_client_kwargs["allow_preview"] = allow_preview
project_client = AIProjectClient(**project_client_kwargs)
return project_client.get_openai_client() return project_client.get_openai_client()
@override @override
@@ -327,7 +327,9 @@ class RawOpenAIChatClient( # type: ignore[misc]
messages = prepend_instructions_to_messages(list(messages), instructions, role="system") messages = prepend_instructions_to_messages(list(messages), instructions, role="system")
# Start with a copy of options # Start with a copy of options
run_options = {k: v for k, v in options.items() if v is not None and k not in {"instructions", "tools"}} run_options = {
k: v for k, v in options.items() if v is not None and k not in {"instructions", "tools", "conversation_id"}
}
# messages # messages
if messages and "messages" not in run_options: if messages and "messages" not in run_options:
+2 -2
View File
@@ -34,7 +34,7 @@ dependencies = [
# connectors and functions # connectors and functions
"openai>=1.99.0", "openai>=1.99.0",
"azure-identity>=1,<2", "azure-identity>=1,<2",
"azure-ai-projects == 2.0.0b4", "azure-ai-projects>=2.0.0,<3.0",
"mcp[ws]>=1.24.0,<2", "mcp[ws]>=1.24.0,<2",
"packaging>=24.1", "packaging>=24.1",
] ]
@@ -55,7 +55,7 @@ all = [
"agent-framework-devui", "agent-framework-devui",
"agent-framework-durabletask", "agent-framework-durabletask",
"agent-framework-foundry-local", "agent-framework-foundry-local",
"agent-framework-github-copilot", "agent-framework-github-copilot; python_version >= '3.11'",
"agent-framework-lab", "agent-framework-lab",
"agent-framework-mem0", "agent-framework-mem0",
"agent-framework-ollama", "agent-framework-ollama",
@@ -626,6 +626,73 @@ async def test_streaming_with_none_delta(
assert any(msg.contents for msg in results) assert any(msg.contents for msg in results)
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_with_conversation_id(
mock_create: AsyncMock,
azure_openai_unit_test_env: dict[str, str],
chat_history: list[Message],
mock_chat_completion_response: ChatCompletion,
) -> None:
"""Test that conversation_id is excluded from the completions create call."""
mock_create.return_value = mock_chat_completion_response
chat_history.append(Message(text="hello world", role="user"))
azure_chat_client = AzureOpenAIChatClient()
await azure_chat_client.get_response(
messages=chat_history,
options={"conversation_id": "12345"},
)
call_kwargs = mock_create.call_args.kwargs
assert "conversation_id" not in call_kwargs
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_streaming_with_conversation_id(
mock_create: AsyncMock,
azure_openai_unit_test_env: dict[str, str],
chat_history: list[Message],
mock_streaming_chat_completion_response: AsyncStream[ChatCompletionChunk],
) -> None:
"""Test that conversation_id is excluded from the streaming completions create call."""
mock_create.return_value = mock_streaming_chat_completion_response
chat_history.append(Message(text="hello world", role="user"))
azure_chat_client = AzureOpenAIChatClient()
async for _ in azure_chat_client.get_response(
messages=chat_history,
options={"conversation_id": "12345"},
stream=True,
):
pass
call_kwargs = mock_create.call_args.kwargs
assert "conversation_id" not in call_kwargs
@patch.object(AsyncChatCompletions, "create", new_callable=AsyncMock)
async def test_cmc_agent_with_service_session_id(
mock_create: AsyncMock,
azure_openai_unit_test_env: dict[str, str],
mock_chat_completion_response: ChatCompletion,
) -> None:
"""Test that agent.run() with a session containing service_session_id works correctly."""
mock_create.return_value = mock_chat_completion_response
azure_chat_client = AzureOpenAIChatClient()
agent = azure_chat_client.as_agent(
name="TestAgent",
instructions="You are a helpful assistant.",
)
session = agent.get_session(service_session_id="12345")
response = await agent.run("hello", session=session)
assert response is not None
call_kwargs = mock_create.call_args.kwargs
assert "conversation_id" not in call_kwargs
@tool(approval_mode="never_require") @tool(approval_mode="never_require")
def get_story_text() -> str: def get_story_text() -> str:
"""Returns a story about Emily and David.""" """Returns a story about Emily and David."""
@@ -3449,3 +3449,66 @@ async def test_streaming_function_calling_response_includes_reasoning_and_tool_r
reasoning_contents = [c for msg in response.messages for c in msg.contents if c.type == "text_reasoning"] reasoning_contents = [c for msg in response.messages for c in msg.contents if c.type == "text_reasoning"]
assert len(reasoning_contents) >= 1 assert len(reasoning_contents) >= 1
assert reasoning_contents[0].id == "rs_test123" assert reasoning_contents[0].id == "rs_test123"
# region _update_conversation_id unit tests
class TestUpdateConversationId:
"""Tests for _update_conversation_id handling dict chat_options."""
def test_chat_options_as_dict(self):
"""When chat_options is a plain dict, conversation_id should be set via key access."""
from agent_framework._tools import _update_conversation_id
kwargs: dict[str, Any] = {"chat_options": {}}
_update_conversation_id(kwargs, "conv_1")
assert kwargs["chat_options"]["conversation_id"] == "conv_1"
def test_chat_options_as_typed_dict(self):
"""When chat_options is a ChatOptions TypedDict, conversation_id should be set via key access."""
from agent_framework import ChatOptions
from agent_framework._tools import _update_conversation_id
opts: ChatOptions = {"temperature": 0.5}
kwargs: dict[str, Any] = {"chat_options": opts}
_update_conversation_id(kwargs, "conv_2")
assert kwargs["chat_options"]["conversation_id"] == "conv_2"
def test_no_chat_options_falls_back_to_kwargs(self):
"""When chat_options is absent, conversation_id should be set directly on kwargs."""
from agent_framework._tools import _update_conversation_id
kwargs: dict[str, Any] = {}
_update_conversation_id(kwargs, "conv_4")
assert kwargs["conversation_id"] == "conv_4"
def test_none_conversation_id_is_noop(self):
"""When conversation_id is None, kwargs should not be modified."""
from agent_framework._tools import _update_conversation_id
kwargs: dict[str, Any] = {"chat_options": {}}
_update_conversation_id(kwargs, None)
assert "conversation_id" not in kwargs["chat_options"]
assert "conversation_id" not in kwargs
def test_options_dict_also_updated(self):
"""The optional options dict should also receive conversation_id."""
from agent_framework._tools import _update_conversation_id
kwargs: dict[str, Any] = {"chat_options": {}}
options: dict[str, Any] = {}
_update_conversation_id(kwargs, "conv_5", options)
assert kwargs["chat_options"]["conversation_id"] == "conv_5"
assert options["conversation_id"] == "conv_5"
def test_dict_overwrites_existing_conversation_id(self):
"""When a dict already has a conversation_id, it should be overwritten."""
from agent_framework._tools import _update_conversation_id
kwargs: dict[str, Any] = {"chat_options": {"conversation_id": "old_id"}}
_update_conversation_id(kwargs, "new_id")
assert kwargs["chat_options"]["conversation_id"] == "new_id"
# endregion
@@ -1161,6 +1161,21 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_t
assert "parallel_tool_calls" not in prepared_options assert "parallel_tool_calls" not in prepared_options
def test_prepare_options_excludes_conversation_id(openai_unit_test_env: dict[str, str]) -> None:
"""Test that conversation_id is excluded from prepared options for chat completions."""
client = OpenAIChatClient()
messages = [Message(role="user", text="test")]
options = {"conversation_id": "12345", "temperature": 0.7}
prepared_options = client._prepare_options(messages, options)
# conversation_id is not a valid parameter for AsyncCompletions.create()
assert "conversation_id" not in prepared_options
# Other options should still be present
assert prepared_options["temperature"] == 0.7
async def test_streaming_exception_handling(openai_unit_test_env: dict[str, str]) -> None: async def test_streaming_exception_handling(openai_unit_test_env: dict[str, str]) -> None:
"""Test that streaming errors are properly handled.""" """Test that streaming errors are properly handled."""
client = OpenAIChatClient() client = OpenAIChatClient()
@@ -544,19 +544,19 @@ class TestFunctionExecutor:
static_wrapped = staticmethod(my_async_func) static_wrapped = staticmethod(my_async_func)
# Direct check on descriptor object fails (this is the bug) # Direct check on descriptor object fails (this is the bug)
assert not asyncio.iscoroutinefunction(static_wrapped) assert not asyncio.iscoroutinefunction(static_wrapped) # type: ignore[reportDeprecated]
assert isinstance(static_wrapped, staticmethod) assert isinstance(static_wrapped, staticmethod)
# But unwrapping __func__ reveals the async function # But unwrapping __func__ reveals the async function
unwrapped = static_wrapped.__func__ unwrapped = static_wrapped.__func__
assert asyncio.iscoroutinefunction(unwrapped) assert asyncio.iscoroutinefunction(unwrapped) # type: ignore[reportDeprecated]
# When accessed via class attribute, Python's descriptor protocol # When accessed via class attribute, Python's descriptor protocol
# automatically unwraps it, so it works: # automatically unwraps it, so it works:
class C: class C:
async_static = static_wrapped async_static = static_wrapped
assert asyncio.iscoroutinefunction(C.async_static) # Works via descriptor protocol assert asyncio.iscoroutinefunction(C.async_static) # type: ignore[reportDeprecated] # Works via descriptor protocol
class TestExecutorExplicitTypes: class TestExecutorExplicitTypes:
@@ -15,11 +15,10 @@ import json
import logging import logging
import uuid import uuid
from abc import abstractmethod from abc import abstractmethod
from collections.abc import Mapping from collections.abc import Callable, Mapping
from dataclasses import dataclass, field from dataclasses import dataclass, field
from inspect import isawaitable from inspect import isawaitable
from typing import Any, cast from typing import Any, cast
from collections.abc import Callable
from agent_framework import ( from agent_framework import (
Content, Content,
@@ -5,8 +5,8 @@
from __future__ import annotations from __future__ import annotations
import re import re
from typing import Any, cast
from collections.abc import Callable from collections.abc import Callable
from typing import Any, cast
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
@@ -26,12 +26,11 @@ from agent_framework._tools import FunctionTool, ToolTypes
from agent_framework._types import AgentRunInputs, normalize_tools from agent_framework._types import AgentRunInputs, normalize_tools
from agent_framework.exceptions import AgentException from agent_framework.exceptions import AgentException
from copilot import CopilotClient, CopilotSession from copilot import CopilotClient, CopilotSession
from copilot.generated.session_events import SessionEvent, SessionEventType from copilot.generated.session_events import PermissionRequest, SessionEvent, SessionEventType
from copilot.types import ( from copilot.types import (
CopilotClientOptions, CopilotClientOptions,
MCPServerConfig, MCPServerConfig,
MessageOptions, MessageOptions,
PermissionRequest,
PermissionRequestResult, PermissionRequestResult,
ResumeSessionConfig, ResumeSessionConfig,
SessionConfig, SessionConfig,
@@ -529,7 +528,7 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
"""Convert an FunctionTool to a Copilot SDK tool.""" """Convert an FunctionTool to a Copilot SDK tool."""
async def handler(invocation: ToolInvocation) -> ToolResult: async def handler(invocation: ToolInvocation) -> ToolResult:
args = invocation.get("arguments", {}) args: dict[str, Any] = invocation.arguments or {}
try: try:
if ai_func.input_model: if ai_func.input_model:
args_instance = ai_func.input_model(**args) args_instance = ai_func.input_model(**args)
@@ -537,13 +536,13 @@ class GitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
else: else:
result = await ai_func.invoke(arguments=args) result = await ai_func.invoke(arguments=args)
return ToolResult( return ToolResult(
textResultForLlm=str(result), text_result_for_llm=str(result),
resultType="success", result_type="success",
) )
except Exception as e: except Exception as e:
return ToolResult( return ToolResult(
textResultForLlm=f"Error: {e}", text_result_for_llm=f"Error: {e}",
resultType="failure", result_type="failure",
error=str(e), error=str(e),
) )
@@ -3,7 +3,7 @@ name = "agent-framework-github-copilot"
description = "GitHub Copilot integration for Microsoft Agent Framework." description = "GitHub Copilot integration for Microsoft Agent Framework."
authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.11"
version = "1.0.0b260304" version = "1.0.0b260304"
license-files = ["LICENSE"] license-files = ["LICENSE"]
urls.homepage = "https://aka.ms/agent-framework" urls.homepage = "https://aka.ms/agent-framework"
@@ -15,7 +15,6 @@ classifiers = [
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.13",
@@ -24,7 +23,7 @@ classifiers = [
] ]
dependencies = [ dependencies = [
"agent-framework-core>=1.0.0rc3", "agent-framework-core>=1.0.0rc3",
"github-copilot-sdk>=0.1.0", "github-copilot-sdk>=0.1.32",
] ]
[tool.uv] [tool.uv]
@@ -66,7 +65,7 @@ include = ["agent_framework_github_copilot"]
[tool.mypy] [tool.mypy]
plugins = ['pydantic.mypy'] plugins = ['pydantic.mypy']
strict = true strict = true
python_version = "3.10" python_version = "3.11"
ignore_missing_imports = true ignore_missing_imports = true
disallow_untyped_defs = true disallow_untyped_defs = true
no_implicit_optional = true no_implicit_optional = true
@@ -16,6 +16,7 @@ from agent_framework import (
) )
from agent_framework.exceptions import AgentException from agent_framework.exceptions import AgentException
from copilot.generated.session_events import Data, SessionEvent, SessionEventType from copilot.generated.session_events import Data, SessionEvent, SessionEventType
from copilot.types import ToolInvocation, ToolResult
from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions from agent_framework_github_copilot import GitHubCopilotAgent, GitHubCopilotOptions
@@ -745,10 +746,11 @@ class TestGitHubCopilotAgentToolConversion:
config = call_args[0][0] config = call_args[0][0]
copilot_tool = config["tools"][0] copilot_tool = config["tools"][0]
result = await copilot_tool.handler({"arguments": {"arg": "test"}}) result = await copilot_tool.handler(ToolInvocation(arguments={"arg": "test"}))
assert result["resultType"] == "success" assert isinstance(result, ToolResult)
assert result["textResultForLlm"] == "Result: test" assert result.result_type == "success"
assert result.text_result_for_llm == "Result: test"
async def test_tool_handler_returns_failure_result_on_error( async def test_tool_handler_returns_failure_result_on_error(
self, self,
@@ -770,11 +772,61 @@ class TestGitHubCopilotAgentToolConversion:
config = call_args[0][0] config = call_args[0][0]
copilot_tool = config["tools"][0] copilot_tool = config["tools"][0]
result = await copilot_tool.handler({"arguments": {"arg": "test"}}) result = await copilot_tool.handler(ToolInvocation(arguments={"arg": "test"}))
assert result["resultType"] == "failure" assert isinstance(result, ToolResult)
assert "Something went wrong" in result["textResultForLlm"] assert result.result_type == "failure"
assert "Something went wrong" in result["error"] assert "Something went wrong" in result.text_result_for_llm
assert "Something went wrong" in result.error
async def test_tool_handler_rejects_raw_dict_invocation(
self,
mock_client: MagicMock,
mock_session: MagicMock,
) -> None:
"""Test that tool handler raises TypeError when called with a raw dict instead of ToolInvocation."""
def my_tool(arg: str) -> str:
"""A test tool."""
return f"Result: {arg}"
agent = GitHubCopilotAgent(client=mock_client, tools=[my_tool])
await agent.start()
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
copilot_tool = config["tools"][0]
with pytest.raises((TypeError, AttributeError)):
await copilot_tool.handler({"arguments": {"arg": "test"}})
async def test_tool_handler_with_empty_arguments(
self,
mock_client: MagicMock,
mock_session: MagicMock,
) -> None:
"""Test that tool handler handles ToolInvocation with empty arguments."""
def no_args_tool() -> str:
"""A tool with no arguments."""
return "no args result"
agent = GitHubCopilotAgent(client=mock_client, tools=[no_args_tool])
await agent.start()
await agent._get_or_create_session(AgentSession()) # type: ignore
call_args = mock_client.create_session.call_args
config = call_args[0][0]
copilot_tool = config["tools"][0]
result = await copilot_tool.handler(ToolInvocation(arguments={}))
assert isinstance(result, ToolResult)
assert result.result_type == "success"
assert result.text_result_for_llm == "no args result"
def test_copilot_tool_passthrough( def test_copilot_tool_passthrough(
self, self,
@@ -784,7 +836,7 @@ class TestGitHubCopilotAgentToolConversion:
from copilot.types import Tool as CopilotTool from copilot.types import Tool as CopilotTool
async def tool_handler(invocation: Any) -> Any: async def tool_handler(invocation: Any) -> Any:
return {"textResultForLlm": "result", "resultType": "success"} return {"text_result_for_llm": "result", "result_type": "success"}
copilot_tool = CopilotTool( copilot_tool = CopilotTool(
name="direct_tool", name="direct_tool",
@@ -813,7 +865,7 @@ class TestGitHubCopilotAgentToolConversion:
return arg return arg
async def tool_handler(invocation: Any) -> Any: async def tool_handler(invocation: Any) -> Any:
return {"textResultForLlm": "result", "resultType": "success"} return {"text_result_for_llm": "result", "result_type": "success"}
copilot_tool = CopilotTool( copilot_tool = CopilotTool(
name="direct_tool", name="direct_tool",
@@ -1,6 +1,6 @@
# Azure AI Agent Examples # Azure AI Agent Examples
This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package. These examples use the `AzureAIClient` with the `azure-ai-projects` 2.x (V2) API surface (see [changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/CHANGELOG.md#200b1-2025-11-11)). For V1 (`azure-ai-agents` 1.x) samples using `AzureAIAgentClient`, see the [Azure AI V1 examples folder](../azure_ai_agent/). This folder contains examples demonstrating different ways to create and use agents with the Azure AI client from the `agent_framework.azure` package. These examples use the `AzureAIClient` with the `azure-ai-projects` 2.x (V2) API surface (see [changelog](https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/ai/azure-ai-projects/CHANGELOG.md#200b1-2025-11-11)). For V1 (`azure-ai-agents` 1.x) samples using `AzureAIAgentClient`, see the [Azure AI V1 examples folder](../azure_ai_agent/). When using preview-only agent creation features on GA SDK versions, create `AIProjectClient` with `allow_preview=True`.
## Examples ## Examples
@@ -15,16 +15,18 @@ SECURITY NOTE: Only enable file permissions when you trust the agent's actions.
import asyncio import asyncio
from agent_framework.github import GitHubCopilotAgent from agent_framework.github import GitHubCopilotAgent
from copilot.types import PermissionRequest, PermissionRequestResult from copilot.generated.session_events import PermissionRequest
from copilot.types import PermissionRequestResult
def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: def prompt_permission(
request: PermissionRequest, context: dict[str, str]
) -> PermissionRequestResult:
"""Permission handler that prompts the user for approval.""" """Permission handler that prompts the user for approval."""
kind = request.get("kind", "unknown") print(f"\n[Permission Request: {request.kind}]")
print(f"\n[Permission Request: {kind}]")
if "path" in request: if request.path is not None:
print(f" Path: {request.get('path')}") print(f" Path: {request.path}")
response = input("Approve? (y/n): ").strip().lower() response = input("Approve? (y/n): ").strip().lower()
if response in ("y", "yes"): if response in ("y", "yes"):
@@ -15,7 +15,8 @@ of MCP-related actions.
import asyncio import asyncio
from agent_framework.github import GitHubCopilotAgent from agent_framework.github import GitHubCopilotAgent
from copilot.types import MCPServerConfig, PermissionRequest, PermissionRequestResult from copilot.generated.session_events import PermissionRequest
from copilot.types import MCPServerConfig, PermissionRequestResult
from dotenv import load_dotenv from dotenv import load_dotenv
# Load environment variables from .env file # Load environment variables from .env file
@@ -24,8 +25,7 @@ load_dotenv()
def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
"""Permission handler that prompts the user for approval.""" """Permission handler that prompts the user for approval."""
kind = request.get("kind", "unknown") print(f"\n[Permission Request: {request.kind}]")
print(f"\n[Permission Request: {kind}]")
response = input("Approve? (y/n): ").strip().lower() response = input("Approve? (y/n): ").strip().lower()
if response in ("y", "yes"): if response in ("y", "yes"):
@@ -21,18 +21,18 @@ More permissions mean more potential for unintended actions.
import asyncio import asyncio
from agent_framework.github import GitHubCopilotAgent from agent_framework.github import GitHubCopilotAgent
from copilot.types import PermissionRequest, PermissionRequestResult from copilot.generated.session_events import PermissionRequest
from copilot.types import PermissionRequestResult
def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
"""Permission handler that prompts the user for approval.""" """Permission handler that prompts the user for approval."""
kind = request.get("kind", "unknown") print(f"\n[Permission Request: {request.kind}]")
print(f"\n[Permission Request: {kind}]")
if "command" in request: if request.full_command_text is not None:
print(f" Command: {request.get('command')}") print(f" Command: {request.full_command_text}")
if "path" in request: if request.path is not None:
print(f" Path: {request.get('path')}") print(f" Path: {request.path}")
response = input("Approve? (y/n): ").strip().lower() response = input("Approve? (y/n): ").strip().lower()
if response in ("y", "yes"): if response in ("y", "yes"):
@@ -14,16 +14,16 @@ Shell commands have full access to your system within the permissions of the run
import asyncio import asyncio
from agent_framework.github import GitHubCopilotAgent from agent_framework.github import GitHubCopilotAgent
from copilot.types import PermissionRequest, PermissionRequestResult from copilot.generated.session_events import PermissionRequest
from copilot.types import PermissionRequestResult
def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
"""Permission handler that prompts the user for approval.""" """Permission handler that prompts the user for approval."""
kind = request.get("kind", "unknown") print(f"\n[Permission Request: {request.kind}]")
print(f"\n[Permission Request: {kind}]")
if "command" in request: if request.full_command_text is not None:
print(f" Command: {request.get('command')}") print(f" Command: {request.full_command_text}")
response = input("Approve? (y/n): ").strip().lower() response = input("Approve? (y/n): ").strip().lower()
if response in ("y", "yes"): if response in ("y", "yes"):
@@ -14,16 +14,16 @@ URL fetching allows the agent to access any URL accessible from your network.
import asyncio import asyncio
from agent_framework.github import GitHubCopilotAgent from agent_framework.github import GitHubCopilotAgent
from copilot.types import PermissionRequest, PermissionRequestResult from copilot.generated.session_events import PermissionRequest
from copilot.types import PermissionRequestResult
def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
"""Permission handler that prompts the user for approval.""" """Permission handler that prompts the user for approval."""
kind = request.get("kind", "unknown") print(f"\n[Permission Request: {request.kind}]")
print(f"\n[Permission Request: {kind}]")
if "url" in request: if request.url is not None:
print(f" URL: {request.get('url')}") print(f" URL: {request.url}")
response = input("Approve? (y/n): ").strip().lower() response = input("Approve? (y/n): ").strip().lower()
if response in ("y", "yes"): if response in ("y", "yes"):
+291 -1178
View File
File diff suppressed because it is too large Load Diff