diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 6dfb902e0a70..0a9b847d4dec 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -36,7 +36,8 @@ Breaking changes in beta classes: ### Sample updates -* Added Hosted Agent creation sample `sample_hosted_agent_create.py`, demonstrating hosted agent version creation and retrieval with `AIProjectClient`. +* Added Hosted Agent creation samples `sample_create_hosted_agent.py` and `sample_create_hosted_agent_async.py`, demonstrating hosted agent version creation and retrieval with `AIProjectClient`. +* Added Hosted Agent code-upload samples `sample_create_hosted_agent_from_code.py` and `sample_create_hosted_agent_from_code_async.py`, demonstrating uploading a code package (zip) as a new hosted agent version. * The Hosted Agent creation sample also demonstrates assigning the hosted agent managed identity the Azure AI User RBAC role on the backing Azure AI account. * Updated the other Hosted Agent samples to reuse an existing Hosted Agent as a prerequisite, instead of creating a new hosted agent version in each sample. * Added Toolbox tool-search sample `sample_toolboxes_with_search_preview.py` and `sample_toolboxes_with_search_preview_async.py`, demonstrating creating a Toolbox version with `ToolboxSearchPreviewTool` and invoking `MCPTool`. diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 8d9d9578ab7d..e1116a1816ae 100644 --- a/sdk/ai/azure-ai-projects/assets.json +++ b/sdk/ai/azure-ai-projects/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "python", "TagPrefix": "python/ai/azure-ai-projects", - "Tag": "python/ai/azure-ai-projects_078d4bf75b" + "Tag": "python/ai/azure-ai-projects_b8c168cd69" } diff --git a/sdk/ai/azure-ai-projects/azure/ai/projects/_utils/utils.py b/sdk/ai/azure-ai-projects/azure/ai/projects/_utils/utils.py index 707b7d8fac75..74ca7f7b13ba 100644 --- a/sdk/ai/azure-ai-projects/azure/ai/projects/_utils/utils.py +++ b/sdk/ai/azure-ai-projects/azure/ai/projects/_utils/utils.py @@ -33,6 +33,17 @@ def prepare_multipart_form_data( body: Mapping[str, Any], multipart_fields: list[str], data_fields: list[str] ) -> list[FileType]: files: list[FileType] = [] + + # Append data fields first so they appear before file parts in the encoded + # multipart body. Some streaming server-side parsers (e.g. the Foundry + # hosted-agents `create_agent_version_from_code` endpoint) require small + # JSON metadata parts to precede large binary file parts; otherwise they + # report the metadata part as missing. + for data_field in data_fields: + data_entry = body.get(data_field) + if data_entry: + files.append((data_field, str(serialize_multipart_data_entry(data_entry)))) + for multipart_field in multipart_fields: multipart_entry = body.get(multipart_field) if isinstance(multipart_entry, list): @@ -40,11 +51,4 @@ def prepare_multipart_form_data( elif multipart_entry: files.append((multipart_field, multipart_entry)) - # if files is empty, sdk core library can't handle multipart/form-data correctly, so - # we put data fields into files with filename as None to avoid that scenario. - for data_field in data_fields: - data_entry = body.get(data_field) - if data_entry: - files.append((data_field, str(serialize_multipart_data_entry(data_entry)))) - return files diff --git a/sdk/ai/azure-ai-projects/post-emitter-fixes.cmd b/sdk/ai/azure-ai-projects/post-emitter-fixes.cmd index 3f55d67e708d..594848cfb591 100644 --- a/sdk/ai/azure-ai-projects/post-emitter-fixes.cmd +++ b/sdk/ai/azure-ai-projects/post-emitter-fixes.cmd @@ -62,6 +62,15 @@ REM Fix Sphinx docutils warnings in get_session_log_stream docstrings (sync + as REM The emitter wraps bullet/code-block lines with insufficient indentation. powershell -Command "$files='azure\ai\projects\operations\_operations.py','azure\ai\projects\aio\operations\_operations.py'; foreach ($f in $files) { $c=Get-Content $f -Raw; $c=$c -replace 'schema\r?\n\s+is not contractual and may include additional keys or change format\r?\n\s+over time [^\r\n]*clients should treat it as an opaque string\)', 'schema is not contractual and may include additional keys or change format over time; clients should treat it as an opaque string)'; $c=$c -replace '(message\":\"Starting)\r?\n\s+(FoundryCBAgent server on port 8088\"})', '$1 $2'; $c=$c -replace '(message\":\"INFO: Application)\r?\n\s+(startup complete\.\"})', '$1 $2'; $c=$c -replace '(message\":\"Successfully)\r?\n\s+(connected to container\"})', '$1 $2'; $c=$c -replace '(message\":\"No logs since)\r?\n\s+(last 60 seconds\"})', '$1 $2'; Set-Content $f $c -NoNewline }" +REM Reorder loops in `prepare_multipart_form_data` (azure\ai\projects\_utils\utils.py). +REM The emitter generates the multipart-file loop before the data-field loop, so JSON +REM metadata parts end up after large binary file parts in the encoded body. Some +REM streaming server-side parsers (e.g. the Foundry hosted-agents +REM `create_agent_version_from_code` endpoint) require the small JSON metadata parts +REM to precede the binary file parts; otherwise they report the metadata part as missing. +REM This rewrite swaps the two loops so data fields are appended first. +powershell -Command "$f='azure\ai\projects\_utils\utils.py'; $c=Get-Content $f -Raw; if ($c -notmatch 'Append data fields first') { $pattern='(?s) files: list\[FileType\] = \[\]\r?\n.*? return files'; $new=(@(' files: list[FileType] = []','',' # Append data fields first so they appear before file parts in the encoded',' # multipart body. Some streaming server-side parsers (e.g. the Foundry',' # hosted-agents `create_agent_version_from_code` endpoint) require small',' # JSON metadata parts to precede large binary file parts; otherwise they',' # report the metadata part as missing.',' for data_field in data_fields:',' data_entry = body.get(data_field)',' if data_entry:',' files.append((data_field, str(serialize_multipart_data_entry(data_entry))))','',' for multipart_field in multipart_fields:',' multipart_entry = body.get(multipart_field)',' if isinstance(multipart_entry, list):',' files.extend([(multipart_field, e) for e in multipart_entry])',' elif multipart_entry:',' files.append((multipart_field, multipart_entry))','',' return files') -join [Environment]::NewLine); $c=[regex]::Replace($c, $pattern, $new); Set-Content $f $c -NoNewline }" + REM Finishing by running 'black' tool to format code. pip install black black --config ../../../eng/black-pyproject.toml . || echo black not found, skipping formatting. diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent-prebuilt.zip b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent-prebuilt.zip new file mode 100644 index 000000000000..99c68e9b0b6b Binary files /dev/null and b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent-prebuilt.zip differ diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent.zip b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent.zip new file mode 100644 index 000000000000..133f1995d33a Binary files /dev/null and b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent.zip differ diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/Dockerfile b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/Dockerfile similarity index 100% rename from sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/Dockerfile rename to sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/Dockerfile diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/README.md b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/README.md similarity index 100% rename from sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/README.md rename to sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/README.md diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/main.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/main.py similarity index 100% rename from sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/main.py rename to sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/main.py diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/requirements.txt b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/requirements.txt new file mode 100644 index 000000000000..baffc50b482c --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/echo-agent/requirements.txt @@ -0,0 +1,3 @@ +azure-ai-agentserver-core==2.0.0b3 +azure-ai-agentserver-invocations==1.0.0b3 +azure-ai-agentserver-responses==1.0.0b5 diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/requirements.txt b/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/requirements.txt deleted file mode 100644 index 65ee9848e259..000000000000 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/assets/responses-echo-agent/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ ---index-url https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/ -azure-ai-agentserver-core==2.0.0a20260410006 -azure-ai-agentserver-invocations==1.0.0a20260410006 -azure-ai-agentserver-responses==1.0.0a20260410006 diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/hosted_agents_util.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/hosted_agents_util.py index 7705776d20fd..ede784f3db33 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/hosted_agents_util.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/hosted_agents_util.py @@ -1,47 +1,99 @@ import asyncio -import logging -from typing import Optional +import hashlib +import time +from pathlib import Path +from typing import Tuple from azure.ai.projects import AIProjectClient from azure.ai.projects.aio import AIProjectClient as AsyncAIProjectClient from azure.ai.projects.models import ( AgentVersionDetails, + CodeDependencyResolution, ) +_ASSETS_DIR = Path(__file__).parent / "assets" + + +def select_echo_agent_code_zip( + use_remote_build: bool, +) -> Tuple[CodeDependencyResolution, str, bytes, str]: + """Pick the dependency-resolution mode and matching echo-agent zip, and load it. + + When ``use_remote_build`` is ``True``, returns REMOTE_BUILD with + ``assets/echo-agent.zip``; otherwise BUNDLED with + ``assets/echo-agent-prebuilt.zip``. + + Reads the zip bytes, computes its SHA-256, and prints a one-line summary. + + Returns ``(dependency_resolution, zip_filename, zip_bytes, zip_sha256)``. + """ + dependency_resolution = ( + CodeDependencyResolution.REMOTE_BUILD if use_remote_build else CodeDependencyResolution.BUNDLED + ) + zip_filename = "echo-agent.zip" if use_remote_build else "echo-agent-prebuilt.zip" + zip_path = _ASSETS_DIR / zip_filename + zip_bytes = zip_path.read_bytes() + zip_sha256 = hashlib.sha256(zip_bytes).hexdigest() + print( + f"Loaded code zip from {zip_path} (dependency_resolution={dependency_resolution.value}): " + f"{len(zip_bytes)} bytes, sha256={zip_sha256}" + ) + return dependency_resolution, zip_filename, zip_bytes, zip_sha256 + + +def wait_for_agent_version_active( + project_client: AIProjectClient, + agent_name: str, + agent_version: str, + *, + max_attempts: int = 60, + poll_interval_seconds: int = 10, +) -> None: + """Poll until the version becomes ``active``; raise on ``failed`` or timeout.""" + print("Waiting for agent version to become active...") + + for attempt in range(max_attempts): + time.sleep(poll_interval_seconds) + version_details = project_client.agents.get_version(agent_name=agent_name, agent_version=agent_version) + status = version_details["status"] + + print(f"Agent version status: {status} (attempt {attempt + 1}/{max_attempts})") + + if status == "active": + print("Agent version is now active") + return + + if status == "failed": + raise RuntimeError(f"Agent version provisioning failed: {dict(version_details)}") + + raise RuntimeError("Timed out waiting for agent version to become active") + async def wait_for_agent_version_active_async( project_client: AsyncAIProjectClient, agent_name: str, agent_version: str, *, - logger: Optional[logging.Logger] = None, max_attempts: int = 60, poll_interval_seconds: int = 10, ) -> None: - if logger: - logger.info("Waiting for agent version to become active...") + """Async variant of :func:`wait_for_agent_version_active`.""" + print("Waiting for agent version to become active...") for attempt in range(max_attempts): await asyncio.sleep(poll_interval_seconds) version_details = await project_client.agents.get_version(agent_name=agent_name, agent_version=agent_version) status = version_details["status"] - if logger: - logger.debug(f"Agent version status: {status} (attempt {attempt + 1}/{max_attempts})") - print(f"Agent version status: {status} (attempt {attempt + 1})") + print(f"Agent version status: {status} (attempt {attempt + 1}/{max_attempts})") if status == "active": - if logger: - logger.info("Agent version is now active") + print("Agent version is now active") return if status == "failed": - if logger: - logger.error(f"Agent version provisioning failed: {dict(version_details)}") raise RuntimeError(f"Agent version provisioning failed: {dict(version_details)}") - if logger: - logger.error("Timed out waiting for agent version to become active") raise RuntimeError("Timed out waiting for agent version to become active") diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/rbac_util.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/rbac_util.py index ce12b18f23d7..40ef3c3cf6a4 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/rbac_util.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/rbac_util.py @@ -3,10 +3,12 @@ from urllib.parse import urlparse from azure.core.credentials import TokenCredential -from azure.core.exceptions import HttpResponseError, ResourceExistsError, ResourceNotFoundError +from azure.core.credentials_async import AsyncTokenCredential +from azure.core.exceptions import ResourceNotFoundError from azure.mgmt.authorization import AuthorizationManagementClient, models as authorization_models +from azure.mgmt.authorization.aio import AuthorizationManagementClient as AsyncAuthorizationManagementClient from azure.mgmt.resource import ResourceManagementClient - +from azure.mgmt.resource.resources.aio import ResourceManagementClient as AsyncResourceManagementClient from azure.ai.projects.models import AgentVersionDetails AZURE_AI_USER_ROLE_DEFINITION_GUID = "53ca6127-db72-4b80-b1b0-d745d6d5456d" @@ -130,3 +132,121 @@ def ensure_agent_identity_rbac( subscription_id=subscription_id, role_id=AZURE_AI_USER_ROLE_DEFINITION_GUID, ) + + +async def _resolve_ai_account_resource_id_async( + credential: AsyncTokenCredential, + account_name: str, + project_name: str, + subscription_id: str, +) -> str: + async with AsyncResourceManagementClient(credential, subscription_id) as resource_client: + project_id_segment = f"/accounts/{account_name}/projects/{project_name}".lower() + matching_projects = [] + async for resource in resource_client.resources.list( + filter="resourceType eq 'Microsoft.CognitiveServices/accounts/projects'" + ): + if resource.id and project_id_segment in resource.id.lower(): + matching_projects.append(resource) + if not matching_projects: + raise RuntimeError( + f"Could not locate Foundry project '{project_name}' in subscription '{subscription_id}'." + ) + + if not matching_projects[0].id: + raise RuntimeError("Foundry project resource ID is empty.") + resource_group_name = _extract_resource_group_name(matching_projects[0].id) + + account_matches = [] + async for resource in resource_client.resources.list_by_resource_group( + resource_group_name=resource_group_name, + filter="resourceType eq 'Microsoft.CognitiveServices/accounts'", + ): + if resource.name == account_name and resource.id: + account_matches.append(resource.id) + if not account_matches: + raise RuntimeError( + f"Could not locate Azure AI account '{account_name}' in resource group '{resource_group_name}'." + ) + return account_matches[0] + + +async def _ensure_agent_identity_rbac_with_role_id_async( + credential: AsyncTokenCredential, + principal_id: str, + scope_resource_id: str, + subscription_id: str, + role_id: str, +) -> tuple[bool, str]: + async with AsyncAuthorizationManagementClient(credential, subscription_id) as authorization_client: + role_definition_id = ( + f"/subscriptions/{subscription_id}/providers/Microsoft.Authorization/roleDefinitions/{role_id}" + ) + role_assignment_name = str( + uuid.uuid5( + uuid.NAMESPACE_URL, + f"{scope_resource_id}|{principal_id}|{role_definition_id}", + ) + ) + + try: + await authorization_client.role_assignments.get(scope_resource_id, role_assignment_name) + print(f"Azure AI User role already assigned to principal {principal_id}.") + return False, role_assignment_name + except ResourceNotFoundError: + pass + + create_parameters_kwargs = cast( + dict[str, Any], + { + "role_definition_id": role_definition_id, + "principal_id": principal_id, + "principal_type": authorization_models.PrincipalType.SERVICE_PRINCIPAL, + }, + ) + parameters = authorization_models.RoleAssignmentCreateParameters(**create_parameters_kwargs) + + await authorization_client.role_assignments.create(scope_resource_id, role_assignment_name, parameters) + print(f"Assigned Azure AI User role to principal {principal_id} at scope {scope_resource_id}.") + return True, role_assignment_name + + +async def ensure_agent_identity_rbac_async( + agent: AgentVersionDetails, + credential: AsyncTokenCredential, + subscription_id: str, + foundry_project_endpoint: str, +) -> None: + """Async variant of :func:`ensure_agent_identity_rbac`. + + :param agent: Agent version details containing ``instance_identity``. + :type agent: ~azure.ai.projects.models.AgentVersionDetails + :param credential: Async credential used for Azure Resource Manager authorization calls. + :type credential: ~azure.core.credentials_async.AsyncTokenCredential + :param subscription_id: Azure subscription ID containing the Foundry project/account. + :type subscription_id: str + :param foundry_project_endpoint: Foundry project endpoint in the format + ``https://.services.ai.azure.com/api/projects/``. + :type foundry_project_endpoint: str + :raises RuntimeError: If the agent identity principal ID is unavailable, or if the + account/project resources cannot be resolved. + :raises ~azure.core.exceptions.HttpResponseError: If role assignment creation fails + for reasons other than an existing assignment. + """ + if not agent.instance_identity or not agent.instance_identity.principal_id: + raise RuntimeError("Agent instance_identity or principal_id is not available.") + principal_id = agent.instance_identity.principal_id + + account_name = urlparse(foundry_project_endpoint).hostname.split(".")[0] # type: ignore[union-attr] + project_name = foundry_project_endpoint.rstrip("/").split("/api/projects/")[1].split("/")[0] + scope_resource_id = await _resolve_ai_account_resource_id_async( + credential, account_name, project_name, subscription_id + ) + + await _ensure_agent_identity_rbac_with_role_id_async( + credential=credential, + principal_id=principal_id, + scope_resource_id=scope_resource_id, + subscription_id=subscription_id, + role_id=AZURE_AI_USER_ROLE_DEFINITION_GUID, + ) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_agent_endpoint.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_agent_endpoint.py index ab873a78060b..82be3e7df1ce 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_agent_endpoint.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_agent_endpoint.py @@ -29,8 +29,8 @@ page of your Microsoft Foundry portal. 2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent. - If you don't have a Hosted Agent, run `sample_hosted_agent_create.py` first - to create one as a prerequisite. + If you don't have a Hosted Agent, run `sample_create_hosted_agent.py` or + `sample_create_hosted_agent_from_code.py` first to create one as a prerequisite. """ import os diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_agent_endpoint_async.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_agent_endpoint_async.py index e2f4ef37d14b..94649b7ad73a 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_agent_endpoint_async.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_agent_endpoint_async.py @@ -29,8 +29,8 @@ page of your Microsoft Foundry portal. 2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent. - If you don't have a Hosted Agent, run `sample_hosted_agent_create.py` first - to create one as a prerequisite. + If you don't have a Hosted Agent, run `sample_create_hosted_agent_async.py` or + `sample_create_hosted_agent_from_code_async.py` first to create one as a prerequisite. """ import asyncio diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_hosted_agent_create.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent.py similarity index 77% rename from sdk/ai/azure-ai-projects/samples/hosted_agents/sample_hosted_agent_create.py rename to sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent.py index 6239a79bf05b..e3e9530626a1 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_hosted_agent_create.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent.py @@ -13,7 +13,7 @@ via `ensure_agent_identity_rbac`. USAGE: - python sample_hosted_agent_create.py + python sample_create_hosted_agent.py Before running the sample: @@ -25,12 +25,12 @@ 2) FOUNDRY_HOSTED_AGENT_NAME - The Hosted Agent name. 3) FOUNDRY_AGENT_CONTAINER_IMAGE - The Hosted Agent container image in the format '/[:|@]'. + You can build a sample image from the `samples/hosted_agents/assets/echo-agent` folder. 4) AZURE_SUBSCRIPTION_ID - Azure subscription ID where the Azure AI account and project are deployed. """ import os -import time from dotenv import load_dotenv @@ -38,6 +38,7 @@ from azure.ai.projects import AIProjectClient from azure.ai.projects.models import HostedAgentDefinition, ProtocolVersionRecord +from hosted_agents_util import wait_for_agent_version_active from rbac_util import ensure_agent_identity_rbac load_dotenv() @@ -48,29 +49,6 @@ subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"] -def wait_for_agent_version_active( - project_client: AIProjectClient, - agent_name: str, - agent_version: str, - max_attempts: int = 60, - poll_interval_seconds: int = 10, -) -> None: - for attempt in range(max_attempts): - time.sleep(poll_interval_seconds) - version_details = project_client.agents.get_version(agent_name=agent_name, agent_version=agent_version) - status = version_details.status - - print(f"Agent version status: {status} (attempt {attempt + 1})") - - if status == "active": - return - - if status == "failed": - raise RuntimeError(f"Agent version provisioning failed: {dict(version_details)}") - - raise RuntimeError("Timed out waiting for agent version to become active") - - with ( DefaultAzureCredential() as credential, AIProjectClient( diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_async.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_async.py new file mode 100644 index 000000000000..fcdf109a15ed --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_async.py @@ -0,0 +1,94 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + Async variant of `sample_create_hosted_agent.py`. Demonstrates CRUD + operations for Hosted Agent versions using the asynchronous AIProjectClient. + + This is the only hosted_agents async sample that sets up agent identity + RBAC via `ensure_agent_identity_rbac_async`. + +USAGE: + python sample_create_hosted_agent_async.py + + Before running the sample: + + pip install "azure-ai-projects>=2.1.0" aiohttp azure-mgmt-authorization azure-mgmt-resource python-dotenv + + Set these environment variables with your own values: + 1) FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint, as found in the Overview + page of your Microsoft Foundry portal. + 2) FOUNDRY_HOSTED_AGENT_NAME - The Hosted Agent name. + 3) FOUNDRY_AGENT_CONTAINER_IMAGE - The Hosted Agent container image in the format + '/[:|@]'. + You can build a sample image from the `samples/hosted_agents/assets/echo-agent` folder. + 4) AZURE_SUBSCRIPTION_ID - Azure subscription ID where the + Azure AI account and project are deployed. +""" + +import asyncio +import os + +from dotenv import load_dotenv + +from azure.identity.aio import DefaultAzureCredential + +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import HostedAgentDefinition, ProtocolVersionRecord +from hosted_agents_util import wait_for_agent_version_active_async +from rbac_util import ensure_agent_identity_rbac_async + + +async def main() -> None: + load_dotenv() + + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] + image = os.environ["FOUNDRY_AGENT_CONTAINER_IMAGE"] + subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"] + + async with ( + DefaultAzureCredential() as credential, + AIProjectClient( + endpoint=endpoint, + credential=credential, + allow_preview=True, + ) as project_client, + ): + created = await project_client.agents.create_version( + agent_name=agent_name, + definition=HostedAgentDefinition( + cpu="0.5", + memory="1Gi", + image=image, + container_protocol_versions=[ + ProtocolVersionRecord(protocol="responses", version="1.0.0"), + ], + ), + metadata={"enableVnextExperience": "true"}, + ) + print(f"Created hosted agent version: {created.version}") + + await wait_for_agent_version_active_async( + project_client=project_client, + agent_name=agent_name, + agent_version=created.version, + ) + + await ensure_agent_identity_rbac_async( + agent=created, + credential=credential, + subscription_id=subscription_id, + foundry_project_endpoint=endpoint, + ) + + fetched = await project_client.agents.get_version(agent_name=agent_name, agent_version=created.version) + print(f"Fetched hosted agent version: {fetched.version}, status: {fetched.status}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code.py new file mode 100644 index 000000000000..afc332e7236e --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code.py @@ -0,0 +1,127 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + Upload a code zip as a new version of a code-based Hosted Agent, + poll for provisioning, and download it back to verify the round-trip. + + The dependency resolution mode is selected via the + `FOUNDRY_HOSTED_AGENT_REMOTE_BUILD` environment variable (default: `false`): + + * `false` (BUNDLED) — uploads `assets/echo-agent-prebuilt.zip`, which + bundles the agent source plus a `packages/` folder with Linux-built + dependencies, so the service skips pip entirely. + * `true` (REMOTE_BUILD) — uploads `assets/echo-agent.zip`, which contains + only the agent source plus `requirements.txt`; the service resolves + dependencies remotely from the public package index. + + The agent must already exist; create it with + `samples/hosted_agents/sample_create_hosted_agent.py`. + +USAGE: + python sample_create_hosted_agent_from_code.py + + Before running the sample: + + pip install "azure-ai-projects>=2.2.0" python-dotenv + + Set these environment variables with your own values: + 1) FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint, as found in the + Overview page of your Microsoft Foundry portal. + 2) FOUNDRY_HOSTED_AGENT_NAME - The Hosted Agent name. Must already exist. + 3) AZURE_SUBSCRIPTION_ID - Azure subscription ID where the Azure AI account + and project are deployed. + 4) FOUNDRY_HOSTED_AGENT_REMOTE_BUILD - Optional. Set to `true` to use + REMOTE_BUILD; defaults to `false` (BUNDLED). +""" + +import hashlib +import os +import tempfile +from pathlib import Path + +from dotenv import load_dotenv + +from azure.identity import DefaultAzureCredential + +from azure.ai.projects import AIProjectClient +from azure.ai.projects.models import ( + CodeConfiguration, + CreateAgentVersionFromCodeContent, + CreateAgentVersionFromCodeMetadata, + HostedAgentDefinition, + ProtocolVersionRecord, +) + +from hosted_agents_util import select_echo_agent_code_zip, wait_for_agent_version_active +from rbac_util import ensure_agent_identity_rbac + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] +agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] +subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"] +use_remote_build = os.environ.get("FOUNDRY_HOSTED_AGENT_REMOTE_BUILD", "false").strip().lower() == "true" + +dependency_resolution, zip_filename, code_zip_bytes, code_zip_sha256 = select_echo_agent_code_zip(use_remote_build) + +with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client, +): + content = CreateAgentVersionFromCodeContent( + metadata=CreateAgentVersionFromCodeMetadata( + description=f"Code-based hosted agent uploaded with dependency_resolution={dependency_resolution.value}.", + definition=HostedAgentDefinition( + cpu="0.5", + memory="1Gi", + code_configuration=CodeConfiguration( + runtime="python_3_12", + entry_point=["python", "main.py"], + dependency_resolution=dependency_resolution, + ), + protocol_versions=[ProtocolVersionRecord(protocol="responses", version="1.0.0")], + ), + ), + code=(zip_filename, code_zip_bytes, "application/zip"), + ) + + created = project_client.beta.agents.create_agent_version_from_code( + agent_name=agent_name, + content=content, + code_zip_sha256=code_zip_sha256, + ) + print(f"Created code-based hosted agent version: {created.version}") + + wait_for_agent_version_active( + project_client=project_client, + agent_name=agent_name, + agent_version=created.version, + ) + + ensure_agent_identity_rbac( + agent=created, + credential=credential, + subscription_id=subscription_id, + foundry_project_endpoint=endpoint, + ) + + # Download the zip for the version we just created, streaming to a temp file. + version_zip_path = Path(tempfile.gettempdir()) / f"{agent_name}-{created.version}.zip" + sha = hashlib.sha256() + with open(version_zip_path, "wb") as f: + for chunk in project_client.beta.agents.download_agent_version_code( + agent_name=agent_name, + agent_version=created.version, + ): + f.write(chunk) + sha.update(chunk) + downloaded_version_sha256 = sha.hexdigest() + print( + f"Downloaded version code zip to {version_zip_path}: {version_zip_path.stat().st_size} bytes, " + f"sha256={downloaded_version_sha256} (matches uploaded: {downloaded_version_sha256 == code_zip_sha256})" + ) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code_async.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code_async.py new file mode 100644 index 000000000000..85b1368f48c1 --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_create_hosted_agent_from_code_async.py @@ -0,0 +1,138 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + Async variant of `sample_create_hosted_agent_from_code.py`. Uploads a code + zip as a new version of a code-based Hosted Agent, polls for provisioning, + and downloads it back to verify the round-trip. + + The dependency resolution mode is selected via the + `FOUNDRY_HOSTED_AGENT_REMOTE_BUILD` environment variable (default: `false`): + + * `false` (BUNDLED) — uploads `assets/echo-agent-prebuilt.zip`, which + bundles the agent source plus a `packages/` folder with Linux-built + dependencies, so the service skips pip entirely. + * `true` (REMOTE_BUILD) — uploads `assets/echo-agent.zip`, which contains + only the agent source plus `requirements.txt`; the service resolves + dependencies remotely from the public package index. + + The agent must already exist; create it with + `samples/hosted_agents/sample_create_hosted_agent_async.py`. + +USAGE: + python sample_create_hosted_agent_from_code_async.py + + Before running the sample: + + pip install "azure-ai-projects>=2.2.0" aiohttp python-dotenv + + Set these environment variables with your own values: + 1) FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint, as found in the + Overview page of your Microsoft Foundry portal. + 2) FOUNDRY_HOSTED_AGENT_NAME - The Hosted Agent name. Must already exist. + 3) AZURE_SUBSCRIPTION_ID - Azure subscription ID where the Azure AI account + and project are deployed. + 4) FOUNDRY_HOSTED_AGENT_REMOTE_BUILD - Optional. Set to `true` to use + REMOTE_BUILD; defaults to `false` (BUNDLED). +""" + +import asyncio +import hashlib +import os +import tempfile +from pathlib import Path + +from dotenv import load_dotenv + +from azure.identity.aio import DefaultAzureCredential + +from azure.ai.projects.aio import AIProjectClient +from azure.ai.projects.models import ( + CodeConfiguration, + CreateAgentVersionFromCodeContent, + CreateAgentVersionFromCodeMetadata, + HostedAgentDefinition, + ProtocolVersionRecord, +) + +from hosted_agents_util import select_echo_agent_code_zip, wait_for_agent_version_active_async +from rbac_util import ensure_agent_identity_rbac_async + + +async def main() -> None: + load_dotenv() + + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] + subscription_id = os.environ["AZURE_SUBSCRIPTION_ID"] + use_remote_build = os.environ.get("FOUNDRY_HOSTED_AGENT_REMOTE_BUILD", "false").strip().lower() == "true" + + dependency_resolution, zip_filename, code_zip_bytes, code_zip_sha256 = select_echo_agent_code_zip(use_remote_build) + + async with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client, + ): + content = CreateAgentVersionFromCodeContent( + metadata=CreateAgentVersionFromCodeMetadata( + description=f"Code-based hosted agent uploaded with dependency_resolution={dependency_resolution.value}.", + definition=HostedAgentDefinition( + cpu="0.5", + memory="1Gi", + code_configuration=CodeConfiguration( + runtime="python_3_12", + entry_point=["python", "main.py"], + dependency_resolution=dependency_resolution, + ), + protocol_versions=[ProtocolVersionRecord(protocol="responses", version="1.0.0")], + ), + ), + code=(zip_filename, code_zip_bytes, "application/zip"), + ) + + created = await project_client.beta.agents.create_agent_version_from_code( + agent_name=agent_name, + content=content, + code_zip_sha256=code_zip_sha256, + ) + print(f"Created code-based hosted agent version: {created.version}") + + await wait_for_agent_version_active_async( + project_client=project_client, + agent_name=agent_name, + agent_version=created.version, + ) + + # ensure_agent_identity_rbac_async uses async ARM management clients with the + # same async credential. + await ensure_agent_identity_rbac_async( + agent=created, + credential=credential, + subscription_id=subscription_id, + foundry_project_endpoint=endpoint, + ) + + # Download the zip for the version we just created, streaming to a temp file. + version_zip_path = Path(tempfile.gettempdir()) / f"{agent_name}-{created.version}.zip" + sha = hashlib.sha256() + version_stream = await project_client.beta.agents.download_agent_version_code( + agent_name=agent_name, + agent_version=created.version, + ) + with open(version_zip_path, "wb") as f: + async for chunk in version_stream: + f.write(chunk) + sha.update(chunk) + downloaded_version_sha256 = sha.hexdigest() + print( + f"Downloaded version code zip to {version_zip_path}: {version_zip_path.stat().st_size} bytes, " + f"sha256={downloaded_version_sha256} (matches uploaded: {downloaded_version_sha256 == code_zip_sha256})" + ) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream.py index 4c495f5e5a22..8957839ec8f9 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream.py @@ -28,8 +28,8 @@ page of your Microsoft Foundry portal. 2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent. - If you don't have a Hosted Agent, run `sample_hosted_agent_create.py` first - to create one as a prerequisite. + If you don't have a Hosted Agent, run `sample_create_hosted_agent.py` or + `sample_create_hosted_agent_from_code.py` first to create one as a prerequisite. NOTE: This sample assumes the Foundry project and Azure AI account are in the same resource group. diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream_async.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream_async.py index 22808cfa27ee..3c2fe3bfa3d9 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream_async.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_session_log_stream_async.py @@ -28,8 +28,8 @@ page of your Microsoft Foundry portal. 2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent. - If you don't have a Hosted Agent, run `sample_hosted_agent_create.py` first - to create one as a prerequisite. + If you don't have a Hosted Agent, run `sample_create_hosted_agent_async.py` or + `sample_create_hosted_agent_from_code_async.py` first to create one as a prerequisite. NOTE: This sample assumes the Foundry project and Azure AI account are in the same resource group. diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_crud.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_crud.py index 310af03e2cdd..5ef49160a3fb 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_crud.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_crud.py @@ -26,8 +26,8 @@ page of your Microsoft Foundry portal. 2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent. - If you don't have a Hosted Agent, run `sample_hosted_agent_create.py` first - to create one as a prerequisite. + If you don't have a Hosted Agent, run `sample_create_hosted_agent.py` or + `sample_create_hosted_agent_from_code.py` first to create one as a prerequisite. SDK FUNCTIONS: - project_client.agents.list_versions: resolves the active version for the existing hosted agent. diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_crud_async.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_crud_async.py index 038c2fe68daa..82362b933488 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_crud_async.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_crud_async.py @@ -26,8 +26,8 @@ page of your Microsoft Foundry portal. 2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent. - If you don't have a Hosted Agent, run `sample_hosted_agent_create.py` first - to create one as a prerequisite. + If you don't have a Hosted Agent, run `sample_create_hosted_agent_async.py` or + `sample_create_hosted_agent_from_code_async.py` first to create one as a prerequisite. SDK FUNCTIONS: - project_client.agents.list_versions: resolves the active version for the existing hosted agent. diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download.py index eda9647b431c..3bc0a932d890 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download.py @@ -26,8 +26,8 @@ page of your Microsoft Foundry portal. 2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent. - If you don't have a Hosted Agent, run `sample_hosted_agent_create.py` first - to create one as a prerequisite. + If you don't have a Hosted Agent, run `sample_create_hosted_agent.py` or + `sample_create_hosted_agent_from_code.py` first to create one as a prerequisite. """ import os diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download_async.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download_async.py index bf1bc228e4b9..cc9ad46eb64f 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download_async.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_sessions_files_upload_download_async.py @@ -26,8 +26,8 @@ page of your Microsoft Foundry portal. 2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent. - If you don't have a Hosted Agent, run `sample_hosted_agent_create.py` first - to create one as a prerequisite. + If you don't have a Hosted Agent, run `sample_create_hosted_agent_async.py` or + `sample_create_hosted_agent_from_code_async.py` first to create one as a prerequisite. """ import asyncio diff --git a/sdk/ai/azure-ai-projects/tests/samples/sample_executor.py b/sdk/ai/azure-ai-projects/tests/samples/sample_executor.py index 4c7d990c2975..9fc9adfa3698 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/sample_executor.py +++ b/sdk/ai/azure-ai-projects/tests/samples/sample_executor.py @@ -242,9 +242,15 @@ def _capture_debug_logs(self): """Capture logger DEBUG output into the same array used for print capture.""" bearer_token_pattern = re.compile(r"(?i)(Bearer\s+)([^\s\"',;]+)") + # Matches raw JWT-like tokens assigned to an "authorization" JSON field + # (e.g., when a sample passes `authorization=token` to MCPTool, the request + # body is logged as `"authorization": "eyJ..."` without a "Bearer " prefix). + json_authorization_pattern = re.compile(r"(?i)(\"authorization\"\s*:\s*\")([^\"]+)(\")") def _sanitize_log_message(message: str) -> str: - return bearer_token_pattern.sub(r"\1", message) + message = bearer_token_pattern.sub(r"\1", message) + message = json_authorization_pattern.sub(r"\1\3", message) + return message class _PrintCaptureLogHandler(logging.Handler): def __init__(self, sink: list[str]): @@ -1044,13 +1050,11 @@ def _resolve_additional_env_vars( resolved: dict[str, str] = {} if _is_live_mode(): - for env_key, _ in playback_values.items(): + for env_key, playback_value in playback_values.items(): live_value = os.environ.get(env_key) if not live_value: - raise ValueError( - f"Missing required environment variable '{env_key}' for live recording of sample '{sample_filename}'. " - "Either set it in your environment/.env file or run in playback mode." - ) + resolved[env_key] = playback_value + continue resolved[env_key] = live_value else: resolved.update(playback_values) diff --git a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py index 92a952615b36..ee3d4d0c7a12 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -181,6 +181,18 @@ def test_chat_completions_samples(self, sample_path: str, **kwargs) -> None: executor.execute() executor.validate_print_calls_by_llm() + @servicePreparer() + @additionalSampleTests( + [ + AdditionalSampleTestDetail( + test_id="sample_create_hosted_agent_from_remote_build", + sample_filename="sample_create_hosted_agent_from_code.py", + env_vars={ + "FOUNDRY_HOSTED_AGENT_REMOTE_BUILD": "true", + }, + ), + ] + ) @pytest.mark.parametrize( "sample_path", get_sample_paths( @@ -188,12 +200,11 @@ def test_chat_completions_samples(self, sample_path: str, **kwargs) -> None: samples_to_skip=[], ), ) - @servicePreparer() @SamplePathPasser() @recorded_by_proxy(RecordedTransport.AZURE_CORE, RecordedTransport.HTTPX) def test_hosted_agents_samples(self, sample_path: str, **kwargs) -> None: - if os.path.basename(sample_path) == "sample_hosted_agent_create.py" and not self.is_live: - pytest.skip("sample_hosted_agent_create.py is skipped in replay mode due to RBAC complications.") + if os.path.basename(sample_path).startswith("sample_create_hosted_agent") and not self.is_live: + pytest.skip("sample_create_hosted_agent.py is skipped in replay mode due to RBAC complications.") env_vars = get_sample_env_vars(kwargs) executor = SyncSampleExecutor(self, sample_path, env_vars=env_vars, **kwargs) executor.execute() diff --git a/sdk/ai/azure-ai-projects/tests/samples/test_samples_async.py b/sdk/ai/azure-ai-projects/tests/samples/test_samples_async.py index 1c742537c5fc..401374346664 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples_async.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples_async.py @@ -3,13 +3,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # ------------------------------------ -import pytest +import pytest, os from devtools_testutils.aio import recorded_by_proxy_async from devtools_testutils import AzureRecordedTestCase, RecordedTransport from test_base import servicePreparer from sample_executor import ( + AdditionalSampleTestDetail, AsyncSampleExecutor, SamplePathPasser, + additionalSampleTests, get_async_sample_paths, ) from test_samples_helpers import get_sample_env_vars @@ -170,6 +172,18 @@ async def test_chat_completions_samples(self, sample_path: str, **kwargs) -> Non await executor.execute_async() await executor.validate_print_calls_by_llm_async() + @servicePreparer() + @additionalSampleTests( + [ + AdditionalSampleTestDetail( + test_id="sample_create_hosted_agent_from_remote_build_async", + sample_filename="sample_create_hosted_agent_from_code_async.py", + env_vars={ + "FOUNDRY_HOSTED_AGENT_REMOTE_BUILD": "true", + }, + ), + ] + ) @pytest.mark.parametrize( "sample_path", get_async_sample_paths( @@ -177,10 +191,11 @@ async def test_chat_completions_samples(self, sample_path: str, **kwargs) -> Non samples_to_skip=[], ), ) - @servicePreparer() @SamplePathPasser() @recorded_by_proxy_async(RecordedTransport.AZURE_CORE, RecordedTransport.HTTPX) async def test_hosted_agents_samples(self, sample_path: str, **kwargs) -> None: + if os.path.basename(sample_path).startswith("sample_create_hosted_agent") and not self.is_live: + pytest.skip("sample_create_hosted_agent.py is skipped in replay mode due to RBAC complications.") env_vars = get_sample_env_vars(kwargs) executor = AsyncSampleExecutor(self, sample_path, env_vars=env_vars, **kwargs) await executor.execute_async()