Skip to content

Commit a568d15

Browse files
authored
Add sample demonstrating routine creation with schedule trigger (#47273)
* Add sample demonstrating routine creation with schedule trigger * Update samples and changelog for routine and dataset generation improvements - Added new routines sample demonstrating triggering by a schedule. - Updated changelog with new routines samples and modifications. - Refactored dataset generation sample to improve structure and error handling. - Adjusted test samples to include new routines and updated skip conditions.
1 parent 5c3be0f commit a568d15

7 files changed

Lines changed: 348 additions & 180 deletions

File tree

sdk/ai/azure-ai-projects/CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44

55
### Sample updates
66

7-
* Added Routines samples `sample_routines_crud.py` demonstrating CRUD operations and `sample_routines_with_timer_trigger.py` demonstrating triggering a routine by a timer.
8-
* `sample_dataset_generation_job_traces_for_evaluation.py` and `sample_dataset_generation_job_traces_for_finetuning.py` are now self-contained: each sample creates a temporary agent, seeds conversations, retries the data generation job over the trace window, and cleans up all created resources.
9-
* Extended `sample_memory_crud.py` and `sample_memory_crud_async.py` to also demonstrate memory item CRUD (`create_memory`, `get_memory`, `update_memory`, `list_memories`, `delete_memory`) in addition to memory store CRUD.
7+
* Added `sample_routines_crud.py` to demonstrate CRUD operations.
8+
* Added `sample_routines_with_timer_trigger.py` to demonstrate triggering a routine with a timer.
9+
* Added `sample_routines_with_schedule_trigger.py` to demonstrate triggering a routine on a recurring cron schedule via `ScheduleRoutineTrigger`.
10+
* Updated `sample_dataset_generation_job_traces_for_evaluation.py` and `sample_dataset_generation_job_traces_for_finetuning.py` to create a temporary agent, seed conversations, retry the data generation job over the trace window, and clean up all created resources.
11+
* Updated `sample_memory_crud.py` and `sample_memory_crud_async.py` to demonstrate memory item CRUD (`create_memory`, `get_memory`, `update_memory`, `list_memories`, `delete_memory`) in addition to memory store CRUD.
1012

1113
## 2.2.0 (2026-05-29)
1214

sdk/ai/azure-ai-projects/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "python",
44
"TagPrefix": "python/ai/azure-ai-projects",
5-
"Tag": "python/ai/azure-ai-projects_7a920efd23"
5+
"Tag": "python/ai/azure-ai-projects_059be0eaf8"
66
}

sdk/ai/azure-ai-projects/samples/datasets/sample_dataset_generation_job_simpleqna_with_prompt_source.py

Lines changed: 177 additions & 171 deletions
Large diffs are not rendered by default.
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# pylint: disable=line-too-long,useless-suppression
2+
# ------------------------------------
3+
# Copyright (c) Microsoft Corporation.
4+
# Licensed under the MIT License.
5+
# ------------------------------------
6+
7+
"""
8+
DESCRIPTION:
9+
This sample demonstrates how to create a Routine that fires on a
10+
recurring cron schedule, then record the resulting runs by polling
11+
`list_runs(...)` using the synchronous AIProjectClient.
12+
13+
The routine is bound to an existing hosted agent and scheduled with a
14+
`ScheduleRoutineTrigger` using a 5-field cron expression. The service
15+
enforces a minimum interval of five minutes, so the sample polls the
16+
run history for up to ~6 minutes to catch the first fire, prints each
17+
observed phase transition, then deletes the routine.
18+
19+
Routines are currently a preview feature. In the Python SDK, you access
20+
these operations via `project_client.beta.routines`.
21+
22+
USAGE:
23+
python sample_routines_with_schedule_trigger.py
24+
25+
Before running the sample:
26+
27+
pip install "azure-ai-projects>=2.2.0" python-dotenv
28+
29+
Set these environment variables with your own values:
30+
1) FOUNDRY_PROJECT_ENDPOINT - The Azure AI Project endpoint, as found in the Overview
31+
page of your Microsoft Foundry portal.
32+
2) FOUNDRY_HOSTED_AGENT_NAME - The name of an existing Hosted Agent to invoke
33+
when the routine schedule fires.
34+
3) POLL_INTERVAL_SECONDS - Optional. Seconds to sleep between run-history polls.
35+
Defaults to 15.
36+
"""
37+
38+
import json
39+
import os
40+
import time
41+
42+
from dotenv import load_dotenv
43+
44+
from azure.core.exceptions import ResourceNotFoundError
45+
from azure.identity import DefaultAzureCredential
46+
47+
from azure.ai.projects import AIProjectClient
48+
from azure.ai.projects.models import (
49+
InvokeAgentResponsesApiRoutineAction,
50+
RoutineRun,
51+
RoutineRunPhase,
52+
ScheduleRoutineTrigger,
53+
)
54+
55+
load_dotenv()
56+
57+
endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"]
58+
agent_name = os.environ["FOUNDRY_HOSTED_AGENT_NAME"]
59+
poll_interval_seconds = int(os.environ.get("POLL_INTERVAL_SECONDS", "15"))
60+
61+
62+
def main() -> None:
63+
with (
64+
DefaultAzureCredential() as credential,
65+
AIProjectClient(endpoint=endpoint, credential=credential, allow_preview=True) as project_client,
66+
):
67+
routine_name = "sample-routine-schedule"
68+
69+
try:
70+
project_client.beta.routines.delete(routine_name)
71+
print(f"Routine `{routine_name}` deleted")
72+
except ResourceNotFoundError:
73+
pass
74+
75+
# Fire every 5 minutes (the service-enforced minimum interval) in UTC.
76+
cron_expression = "*/5 * * * *"
77+
time_zone = "UTC"
78+
created = project_client.beta.routines.create_or_update(
79+
routine_name,
80+
description="Routine used by the schedule-trigger sample.",
81+
enabled=True,
82+
triggers={
83+
"every_five_minutes": ScheduleRoutineTrigger(
84+
cron_expression=cron_expression,
85+
time_zone=time_zone,
86+
)
87+
},
88+
action=InvokeAgentResponsesApiRoutineAction(agent_name=agent_name),
89+
)
90+
print(
91+
f"Created routine: {created.name} enabled={created.enabled} "
92+
f"cron={cron_expression!r} time_zone={time_zone!r}"
93+
)
94+
95+
try:
96+
terminal_phases = {RoutineRunPhase.COMPLETED, RoutineRunPhase.FAILED}
97+
seen_phases: dict[str, str] = {}
98+
final_run: RoutineRun | None = None
99+
100+
# Poll for up to ~6m30s to catch the first scheduled fire.
101+
max_polls = max(1, (6 * 60 + 30) // poll_interval_seconds + 1)
102+
print(
103+
f"Poll `{routine_name}` every {poll_interval_seconds}s for new runs "
104+
f"(up to {max_polls} iterations, ~6m30s).",
105+
end="",
106+
flush=True,
107+
)
108+
dots_pending = False
109+
for _ in range(max_polls):
110+
runs = list(project_client.beta.routines.list_runs(routine_name, limit=20, order="desc"))
111+
for run in runs:
112+
if seen_phases.get(run.id) == run.phase:
113+
continue
114+
seen_phases[run.id] = str(run.phase)
115+
if dots_pending:
116+
print()
117+
dots_pending = False
118+
print(
119+
f" - run_id={run.id} phase={run.phase} status={run.status} "
120+
f"trigger_type={run.trigger_type} triggered_at={run.triggered_at} ended_at={run.ended_at}"
121+
)
122+
if str(run.status).lower() == "finished":
123+
final_run = run
124+
125+
if final_run is not None:
126+
break
127+
time.sleep(poll_interval_seconds)
128+
print(".", end="", flush=True)
129+
dots_pending = True
130+
131+
if dots_pending:
132+
print()
133+
134+
if final_run:
135+
print("Final run:")
136+
print(json.dumps(final_run.as_dict(), indent=2, default=str))
137+
# Note: retrieving the response body produced by a routine-dispatched
138+
# run via `openai_client.responses.retrieve(final_run.response_id)` is
139+
# not yet supported by the service for this scenario.
140+
else:
141+
print("Schedule did not produce a terminal run within the deadline.")
142+
except KeyboardInterrupt:
143+
print("Interrupted by user; cleaning up routine before exiting.")
144+
finally:
145+
# Always delete the routine so it stops firing on the schedule,
146+
# even if the sample was interrupted or raised an exception.
147+
try:
148+
project_client.beta.routines.delete(routine_name)
149+
print("Routine deleted")
150+
except ResourceNotFoundError:
151+
pass
152+
153+
154+
if __name__ == "__main__":
155+
main()

sdk/ai/azure-ai-projects/samples/hosted_agents/sample_routines_with_timer_trigger.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@
107107
while time.monotonic() < deadline:
108108
runs = list(project_client.beta.routines.list_runs(routine_name, limit=20, order="desc"))
109109
for run in runs:
110-
if run.id is None:
111-
continue
112110
if seen_phases.get(run.id) == run.phase:
113111
continue
114112
seen_phases[run.id] = run.phase # type: ignore[assignment]

sdk/ai/azure-ai-projects/tests/samples/test_samples.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def test_models_samples(self, sample_path: str, **kwargs) -> None:
197197
get_sample_paths(
198198
"datasets",
199199
samples_to_skip=[
200-
"sample_dataset_generation_job_simpleqna_with_prompt_source.py", # PR #47067: recording not yet available
200+
"sample_dataset_generation_job_simpleqna_with_prompt_source.py", # Specified through AdditionalSampleTestDetail
201201
"sample_dataset_generation_job_traces_for_finetuning.py", # PR #47067: recording not yet available
202202
"sample_dataset_generation_job_simpleqna_for_finetuning.py", # PR #47067: recording not yet available
203203
"sample_dataset_generation_job_traces_for_evaluation.py", # PR #47067: recording not yet available
@@ -240,13 +240,21 @@ def test_chat_completions_samples(self, sample_path: str, **kwargs) -> None:
240240
"FOUNDRY_HOSTED_AGENT_REMOTE_BUILD": "true",
241241
},
242242
),
243+
AdditionalSampleTestDetail(
244+
test_id="sample_routines_with_schedule_trigger",
245+
sample_filename="sample_routines_with_schedule_trigger.py",
246+
env_vars={
247+
"POLL_INTERVAL_SECONDS": "300",
248+
},
249+
),
243250
]
244251
)
245252
@pytest.mark.parametrize(
246253
"sample_path",
247254
get_sample_paths(
248255
"hosted_agents",
249256
samples_to_skip=[
257+
"sample_routines_with_schedule_trigger.py", # Specify through AdditionalSampleTestDetail
250258
"sample_routines_crud.py", # Skipped due to service serialization issues
251259
"sample_routines_with_timer_trigger.py", # Skipped due to service serialization issues
252260
],

sdk/ai/azure-ai-projects/tests/samples/test_samples_async.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ async def test_agent_tools_samples_async(self, sample_path: str, **kwargs) -> No
5050
"sample_path",
5151
get_async_sample_paths(
5252
"memories",
53-
samples_to_skip=[
54-
],
53+
samples_to_skip=[],
5554
),
5655
)
5756
@servicePreparer()

0 commit comments

Comments
 (0)