From 8427585072bfc2a3b80f7521cc390385d515d194 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Fri, 17 Jul 2026 15:30:09 -0700 Subject: [PATCH 1/9] Add sample routines for hosted agents with various triggers --- .../sample_routines_crud.py | 46 ++++- .../sample_routines_with_dispatch.py | 53 ++++- ...mple_routines_with_github_issue_trigger.py | 185 ++++++++++++++++++ .../sample_routines_with_schedule_trigger.py | 53 ++++- .../sample_routines_with_timer_trigger.py | 67 ++++--- 5 files changed, 351 insertions(+), 53 deletions(-) rename sdk/ai/azure-ai-projects/samples/{routines => hosted_agents}/sample_routines_crud.py (64%) rename sdk/ai/azure-ai-projects/samples/{routines => hosted_agents}/sample_routines_with_dispatch.py (65%) create mode 100644 sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py rename sdk/ai/azure-ai-projects/samples/{routines => hosted_agents}/sample_routines_with_schedule_trigger.py (72%) rename sdk/ai/azure-ai-projects/samples/{routines => hosted_agents}/sample_routines_with_timer_trigger.py (66%) diff --git a/sdk/ai/azure-ai-projects/samples/routines/sample_routines_crud.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py similarity index 64% rename from sdk/ai/azure-ai-projects/samples/routines/sample_routines_crud.py rename to sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py index 45aadc1f8b26..273f02dda296 100644 --- a/sdk/ai/azure-ai-projects/samples/routines/sample_routines_crud.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py @@ -9,10 +9,11 @@ This sample demonstrates how to perform CRUD operations on Routines using the synchronous AIProjectClient. - It creates a routine bound to an existing hosted agent, retrieves it, - toggles its `enabled` state via `disable` / `enable`, lists routines, - and finally deletes it. A `CustomRoutineTrigger` is used to keep the - sample self-contained (no GitHub or schedule resources required). + It uploads the basic hosted-agent code from `assets/basic-agent/` as a + temporary hosted-agent version, creates a routine bound to that hosted + agent, retrieves it, toggles its `enabled` state via `disable` / `enable`, + lists routines, and finally deletes it. A `CustomRoutineTrigger` is used + to keep the sample self-contained (no GitHub or schedule resources required). Routines are currently a preview feature. In the Python SDK, you access these operations via `project_client.beta.routines`. @@ -27,12 +28,13 @@ 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 name of an existing Hosted Agent to invoke - when the routine fires. + 2) FOUNDRY_MODEL_NAME - The deployment name of the AI model used by the + temporary hosted agent. + 3) FOUNDRY_HOSTED_AGENT_NAME - Optional. The Hosted Agent name. Defaults to + `MyHostedAgent`. """ import os - from dotenv import load_dotenv from azure.core.exceptions import ResourceNotFoundError @@ -40,16 +42,23 @@ from azure.ai.projects import AIProjectClient from azure.ai.projects.models import ( + CodeConfiguration, CustomRoutineTrigger, + HostedAgentDefinition, InvokeAgentResponsesApiRoutineAction, + ProtocolVersionRecord, Routine, RoutineTrigger, ) +from hosted_agents_util import create_version_from_code, select_basic_agent_code_zip + load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +model_name = os.environ["FOUNDRY_MODEL_NAME"] +dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) def print_routine_state(routine: Routine) -> None: @@ -57,8 +66,29 @@ def print_routine_state(routine: Routine) -> None: with ( + code_zip_stream as code_stream, DefaultAzureCredential() as credential, AIProjectClient(endpoint=endpoint, credential=credential) as project_client, + create_version_from_code( + project_client=project_client, + agent_name=agent_name, + description="Routines CRUD hosted agent uploaded from assets/basic-agent.", + definition=HostedAgentDefinition( + cpu="0.5", + memory="1Gi", + code_configuration=CodeConfiguration( + runtime="python_3_14", + entry_point=["python", "main.py"], + dependency_resolution=dependency_resolution, + ), + environment_variables={ + "FOUNDRY_PROJECT_ENDPOINT": endpoint, + "FOUNDRY_MODEL_NAME": model_name, + }, + protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")], + ), + code=code_stream, + ), ): routine_name = "sample-routine" diff --git a/sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_dispatch.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_dispatch.py similarity index 65% rename from sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_dispatch.py rename to sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_dispatch.py index 1c5164cf00c8..408ffe971161 100644 --- a/sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_dispatch.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_dispatch.py @@ -11,13 +11,15 @@ resulting run by polling `list_runs(...)` using the synchronous AIProjectClient. - The routine is bound to an existing hosted agent. Because the trigger is - a `CustomRoutineTrigger`, the routine never fires on its own; the sample - explicitly invokes it with `project_client.beta.routines.dispatch(...)` - passing an `InvokeAgentResponsesApiDispatchPayload` carrying the input - sent to the agent. The sample then polls the run history until a - terminal phase is reached (or a deadline elapses), printing each - observed transition. The routine is deleted at the end of the sample. + The sample uploads the basic hosted-agent code from `assets/basic-agent/` + as a temporary hosted-agent version and routes the configured hosted agent + name to that version. Because the trigger is a `CustomRoutineTrigger`, the + routine never fires on its own; the sample explicitly invokes it with + `project_client.beta.routines.dispatch(...)` passing an + `InvokeAgentResponsesApiDispatchPayload` carrying the input sent to the + agent. The sample then polls the run history until a terminal phase is + reached (or a deadline elapses), printing each observed transition. The + routine and hosted-agent version are deleted at the end of the sample. Routines are currently a preview feature. In the Python SDK, you access these operations via `project_client.beta.routines`. @@ -32,8 +34,10 @@ 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 name of an existing Hosted Agent to invoke - when the routine is dispatched. + 2) FOUNDRY_MODEL_NAME - The deployment name of the AI model used by the + temporary hosted agent. + 3) FOUNDRY_HOSTED_AGENT_NAME - Optional. The Hosted Agent name. Defaults to + `MyHostedAgent`. """ import json @@ -47,22 +51,51 @@ from azure.ai.projects import AIProjectClient from azure.ai.projects.models import ( + CodeConfiguration, + CodeDependencyResolution, CustomRoutineTrigger, + HostedAgentDefinition, InvokeAgentResponsesApiDispatchPayload, InvokeAgentResponsesApiRoutineAction, + ProtocolVersionRecord, RoutineRun, RoutineRunPhase, ) +from hosted_agents_util import create_version_from_code, select_basic_agent_code_zip + load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +model_name = os.environ["FOUNDRY_MODEL_NAME"] +dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) with ( + code_zip_stream as code_stream, DefaultAzureCredential() as credential, AIProjectClient(endpoint=endpoint, credential=credential) as project_client, + create_version_from_code( + project_client=project_client, + agent_name=agent_name, + description="Routines dispatch hosted agent uploaded from assets/basic-agent.", + definition=HostedAgentDefinition( + cpu="0.5", + memory="1Gi", + code_configuration=CodeConfiguration( + runtime="python_3_14", + entry_point=["python", "main.py"], + dependency_resolution=CodeDependencyResolution.REMOTE_BUILD, + ), + environment_variables={ + "FOUNDRY_PROJECT_ENDPOINT": endpoint, + "FOUNDRY_MODEL_NAME": model_name, + }, + protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")], + ), + code=code_stream, + ), ): routine_name = "sample-routine-dispatch" diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py new file mode 100644 index 000000000000..22de0fc8e272 --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py @@ -0,0 +1,185 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + This sample demonstrates how to create a Routine that fires when a GitHub + issue is opened in a GitHub repository. + + The sample first uploads the basic hosted-agent code from + `samples/hosted_agents/assets/basic-agent/` as a temporary hosted-agent + version, routes the configured hosted agent name to that version, and then + creates a routine configured with a `GitHubIssueRoutineTrigger`. The trigger + uses a GitHub-compatible Foundry RemoteTool connection supplied through + `GITHUB_CONNECTION_NAME`. After creating the routine, open an issue in the + configured repository to fire it. The sample polls the routine run history + for a short period and then deletes the routine and hosted-agent version. + + Routines are currently a preview feature. In the Python SDK, you access + these operations via `project_client.beta.routines`. + +USAGE: + python sample_routines_with_github_issue_trigger.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_MODEL_NAME - The deployment name of the AI model used by the + temporary hosted agent. + 3) FOUNDRY_HOSTED_AGENT_NAME - Optional. The hosted agent name to route to + the temporary uploaded version. Defaults to `MyHostedAgent`. + 4) GITHUB_CONNECTION_NAME - The Foundry GitHub RemoteTool connection name. + The connection must be GitHub-compatible and use PAT or OAuth2 credentials. + 5) GITHUB_USERNAME - The GitHub owner or organization name. + 6) GITHUB_REPOSITORY - The GitHub repository name in the format of https://github.com/xxx/xxx.git. + 7) POLL_INTERVAL_SECONDS - Optional. Seconds to sleep between run-history polls. + Defaults to 10. +""" + +import json +import os +import time + +from dotenv import load_dotenv + +from azure.core.exceptions import ResourceNotFoundError +from azure.identity import DefaultAzureCredential + +from azure.ai.projects import AIProjectClient +from azure.ai.projects.models import ( + CodeConfiguration, + GitHubIssueEvent, + GitHubIssueRoutineTrigger, + HostedAgentDefinition, + InvokeAgentResponsesApiRoutineAction, + ProtocolVersionRecord, + RoutineRun, +) + +from hosted_agents_util import create_version_from_code, select_basic_agent_code_zip + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +model_name = os.environ["FOUNDRY_MODEL_NAME"] +github_connection_name = os.environ["GITHUB_CONNECTION_NAME"] +poll_interval_seconds = int(os.environ.get("POLL_INTERVAL_SECONDS", "10")) + +github_owner = os.environ["GITHUB_USERNAME"] +github_repository = os.environ["GITHUB_REPOSITORY"] + + +def main() -> None: + dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) + + with ( + code_zip_stream as code_stream, + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential) as project_client, + create_version_from_code( + project_client=project_client, + agent_name=agent_name, + description="GitHub issue routine sample hosted agent uploaded from assets/basic-agent.", + definition=HostedAgentDefinition( + cpu="0.5", + memory="1Gi", + code_configuration=CodeConfiguration( + runtime="python_3_14", + entry_point=["python", "main.py"], + dependency_resolution=dependency_resolution, + ), + environment_variables={ + "FOUNDRY_PROJECT_ENDPOINT": endpoint, + "FOUNDRY_MODEL_NAME": model_name, + }, + protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")], + ), + code=code_stream, + ), + ): + routine_name = "sample-routine-github-issue" + + print(f"Preparing routine `{routine_name}` for {github_repository}.") + try: + print(f"Deleting any existing routine `{routine_name}`.") + project_client.beta.routines.delete(routine_name) + print(f"Routine `{routine_name}` deleted") + except ResourceNotFoundError: + pass + + print(f"Creating routine `{routine_name}`.") + created = project_client.beta.routines.create_or_update( + routine_name, + description="Routine used by the GitHub issue trigger sample.", + enabled=True, + triggers={ + "on-issue": GitHubIssueRoutineTrigger( + connection_id=github_connection_name, # currently it accept connection name + owner=github_owner, + repository=github_repository, + issue_event=GitHubIssueEvent.OPENED, + ), + }, + action=InvokeAgentResponsesApiRoutineAction(agent_name=agent_name), + ) + print( + f"Created routine: {created.name} enabled={created.enabled} " + f"repo={github_owner}/{github_repository} event={GitHubIssueEvent.OPENED}" + ) + print(f"Open a GitHub issue in {github_repository} to fire the routine.") + print("Waiting for a routine run for up to 10 minutes...") + + try: + seen_phases: dict[str, str] = {} + final_run: RoutineRun | None = None + run_was_triggered = False + terminal_statuses = {"finished", "failed", "killed"} + + deadline = time.monotonic() + 600 + while time.monotonic() < deadline: + runs = list(project_client.beta.routines.list_runs(routine_name, limit=20, order="desc")) + for run in runs: + run_was_triggered = True + current_phase = str(run.phase) + if seen_phases.get(run.id) == current_phase: + continue + seen_phases[run.id] = current_phase + print( + f" - run_id={run.id} phase={run.phase} status={run.status} " + f"trigger_type={run.trigger_type} triggered_at={run.triggered_at} ended_at={run.ended_at}" + ) + if str(run.status).lower() in terminal_statuses: + final_run = run + + if final_run is not None: + break + time.sleep(poll_interval_seconds) + + if final_run: + print("Final run:") + print(json.dumps(final_run.as_dict(), indent=2, default=str)) + print(f"The response Id is {final_run.response_id}") + elif run_was_triggered: + print("A routine run was observed, but no terminal run state was reached within the deadline.") + else: + print("No GitHub issue-triggered run was observed within the deadline.") + except KeyboardInterrupt: + print("Interrupted by user; cleaning up routine before exiting.") + finally: + try: + project_client.beta.routines.delete(routine_name) + print("Routine deleted") + except ResourceNotFoundError: + pass + + +if __name__ == "__main__": + main() diff --git a/sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_schedule_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_schedule_trigger.py similarity index 72% rename from sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_schedule_trigger.py rename to sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_schedule_trigger.py index 7f3b6404d644..533581ebe8eb 100644 --- a/sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_schedule_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_schedule_trigger.py @@ -10,11 +10,14 @@ recurring cron schedule, then record the resulting runs by polling `list_runs(...)` using the synchronous AIProjectClient. - The routine is bound to an existing hosted agent and scheduled with a + The sample uploads the basic hosted-agent code from `assets/basic-agent/` + as a temporary hosted-agent version, routes the configured hosted agent + name to that version, and schedules the routine with a `ScheduleRoutineTrigger` using a 5-field cron expression. The service enforces a minimum interval of five minutes, so the sample polls the run history for up to ~6 minutes to catch the first fire, prints each - observed phase transition, then deletes the routine. + observed phase transition, then deletes the routine and hosted-agent + version. Routines are currently a preview feature. In the Python SDK, you access these operations via `project_client.beta.routines`. @@ -29,15 +32,19 @@ 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 name of an existing Hosted Agent to invoke - when the routine schedule fires. - 3) POLL_INTERVAL_SECONDS - Optional. Seconds to sleep between run-history polls. - Defaults to 15. + 2) FOUNDRY_MODEL_NAME - The deployment name of the AI model used by the + temporary hosted agent. + 3) FOUNDRY_HOSTED_AGENT_NAME - Optional. The Hosted Agent name. Defaults to + `MyHostedAgent`. + 4) POLL_INTERVAL_SECONDS - Optional. Seconds to sleep between run-history polls. + Defaults to 15. """ import json import os +import sys import time +from pathlib import Path from dotenv import load_dotenv @@ -46,23 +53,55 @@ from azure.ai.projects import AIProjectClient from azure.ai.projects.models import ( + CodeConfiguration, + HostedAgentDefinition, InvokeAgentResponsesApiRoutineAction, + ProtocolVersionRecord, RoutineRun, RoutineRunPhase, ScheduleRoutineTrigger, ) +_HOSTED_AGENTS_DIR = Path(__file__).resolve().parent +if str(_HOSTED_AGENTS_DIR) not in sys.path: + sys.path.insert(0, str(_HOSTED_AGENTS_DIR)) + +from hosted_agents_util import create_version_from_code, select_basic_agent_code_zip + load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +model_name = os.environ["FOUNDRY_MODEL_NAME"] poll_interval_seconds = int(os.environ.get("POLL_INTERVAL_SECONDS", "15")) +dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) def main() -> None: with ( + code_zip_stream as code_stream, DefaultAzureCredential() as credential, AIProjectClient(endpoint=endpoint, credential=credential) as project_client, + create_version_from_code( + project_client=project_client, + agent_name=agent_name, + description="Routines schedule hosted agent uploaded from assets/basic-agent.", + definition=HostedAgentDefinition( + cpu="0.5", + memory="1Gi", + code_configuration=CodeConfiguration( + runtime="python_3_14", + entry_point=["python", "main.py"], + dependency_resolution=dependency_resolution, + ), + environment_variables={ + "FOUNDRY_PROJECT_ENDPOINT": endpoint, + "FOUNDRY_MODEL_NAME": model_name, + }, + protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")], + ), + code=code_stream, + ), ): routine_name = "sample-routine-schedule" diff --git a/sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_timer_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py similarity index 66% rename from sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_timer_trigger.py rename to sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py index 1ef31fb4a108..b9b8256b4d4d 100644 --- a/sdk/ai/azure-ai-projects/samples/routines/sample_routines_with_timer_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py @@ -10,10 +10,12 @@ from a one-shot timer trigger, then record the resulting runs by polling `list_runs(...)` using the synchronous AIProjectClient. - The routine is bound to an existing hosted agent and scheduled to fire a - short time in the future. The sample then polls the run history until a - terminal phase is reached (or a deadline elapses), printing each observed - transition. The routine is deleted at the end of the sample. + The sample uploads the basic hosted-agent code from `assets/basic-agent/` + as a temporary hosted-agent version, routes the configured hosted agent + name to that version, and schedules the routine to fire a short time in + the future. The sample then polls the run history until a terminal phase is + reached (or a deadline elapses), printing each observed transition. The + routine and hosted-agent version are deleted at the end of the sample. Routines are currently a preview feature. In the Python SDK, you access these operations via `project_client.beta.routines`. @@ -28,8 +30,10 @@ 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 name of an existing Hosted Agent to invoke - when the routine timer fires. + 2) FOUNDRY_MODEL_NAME - The deployment name of the AI model used by the + temporary hosted agent. + 3) FOUNDRY_HOSTED_AGENT_NAME - Optional. The Hosted Agent name. Defaults to + `MyHostedAgent`. """ import datetime @@ -40,47 +44,54 @@ from dotenv import load_dotenv from azure.core.exceptions import ResourceNotFoundError -from azure.core.settings import settings - -settings.tracing_implementation = "opentelemetry" -from opentelemetry import trace -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import SimpleSpanProcessor, ConsoleSpanExporter -from azure.monitor.opentelemetry import configure_azure_monitor -from azure.ai.projects.telemetry import AIProjectInstrumentor from azure.identity import DefaultAzureCredential from azure.ai.projects import AIProjectClient from azure.ai.projects.models import ( + CodeConfiguration, + HostedAgentDefinition, InvokeAgentResponsesApiRoutineAction, + ProtocolVersionRecord, RoutineRun, RoutineRunPhase, TimerRoutineTrigger, ) -load_dotenv() +from hosted_agents_util import create_version_from_code, select_basic_agent_code_zip -# Console exporter: spans printed to stdout as they finish. -tracer_provider = TracerProvider() -tracer_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) -trace.set_tracer_provider(tracer_provider) -tracer = trace.get_tracer(__name__) -AIProjectInstrumentor().instrument() +load_dotenv() endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] - +agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") +model_name = os.environ["FOUNDRY_MODEL_NAME"] +dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) with ( + code_zip_stream as code_stream, DefaultAzureCredential() as credential, AIProjectClient(endpoint=endpoint, credential=credential) as project_client, + create_version_from_code( + project_client=project_client, + agent_name=agent_name, + description="Routines timer hosted agent uploaded from assets/basic-agent.", + definition=HostedAgentDefinition( + cpu="0.5", + memory="1Gi", + code_configuration=CodeConfiguration( + runtime="python_3_14", + entry_point=["python", "main.py"], + dependency_resolution=dependency_resolution, + ), + environment_variables={ + "FOUNDRY_PROJECT_ENDPOINT": endpoint, + "FOUNDRY_MODEL_NAME": model_name, + }, + protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")], + ), + code=code_stream, + ), ): - # Azure Monitor exporter: same spans also sent to the Application Insights - # resource attached to the Foundry project, viewable in the "Tracing" tab - # on ai.azure.com. - configure_azure_monitor(connection_string=project_client.telemetry.get_application_insights_connection_string()) - routine_name = "sample-routine-timer" try: From 8fd15077d0d19849f4af7ae369ee1972f765e04f Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 20 Jul 2026 08:31:22 -0700 Subject: [PATCH 2/9] Remove commented-out test for routines samples in test_samples.py --- .../tests/samples/test_samples.py | 32 ------------------- 1 file changed, 32 deletions(-) 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 259029e5a50f..21c1d14859e1 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -363,38 +363,6 @@ def test_toolboxes_samples(self, sample_path: str, **kwargs) -> None: executor.execute() executor.validate_print_calls_by_llm() - @servicePreparer() - # @additionalSampleTests( - # [ - # AdditionalSampleTestDetail( - # test_id="sample_routines_with_schedule_trigger", - # sample_filename="sample_routines_with_schedule_trigger.py", - # env_vars={ - # "POLL_INTERVAL_SECONDS": "300", - # }, - # ), - # ] - # ) - @pytest.mark.parametrize( - "sample_path", - get_sample_paths( - "routines", - samples_to_skip=[ - "sample_routines_with_schedule_trigger.py", # Specify through AdditionalSampleTestDetail - "sample_routines_crud.py", # Skipped due to service serialization issues - "sample_routines_with_timer_trigger.py", # Skipped due to service serialization issues - "sample_routines_with_dispatch.py", # 403: test identity lacks routines/dispatch data-action - ], - ), - ) - @SamplePathPasser() - @recorded_by_proxy(RecordedTransport.AZURE_CORE, RecordedTransport.HTTPX) - def test_routines_samples(self, sample_path: str, **kwargs) -> None: - env_vars = get_sample_env_vars(kwargs) - executor = SyncSampleExecutor(self, sample_path, env_vars=env_vars, **kwargs) - executor.execute() - executor.validate_print_calls_by_llm() - @pytest.mark.parametrize( "sample_path", get_sample_paths( From be89289f65401d122e51778cc687b7c1406135af Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 20 Jul 2026 12:18:02 -0700 Subject: [PATCH 3/9] Update asset tag and add new sample routines to test_samples.py --- sdk/ai/azure-ai-projects/assets.json | 2 +- sdk/ai/azure-ai-projects/tests/samples/test_samples.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 19f1f7e5f9d3..8aa3af13b87e 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_875c4f833f" + "Tag": "python/ai/azure-ai-projects_f47dcaa04b" } 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 21c1d14859e1..3ecc5040a446 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -299,6 +299,8 @@ def test_chat_completions_samples(self, sample_path: str, **kwargs) -> None: "sample_session_log_stream.py", # Specified through AdditionalSampleTestDetail "sample_sessions_crud.py", # Specified through AdditionalSampleTestDetail "sample_sessions_files_upload_download.py", # Specified through AdditionalSampleTestDetail + "sample_routines_with_dispatch.py", # 500 + "sample_routines_with_github_issue_trigger.py", # Cannot run without interact on Github ], ), ) From 3a8347ca9e8607743778c5f5b02f12bae2785573 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 20 Jul 2026 13:33:41 -0700 Subject: [PATCH 4/9] Add new sample routines for scheduled and timer triggers in test_samples.py --- sdk/ai/azure-ai-projects/tests/samples/test_samples.py | 2 ++ 1 file changed, 2 insertions(+) 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 3ecc5040a446..8d84a889a14c 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -300,6 +300,8 @@ def test_chat_completions_samples(self, sample_path: str, **kwargs) -> None: "sample_sessions_crud.py", # Specified through AdditionalSampleTestDetail "sample_sessions_files_upload_download.py", # Specified through AdditionalSampleTestDetail "sample_routines_with_dispatch.py", # 500 + "sample_routines_with_schedule_trigger.py", # 500 + "sample_routines_with_timer_trigger.py", # Timer is used causing request response not matched "sample_routines_with_github_issue_trigger.py", # Cannot run without interact on Github ], ), From 442ec13c3c1989264c31be2e507770a2d1edf192 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 20 Jul 2026 17:36:57 -0700 Subject: [PATCH 5/9] Remove sample routines CRUD script from hosted_agents directory --- .../hosted_agents/sample_routines_crud.py | 141 ------------------ 1 file changed, 141 deletions(-) delete mode 100644 sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py deleted file mode 100644 index 273f02dda296..000000000000 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py +++ /dev/null @@ -1,141 +0,0 @@ -# pylint: disable=line-too-long,useless-suppression -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -""" -DESCRIPTION: - This sample demonstrates how to perform CRUD operations on Routines - using the synchronous AIProjectClient. - - It uploads the basic hosted-agent code from `assets/basic-agent/` as a - temporary hosted-agent version, creates a routine bound to that hosted - agent, retrieves it, toggles its `enabled` state via `disable` / `enable`, - lists routines, and finally deletes it. A `CustomRoutineTrigger` is used - to keep the sample self-contained (no GitHub or schedule resources required). - - Routines are currently a preview feature. In the Python SDK, you access - these operations via `project_client.beta.routines`. - -USAGE: - python sample_routines_crud.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_MODEL_NAME - The deployment name of the AI model used by the - temporary hosted agent. - 3) FOUNDRY_HOSTED_AGENT_NAME - Optional. The Hosted Agent name. Defaults to - `MyHostedAgent`. -""" - -import os -from dotenv import load_dotenv - -from azure.core.exceptions import ResourceNotFoundError -from azure.identity import DefaultAzureCredential - -from azure.ai.projects import AIProjectClient -from azure.ai.projects.models import ( - CodeConfiguration, - CustomRoutineTrigger, - HostedAgentDefinition, - InvokeAgentResponsesApiRoutineAction, - ProtocolVersionRecord, - Routine, - RoutineTrigger, -) - -from hosted_agents_util import create_version_from_code, select_basic_agent_code_zip - -load_dotenv() - -endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] -agent_name = os.environ.get("FOUNDRY_HOSTED_AGENT_NAME", "MyHostedAgent") -model_name = os.environ["FOUNDRY_MODEL_NAME"] -dependency_resolution, code_zip_stream = select_basic_agent_code_zip(True) - - -def print_routine_state(routine: Routine) -> None: - print(f" - routine `{routine.name}` enabled={routine.enabled} description={routine.description!r}") - - -with ( - code_zip_stream as code_stream, - DefaultAzureCredential() as credential, - AIProjectClient(endpoint=endpoint, credential=credential) as project_client, - create_version_from_code( - project_client=project_client, - agent_name=agent_name, - description="Routines CRUD hosted agent uploaded from assets/basic-agent.", - definition=HostedAgentDefinition( - cpu="0.5", - memory="1Gi", - code_configuration=CodeConfiguration( - runtime="python_3_14", - entry_point=["python", "main.py"], - dependency_resolution=dependency_resolution, - ), - environment_variables={ - "FOUNDRY_PROJECT_ENDPOINT": endpoint, - "FOUNDRY_MODEL_NAME": model_name, - }, - protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")], - ), - code=code_stream, - ), -): - - routine_name = "sample-routine" - - try: - project_client.beta.routines.delete(routine_name) - print(f"Routine `{routine_name}` deleted") - except ResourceNotFoundError: - pass - - triggers: dict[str, RoutineTrigger] = { - "manual": CustomRoutineTrigger( - provider="sample-provider", - event_name="sample-event", - parameters={"source": "sample_routines_crud"}, - ), - } - - action = InvokeAgentResponsesApiRoutineAction(agent_name=agent_name) - - created = project_client.beta.routines.create_or_update( - routine_name, - description="Routine created by the azure-ai-projects sample.", - enabled=True, - triggers=triggers, - action=action, - ) - print(f"Created routine: {created.name} enabled={created.enabled}") - - disabled = project_client.beta.routines.disable(routine_name) - print(f"Disabled routine: {disabled.name} enabled={disabled.enabled}") - - fetched = project_client.beta.routines.get(routine_name) - print("Retrieved routine after disable:") - print_routine_state(fetched) - - enabled = project_client.beta.routines.enable(routine_name) - print(f"Enabled routine: {enabled.name} enabled={enabled.enabled}") - - fetched = project_client.beta.routines.get(routine_name) - print("Retrieved routine after enable:") - print_routine_state(fetched) - - routines = list(project_client.beta.routines.list()) - print(f"Found {len(routines)} routine(s):") - for item in routines: - print(f" - {item.name} enabled={item.enabled}") - - project_client.beta.routines.delete(routine_name) - print("Routine deleted") From 73b1a394815a0b951f1edaa0b5e4f418303fb902 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 20 Jul 2026 20:59:15 -0700 Subject: [PATCH 6/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../hosted_agents/sample_routines_with_github_issue_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py index 22de0fc8e272..6107707b2b6d 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py @@ -122,7 +122,7 @@ def main() -> None: enabled=True, triggers={ "on-issue": GitHubIssueRoutineTrigger( - connection_id=github_connection_name, # currently it accept connection name + connection_id=github_connection_name, # Currently accepts a connection name. owner=github_owner, repository=github_repository, issue_event=GitHubIssueEvent.OPENED, From 07882e564844a5de3d93c4088b5b88589dc08573 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 20 Jul 2026 20:59:42 -0700 Subject: [PATCH 7/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../hosted_agents/sample_routines_with_github_issue_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py index 6107707b2b6d..56618a0cc7b3 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py @@ -26,7 +26,7 @@ Before running the sample: - pip install "azure-ai-projects>=2.2.0" python-dotenv + pip install "azure-ai-projects>=2.3.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 From 9f8e64e7595f9f259667df85ebbc19dc013cce00 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Mon, 20 Jul 2026 21:04:40 -0700 Subject: [PATCH 8/9] clean up --- .../hosted_agents/sample_routines_with_github_issue_trigger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py index 56618a0cc7b3..d7a836f83bb3 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_github_issue_trigger.py @@ -35,7 +35,7 @@ temporary hosted agent. 3) FOUNDRY_HOSTED_AGENT_NAME - Optional. The hosted agent name to route to the temporary uploaded version. Defaults to `MyHostedAgent`. - 4) GITHUB_CONNECTION_NAME - The Foundry GitHub RemoteTool connection name. + 4) GITHUB_CONNECTION_NAME - The Foundry GitHub RemoteTool connection name. The connection must be GitHub-compatible and use PAT or OAuth2 credentials. 5) GITHUB_USERNAME - The GitHub owner or organization name. 6) GITHUB_REPOSITORY - The GitHub repository name in the format of https://github.com/xxx/xxx.git. From 743a63d6809c761e4c5b25e46b41999c6276c2bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Jul 2026 04:05:55 +0000 Subject: [PATCH 9/9] Fix azure-ai-projects README routines sample path Co-authored-by: howieleung <177042912+howieleung@users.noreply.github.com> --- sdk/ai/azure-ai-projects/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/README.md b/sdk/ai/azure-ai-projects/README.md index 67fa1864a635..ff4feb4c87c5 100644 --- a/sdk/ai/azure-ai-projects/README.md +++ b/sdk/ai/azure-ai-projects/README.md @@ -186,7 +186,7 @@ The table below lists the operation groups supported by the client library, with | Models (preview) | | `samples/models/` | | Red teams (preview) | | `samples/red_team/` | | Responses | [Responses API](https://platform.openai.com/docs/api-reference/responses) | `samples/responses/` | -| Routines (preview) | [Routines overview](https://learn.microsoft.com/azure/foundry/agents/concepts/routines) | `samples/routines/` | +| Routines (preview) | [Routines overview](https://learn.microsoft.com/azure/foundry/agents/concepts/routines) | `samples/hosted_agents/` | | Sessions | [Manage hosted sessions](https://learn.microsoft.com/azure/foundry/agents/how-to/manage-hosted-sessions?pivots=python) | `samples/hosted_agents/` | | Skills (preview) | | `samples/skills/` | | Toolboxes | [Curate intent-based toolbox in Foundry](https://learn.microsoft.com/azure/foundry/agents/how-to/tools/toolbox?pivots=python) | `samples/hosted_agents/`, `samples/toolboxes/` |