Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -27,38 +28,67 @@
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
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["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:
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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"]
Comment on lines +73 to +77


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()
Loading
Loading