From 1d0d54838f14aacdecefe427c5e0477f2ffc80f7 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Fri, 29 May 2026 09:36:32 -0700 Subject: [PATCH 1/6] samples --- .../hosted_agents/sample_skill_in_toolbox.py | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py new file mode 100644 index 000000000000..f609d6a0b805 --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py @@ -0,0 +1,163 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + This sample demonstrates how to invoke a Skill packaged inside a Toolbox + from a Prompt Agent response, using the synchronous AIProjectClient and + the OpenAI-compatible client. + + It creates a Skill with inline content describing how to compute + shipping cost, then creates a Toolbox version that references the skill + (the only tool in the toolbox is `ToolboxSearchPreviewTool`, which + exposes the toolbox over its versioned `/mcp` endpoint). A Prompt Agent + is created with an `MCPTool` pointed at that `/mcp` URL, and + `openai_client.responses.create` is called with a question that mentions + the skill's domain so the agent's `tool_search` query matches the skill + text. + + Skills and Toolboxes are currently preview features. In the Python SDK, + you access these operations via `project_client.beta.skills` and + `project_client.beta.toolboxes`. + +USAGE: + python sample_skill_in_toolbox.py + + Before running the sample: + + pip install "azure-ai-projects>=2.2.0" python-dotenv openai + + 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, as found under + the "Name" column in the "Models + endpoints" tab in your Microsoft + Foundry project. +""" + +import os + +from dotenv import load_dotenv +from openai import BadRequestError + +from azure.core.exceptions import ResourceNotFoundError +from azure.identity import DefaultAzureCredential + +from azure.ai.projects import AIProjectClient +from azure.ai.projects.models import ( + MCPTool, + PromptAgentDefinition, + SkillInlineContent, + ToolboxSearchPreviewTool, + ToolboxSkillReference, +) + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + +SKILL_NAME = "shipping-cost-skill" +TOOLBOX_NAME = "toolbox_with_skill" +AGENT_NAME = "SkillToolboxAgent" +TOOLBOX_MCP_LABEL = "skill-toolbox" + + +with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client, + project_client.get_openai_client() as openai_client, +): + + try: + project_client.beta.toolboxes.delete(TOOLBOX_NAME) + print(f"Toolbox `{TOOLBOX_NAME}` deleted") + except ResourceNotFoundError: + pass + + try: + project_client.beta.skills.delete(SKILL_NAME) + print(f"Skill `{SKILL_NAME}` deleted") + except ResourceNotFoundError: + pass + + skill_version = project_client.beta.skills.create( + name=SKILL_NAME, + inline_content=SkillInlineContent( + description="Compute shipping cost for a package given weight and destination.", + instructions=( + "You are a shipping cost calculator. When asked to compute " + "shipping cost, use this formula: cost (USD) = 5 + 2 * weight_kg " + "for domestic destinations, and cost (USD) = 15 + 4 * weight_kg " + "for international destinations. Always state the formula you used." + ), + metadata={"revision": "1"}, + ), + ) + print(f"Created skill: {skill_version.name} version={skill_version.version}") + + toolbox_version = project_client.beta.toolboxes.create_version( + name=TOOLBOX_NAME, + description="Toolbox that exposes the shipping-cost kill via /mcp.", + tools=[ToolboxSearchPreviewTool()], + skills=[ + ToolboxSkillReference(name=skill_version.name, version=skill_version.version), + ], + ) + print(f"Created toolbox: {toolbox_version.name} version={toolbox_version.version}") + + toolbox_mcp_url = f"{endpoint}/toolboxes/{TOOLBOX_NAME}/versions/{toolbox_version.version}/mcp?api-version=v1" + token = credential.get_token("https://ai.azure.com/.default").token + + toolbox_mcp_tool = MCPTool( + server_label=TOOLBOX_MCP_LABEL, + server_url=toolbox_mcp_url, + authorization=token, + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + require_approval="never", + ) + + agent = project_client.agents.create_version( + agent_name=AGENT_NAME, + definition=PromptAgentDefinition( + model=os.environ["FOUNDRY_MODEL_NAME"], + instructions=( + "You help customers compute shipping costs. The connected " + "toolbox exposes a shipping-cost skill. Always start by calling " + "`tool_search` with a query like 'shipping-cost-skill' to locate the " + "skill, then `call_tool` to invoke it before answering." + ), + tools=[toolbox_mcp_tool], + ), + ) + print(f"Agent created (id={agent.id}, name={agent.name}, version={agent.version})") + + user_input = "Compute the shipping cost for a 3 kg package shipped domestically." + print(f"User: {user_input}") + response = openai_client.responses.create( + input=user_input, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + + for item in response.output: + if item.type == "mcp_list_tools": + print(f"mcp_list_tools server_label={item.server_label} tools={[t.name for t in (item.tools or [])]}") + elif item.type == "mcp_call": + print(f"mcp_call server_label={item.server_label} name={item.name} error={item.error}") + if getattr(item, "output", None): + print(f" output: {item.output}") + elif item.type == "mcp_approval_request": + print(f"mcp_approval_request server_label={item.server_label} name={item.name}") + else: + print(f"output item type={item.type}") + + print(f"Response: {response.output_text}") + + project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) + print(f"Agent version {agent.version} deleted") + project_client.beta.toolboxes.delete(TOOLBOX_NAME) + print("Toolbox deleted") + project_client.beta.skills.delete(SKILL_NAME) + print("Skill deleted") From b629d89d31c48d78b3be3210eacd08638802d2ab Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Fri, 29 May 2026 15:12:49 -0700 Subject: [PATCH 2/6] routine samples --- sdk/ai/azure-ai-projects/CHANGELOG.md | 1 + .../hosted_agents/sample_routines_crud.py | 111 +++++++++++++++++ .../sample_routines_with_timer_trigger.py | 116 ++++++++++++++++++ .../tests/samples/test_samples.py | 5 +- 4 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py create mode 100644 sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py diff --git a/sdk/ai/azure-ai-projects/CHANGELOG.md b/sdk/ai/azure-ai-projects/CHANGELOG.md index 519ab8a6b5c8..e832d414beea 100644 --- a/sdk/ai/azure-ai-projects/CHANGELOG.md +++ b/sdk/ai/azure-ai-projects/CHANGELOG.md @@ -60,6 +60,7 @@ Breaking changes in beta classes: * 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 Routines samples `sample_routines_crud.py` demonstring CRUD operations and `sample_routines_with_timer_trigger.py` trigger a routine by a timer. * 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`. * Added `.beta.models` samples under `samples/models/`: * `sample_models_basic.py` — synchronous end-to-end registration via the `create` helper (uses `azcopy`), followed by `get`, `list_versions`, `list`, `get_credentials`, `update`, and `delete`. 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 new file mode 100644 index 000000000000..f71a74b33b48 --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_crud.py @@ -0,0 +1,111 @@ +# 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 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). + + 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_HOSTED_AGENT_NAME - The name of an existing Hosted Agent to invoke + when the routine fires. +""" + +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 ( + CustomRoutineTrigger, + InvokeAgentResponsesApiRoutineAction, + Routine, + RoutineTrigger, +) + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] +agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] + + +def print_routine_state(routine: Routine) -> None: + print(f" - routine `{routine.name}` enabled={routine.enabled} description={routine.description!r}") + + +with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client, +): + + 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") diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py new file mode 100644 index 000000000000..b2d6504745cc --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py @@ -0,0 +1,116 @@ +# 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 automatically + 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. + + Routines are currently a preview feature. In the Python SDK, you access + these operations via `project_client.beta.routines`. + +USAGE: + python sample_routines_with_timer_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_HOSTED_AGENT_NAME - The name of an existing Hosted Agent to invoke + when the routine timer fires. +""" + +import datetime +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 ( + InvokeAgentResponsesApiRoutineAction, + RoutineRun, + RoutineRunPhase, + TimerRoutineTrigger, +) + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] +agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] + + +with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client, +): + + routine_name = "sample-routine-timer" + + try: + project_client.beta.routines.delete(routine_name) + print(f"Routine `{routine_name}` deleted") + except ResourceNotFoundError: + pass + + fire_at = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=20) + created = project_client.beta.routines.create_or_update( + routine_name, + description="Routine used by the timer-trigger sample.", + enabled=True, + triggers={"once": TimerRoutineTrigger(at=fire_at)}, + action=InvokeAgentResponsesApiRoutineAction(agent_name=agent_name), + ) + print(f"Created routine: {created.name} enabled={created.enabled} fire_at={fire_at.isoformat()}") + + terminal_phases = {RoutineRunPhase.COMPLETED, RoutineRunPhase.FAILED} + seen_phases: dict[str, RoutineRunPhase] = {} + final_run: RoutineRun | None = None + + deadline = time.monotonic() + 180 + while time.monotonic() < deadline: + runs = list(project_client.beta.routines.list_runs(routine_name, limit=20, order="desc")) + for run in runs: + if run.id is None: + continue + if seen_phases.get(run.id) == run.phase: + continue + seen_phases[run.id] = run.phase # type: ignore[assignment] + 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() == "finished": + final_run = run + + if final_run is not None: + break + time.sleep(5) + + if final_run: + print("Final run:") + print(json.dumps(final_run.as_dict(), indent=2, default=str)) + # Note: retrieving the response body produced by a routine-dispatched + # run via `openai_client.responses.retrieve(final_run.response_id)` is + # not yet supported by the service for this scenario. + else: + print("Timer did not produce a terminal run within the deadline.") + + project_client.beta.routines.delete(routine_name) + print("Routine deleted") 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 598e9e3e564e..29e26e111d53 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -250,7 +250,10 @@ def test_chat_completions_samples(self, sample_path: str, **kwargs) -> None: "sample_path", get_sample_paths( "hosted_agents", - samples_to_skip=[], + samples_to_skip=[ + "sample_routines_dispatch_and_runs.py", # Skipped due to service serialization issues + "sample_routines_with_timer_trigger.py", # Skipped due to service serialization issues + ], ), ) @SamplePathPasser() From 4d388f78be23f5891b0d36e0e50734900672292f Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Fri, 29 May 2026 15:29:13 -0700 Subject: [PATCH 3/6] Remove sample_skill_in_toolbox.py as part of cleanup --- .../hosted_agents/sample_skill_in_toolbox.py | 163 ------------------ 1 file changed, 163 deletions(-) delete mode 100644 sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py deleted file mode 100644 index f609d6a0b805..000000000000 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py +++ /dev/null @@ -1,163 +0,0 @@ -# pylint: disable=line-too-long,useless-suppression -# ------------------------------------ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. -# ------------------------------------ - -""" -DESCRIPTION: - This sample demonstrates how to invoke a Skill packaged inside a Toolbox - from a Prompt Agent response, using the synchronous AIProjectClient and - the OpenAI-compatible client. - - It creates a Skill with inline content describing how to compute - shipping cost, then creates a Toolbox version that references the skill - (the only tool in the toolbox is `ToolboxSearchPreviewTool`, which - exposes the toolbox over its versioned `/mcp` endpoint). A Prompt Agent - is created with an `MCPTool` pointed at that `/mcp` URL, and - `openai_client.responses.create` is called with a question that mentions - the skill's domain so the agent's `tool_search` query matches the skill - text. - - Skills and Toolboxes are currently preview features. In the Python SDK, - you access these operations via `project_client.beta.skills` and - `project_client.beta.toolboxes`. - -USAGE: - python sample_skill_in_toolbox.py - - Before running the sample: - - pip install "azure-ai-projects>=2.2.0" python-dotenv openai - - 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, as found under - the "Name" column in the "Models + endpoints" tab in your Microsoft - Foundry project. -""" - -import os - -from dotenv import load_dotenv -from openai import BadRequestError - -from azure.core.exceptions import ResourceNotFoundError -from azure.identity import DefaultAzureCredential - -from azure.ai.projects import AIProjectClient -from azure.ai.projects.models import ( - MCPTool, - PromptAgentDefinition, - SkillInlineContent, - ToolboxSearchPreviewTool, - ToolboxSkillReference, -) - -load_dotenv() - -endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] - -SKILL_NAME = "shipping-cost-skill" -TOOLBOX_NAME = "toolbox_with_skill" -AGENT_NAME = "SkillToolboxAgent" -TOOLBOX_MCP_LABEL = "skill-toolbox" - - -with ( - DefaultAzureCredential() as credential, - AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client, - project_client.get_openai_client() as openai_client, -): - - try: - project_client.beta.toolboxes.delete(TOOLBOX_NAME) - print(f"Toolbox `{TOOLBOX_NAME}` deleted") - except ResourceNotFoundError: - pass - - try: - project_client.beta.skills.delete(SKILL_NAME) - print(f"Skill `{SKILL_NAME}` deleted") - except ResourceNotFoundError: - pass - - skill_version = project_client.beta.skills.create( - name=SKILL_NAME, - inline_content=SkillInlineContent( - description="Compute shipping cost for a package given weight and destination.", - instructions=( - "You are a shipping cost calculator. When asked to compute " - "shipping cost, use this formula: cost (USD) = 5 + 2 * weight_kg " - "for domestic destinations, and cost (USD) = 15 + 4 * weight_kg " - "for international destinations. Always state the formula you used." - ), - metadata={"revision": "1"}, - ), - ) - print(f"Created skill: {skill_version.name} version={skill_version.version}") - - toolbox_version = project_client.beta.toolboxes.create_version( - name=TOOLBOX_NAME, - description="Toolbox that exposes the shipping-cost kill via /mcp.", - tools=[ToolboxSearchPreviewTool()], - skills=[ - ToolboxSkillReference(name=skill_version.name, version=skill_version.version), - ], - ) - print(f"Created toolbox: {toolbox_version.name} version={toolbox_version.version}") - - toolbox_mcp_url = f"{endpoint}/toolboxes/{TOOLBOX_NAME}/versions/{toolbox_version.version}/mcp?api-version=v1" - token = credential.get_token("https://ai.azure.com/.default").token - - toolbox_mcp_tool = MCPTool( - server_label=TOOLBOX_MCP_LABEL, - server_url=toolbox_mcp_url, - authorization=token, - headers={"Foundry-Features": "Toolboxes=V1Preview"}, - require_approval="never", - ) - - agent = project_client.agents.create_version( - agent_name=AGENT_NAME, - definition=PromptAgentDefinition( - model=os.environ["FOUNDRY_MODEL_NAME"], - instructions=( - "You help customers compute shipping costs. The connected " - "toolbox exposes a shipping-cost skill. Always start by calling " - "`tool_search` with a query like 'shipping-cost-skill' to locate the " - "skill, then `call_tool` to invoke it before answering." - ), - tools=[toolbox_mcp_tool], - ), - ) - print(f"Agent created (id={agent.id}, name={agent.name}, version={agent.version})") - - user_input = "Compute the shipping cost for a 3 kg package shipped domestically." - print(f"User: {user_input}") - response = openai_client.responses.create( - input=user_input, - extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, - ) - - for item in response.output: - if item.type == "mcp_list_tools": - print(f"mcp_list_tools server_label={item.server_label} tools={[t.name for t in (item.tools or [])]}") - elif item.type == "mcp_call": - print(f"mcp_call server_label={item.server_label} name={item.name} error={item.error}") - if getattr(item, "output", None): - print(f" output: {item.output}") - elif item.type == "mcp_approval_request": - print(f"mcp_approval_request server_label={item.server_label} name={item.name}") - else: - print(f"output item type={item.type}") - - print(f"Response: {response.output_text}") - - project_client.agents.delete_version(agent_name=agent.name, agent_version=agent.version) - print(f"Agent version {agent.version} deleted") - project_client.beta.toolboxes.delete(TOOLBOX_NAME) - print("Toolbox deleted") - project_client.beta.skills.delete(SKILL_NAME) - print("Skill deleted") From 399a3be93aba33fb0780215966611a629e264816 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Fri, 29 May 2026 17:13:47 -0700 Subject: [PATCH 4/6] update samples --- .../sample_routines_with_timer_trigger.py | 20 +++ .../hosted_agents/sample_skill_in_toolbox.py | 155 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py index b2d6504745cc..74b12ab4e775 100644 --- a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py @@ -40,6 +40,15 @@ 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 @@ -52,6 +61,13 @@ load_dotenv() +# 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() + endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"] @@ -60,6 +76,10 @@ DefaultAzureCredential() as credential, AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client, ): + # 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" diff --git a/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py new file mode 100644 index 000000000000..af07a47bae37 --- /dev/null +++ b/sdk/ai/azure-ai-projects/samples/hosted_agents/sample_skill_in_toolbox.py @@ -0,0 +1,155 @@ +# pylint: disable=line-too-long,useless-suppression +# ------------------------------------ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# ------------------------------------ + +""" +DESCRIPTION: + This sample demonstrates how to expose a Skill to a Prompt Agent via a + Toolbox, using the synchronous AIProjectClient and the OpenAI-compatible + client. + + It creates a Skill with inline content describing how to compute shipping + cost, then creates a Toolbox version that references the skill. A Prompt + Agent is created with an `MCPTool` pointed at the toolbox's versioned + `/mcp` endpoint. The skill's instructions are injected into the agent's + context, so when asked a shipping-cost question the agent answers directly + using the skill's formula. + + Skills and Toolboxes are currently preview features. In the Python SDK, + you access these operations via `project_client.beta.skills` and + `project_client.beta.toolboxes`. + +USAGE: + python sample_skill_in_toolbox.py + + Before running the sample: + + pip install "azure-ai-projects>=2.2.0" python-dotenv openai + + 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, as found under + the "Name" column in the "Models + endpoints" tab in your Microsoft + Foundry project. +""" + +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 ( + MCPTool, + PromptAgentDefinition, + SkillInlineContent, + ToolboxSearchPreviewTool, + ToolboxSkillReference, +) + +load_dotenv() + +endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] + +SKILL_NAME = "shipping-cost-skill" +TOOLBOX_NAME = "toolbox_with_skill" +AGENT_NAME = "SkillToolboxAgent" + + +with ( + DefaultAzureCredential() as credential, + AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client, + project_client.get_openai_client() as openai_client, +): + + try: + project_client.beta.toolboxes.delete(TOOLBOX_NAME) + except ResourceNotFoundError: + pass + + try: + project_client.beta.skills.delete(SKILL_NAME) + except ResourceNotFoundError: + pass + + skill_version = project_client.beta.skills.create( + name=SKILL_NAME, + inline_content=SkillInlineContent( + description="Compute shipping cost for a package given weight and destination.", + instructions=( + "You are a shipping cost calculator. When asked to compute " + "shipping cost, use this formula: cost (USD) = 5 + 2 * weight_kg " + "for domestic destinations, and cost (USD) = 15 + 4 * weight_kg " + "for international destinations. Always state the formula you used." + ), + metadata={"revision": "1"}, + ), + ) + print(f"Created skill: {skill_version.name} version={skill_version.version}") + + toolbox_version = project_client.beta.toolboxes.create_version( + name=TOOLBOX_NAME, + description="Toolbox exposing a shipping-cost skill.", + tools=[ToolboxSearchPreviewTool()], + skills=[ToolboxSkillReference(name=skill_version.name, version=skill_version.version)], + ) + print(f"Created toolbox: {toolbox_version.name} version={toolbox_version.version}") + + toolbox_mcp_url = f"{endpoint}/toolboxes/{TOOLBOX_NAME}/versions/{toolbox_version.version}/mcp?api-version=v1" + token = credential.get_token("https://ai.azure.com/.default").token + + toolbox_mcp_tool = MCPTool( + server_label="skill-toolbox", + server_url=toolbox_mcp_url, + authorization=token, + headers={"Foundry-Features": "Toolboxes=V1Preview"}, + require_approval="never", + ) + + agent = project_client.agents.create_version( + agent_name=AGENT_NAME, + definition=PromptAgentDefinition( + model=os.environ["FOUNDRY_MODEL_NAME"], + instructions=( + "Answer the user using the `shipping-cost-skill` instructions " + "available in your context. Do not call `tool_search`; the " + "skill rules are already part of your knowledge. Apply the " + "skill's formula exactly as given and state the formula in " + "your answer." + ), + temperature=0, + tools=[toolbox_mcp_tool], + ), + ) + print(f"Agent created (id={agent.id}, name={agent.name}, version={agent.version})") + + user_input = "Compute the shipping cost for a 3 kg package shipped domestically." + print(f"User: {user_input}") + response = openai_client.responses.create( + input=user_input, + extra_body={"agent_reference": {"name": agent.name, "type": "agent_reference"}}, + ) + + for item in response.output: + if item.type == "mcp_list_tools": + print(f"mcp_list_tools server_label={item.server_label} tools={[t.name for t in (item.tools or [])]}") + elif item.type == "mcp_call": + print(f"mcp_call server_label={item.server_label} name={item.name} error={item.error}") + if getattr(item, "output", None): + print(f" output: {item.output}") + elif item.type == "mcp_approval_request": + print(f"mcp_approval_request server_label={item.server_label} name={item.name}") + else: + print(f"output item type={item.type}") + + print(f"Response: {response.output_text}") + + project_client.beta.toolboxes.delete(TOOLBOX_NAME) + print("Toolbox deleted") + project_client.beta.skills.delete(SKILL_NAME) + print("Skill deleted") From b15620d27f599a5c6107478f37fde80692198463 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Fri, 29 May 2026 19:41:12 -0700 Subject: [PATCH 5/6] recording --- sdk/ai/azure-ai-projects/assets.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index b8b603a2eb49..47c3417c5202 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_449a9f8e06" + "Tag": "python/ai/azure-ai-projects_8a70d850b6" } From 255d2529f5f5c44ed1d813162e64fc06b0fe55d0 Mon Sep 17 00:00:00 2001 From: Howie Leung Date: Fri, 29 May 2026 23:08:31 -0700 Subject: [PATCH 6/6] Update assets.json Tag and enable additional sample tests in test_samples.py --- sdk/ai/azure-ai-projects/assets.json | 2 +- ...ple_agent_trace_evaluation_smart_filter.py | 4 ++-- ...ed_agent_traces_evaluation_smart_filter.py | 8 ++++--- .../tests/samples/test_samples.py | 24 +++++++++---------- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/sdk/ai/azure-ai-projects/assets.json b/sdk/ai/azure-ai-projects/assets.json index 47c3417c5202..a3832e93b84d 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_8a70d850b6" + "Tag": "python/ai/azure-ai-projects_11a3786ca2" } diff --git a/sdk/ai/azure-ai-projects/samples/evaluations/sample_agent_trace_evaluation_smart_filter.py b/sdk/ai/azure-ai-projects/samples/evaluations/sample_agent_trace_evaluation_smart_filter.py index bb1e44c4898a..983e27916ef2 100644 --- a/sdk/ai/azure-ai-projects/samples/evaluations/sample_agent_trace_evaluation_smart_filter.py +++ b/sdk/ai/azure-ai-projects/samples/evaluations/sample_agent_trace_evaluation_smart_filter.py @@ -127,7 +127,7 @@ "start_time": start_time, "end_time": end_time, "max_traces": args.max_traces, - "filter_strategy": "smart_filtering" + "filter_strategy": "smart_filtering", } if args.agent_id: @@ -173,4 +173,4 @@ print(f"\nāœ— Evaluation run failed: {run.error}") client.evals.delete(eval_id=eval_object.id) - print("Evaluation deleted") \ No newline at end of file + print("Evaluation deleted") diff --git a/sdk/ai/azure-ai-projects/samples/evaluations/sample_scheduled_agent_traces_evaluation_smart_filter.py b/sdk/ai/azure-ai-projects/samples/evaluations/sample_scheduled_agent_traces_evaluation_smart_filter.py index 4e39ea2eb539..b3f337f6c522 100644 --- a/sdk/ai/azure-ai-projects/samples/evaluations/sample_scheduled_agent_traces_evaluation_smart_filter.py +++ b/sdk/ai/azure-ai-projects/samples/evaluations/sample_scheduled_agent_traces_evaluation_smart_filter.py @@ -43,7 +43,7 @@ Schedule, RecurrenceTrigger, DailyRecurrenceSchedule, - EvaluationScheduleTask + EvaluationScheduleTask, ) import time @@ -198,6 +198,7 @@ def assign_rbac(): # pylint: disable=too-many-statements print("An unexpected error occurred. Please check the error details above.") raise + def schedule_trace_evaluation(): endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"] model_deployment_name = os.environ["FOUNDRY_MODEL_NAME"] @@ -284,7 +285,7 @@ def schedule_trace_evaluation(): "start_time": start_time, "end_time": end_time, "max_traces": args.max_traces, - "filter_strategy": "smart_filtering" + "filter_strategy": "smart_filtering", } if args.agent_id: @@ -304,7 +305,7 @@ def schedule_trace_evaluation(): eval_run_object = { "eval_id": eval_object.id, "name": "trace_eval_with_smart_filter", - "data_source": data_source + "data_source": data_source, } print("Eval Run:") @@ -335,5 +336,6 @@ def schedule_trace_evaluation(): client.evals.delete(eval_id=eval_object.id) print("Evaluation deleted") + if __name__ == "__main__": main() 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 29e26e111d53..5f5504bba6f4 100644 --- a/sdk/ai/azure-ai-projects/tests/samples/test_samples.py +++ b/sdk/ai/azure-ai-projects/tests/samples/test_samples.py @@ -185,17 +185,17 @@ def test_models_samples(self, sample_path: str, **kwargs) -> None: # fails the test). @servicePreparer() - # @additionalSampleTests( - # [ - # AdditionalSampleTestDetail( - # test_id="sample_dataset_generation_job_simpleqna_with_prompt_source", - # sample_filename="sample_dataset_generation_job_simpleqna_with_prompt_source.py", - # env_vars={ - # "POLL_INTERVAL_SECONDS": "60", - # }, - # ), - # ] - # ) + @additionalSampleTests( + [ + AdditionalSampleTestDetail( + test_id="sample_dataset_generation_job_simpleqna_with_prompt_source", + sample_filename="sample_dataset_generation_job_simpleqna_with_prompt_source.py", + env_vars={ + "POLL_INTERVAL_SECONDS": "60", + }, + ), + ] + ) @pytest.mark.parametrize( "sample_path", get_sample_paths( @@ -251,7 +251,7 @@ def test_chat_completions_samples(self, sample_path: str, **kwargs) -> None: get_sample_paths( "hosted_agents", samples_to_skip=[ - "sample_routines_dispatch_and_runs.py", # Skipped due to service serialization issues + "sample_routines_crud.py", # Skipped due to service serialization issues "sample_routines_with_timer_trigger.py", # Skipped due to service serialization issues ], ),