
So far your agents have mostly read things. The moment an agent can do something (issue a refund, close a ticket, change a record), the question stops being “is the answer good?” and becomes “who allowed that?”. AI-103 expects you to know where Foundry lets a person approve a tool call before it runs, how to keep a loop from running for ever, and how a model can check and improve its own work. At work, this is the difference between an agent your risk committee signs off and one it switches off. Today you build an approval gate and a capped self-critique loop, and you see both work.
build_agent.py). A local approvals.jsonl audit file. No new Azure resource types.mcp_approval.py ends with delete_version. If you rebuilt the Session 13 agent, run its cleanup.py and delete_file.sh again, which also remove its vector store and file. Confirm in the Foundry portal that no test agent remains. Then delete the model deployment: Build → Models → open the deployment → delete, or az cognitiveservices account deployment delete --deployment-name <name> -n <resource> -g rg-ai103. Keep rg-ai103 and the project.| Mechanism | Where it lives | What actually pauses |
|---|---|---|
MCP tool approval (require_approval) | Foundry Agent Service, on an MCPTool | The response returns an mcp_approval_request; the tool does not run until you send an mcp_approval_response |
Function tool (FunctionTool) | Foundry Agent Service | The model only asks for the call. Your app executes the function, so your code decides whether it runs at all |
| Human in the loop workflow template | Foundry workflows (preview) | An Ask a question node waits for the user. Learn says Foundry is retiring workflows on December 1, 2026, and points new builds to Microsoft Agent Framework |
| Tool approval in Agent Framework | Microsoft Agent Framework, @tool(approval_mode="always_require") | The run returns user_input_requests; you reply with to_function_approval_response |
Put approval gates on consequential tools: writes, payments, deletions. In Foundry Agent Service, an MCP tool uses require_approval; a function tool is executed by your app, so its gate lives in your own code.
require_approval into a tools agent. Today you turn it into a control you could defend to an auditor. The setting takes "always" (the default), "never", or a dictionary that names tools: {"always": [...]} for the tools that need approval, or {"never": [...]} for the tools that do not. allowed_tools limits which of the server’s tools the agent can use at all, and server_label identifies the server to the agent.mcp_approval_request item carries the server label, tool name and arguments; you answer with an mcp_approval_response holding the approval_request_id and approve, chained with previous_response_id. What is new here: every decision is written to an audit file with who made it and when, the rounds are capped, and the rejection path is tested.agent_reference in extra_body. The audit record is my addition, not a Foundry feature: in production it would go to your logging system.import datetime, getpass, json, os
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import PromptAgentDefinition, MCPTool
from openai.types.responses.response_input_param import McpApprovalResponse
project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential())
openai = project.get_openai_client()
agent = project.agents.create_version(
agent_name="approval-agent",
definition=PromptAgentDefinition(
model=os.environ["FOUNDRY_MODEL_NAME"],
instructions="Use the mslearn tools for Microsoft product questions. "
"If a tool call is rejected, say so and answer without it.",
tools=[MCPTool(server_label="mslearn",
server_url="https://learn.microsoft.com/api/mcp",
require_approval="always")],
),
)
ref = {"agent_reference": {"name": agent.name, "type": "agent_reference"}}
def review(item): # show the proposed call, ask a person, keep an audit record
print(f"{item.server_label} wants {item.name} with {item.arguments}")
ok = input("Approve? (y/N): ").strip().lower() == "y"
record = {"time": datetime.datetime.now().isoformat(), "reviewer": getpass.getuser(),
"request_id": item.id, "tool": item.name, "approved": ok}
with open("approvals.jsonl", "a") as log:
log.write(json.dumps(record) + "\n")
return McpApprovalResponse(type="mcp_approval_response",
approve=ok, approval_request_id=item.id)
response = openai.responses.create(input="How do I delete a Foundry project?",
extra_body=ref)
for _ in range(5): # cap the approval rounds
approvals = [review(i) for i in response.output if i.type == "mcp_approval_request"]
if not approvals:
break
response = openai.responses.create(input=approvals,
previous_response_id=response.id, extra_body=ref)
print(response.output_text)
project.agents.delete_version(agent_name=agent.name, agent_version=agent.version)mslearn wants <tool-name> with <the arguments the model proposed>
Approve? (y/N): n
<an answer that says the lookup was declined>approvals.jsonl. Read how the agent answers without its tool: that behaviour is part of your design.require_approval defaults to "always". The response then carries an mcp_approval_request; you reply with an mcp_approval_response holding approval_request_id and approve, linked by previous_response_id.
FunctionTool, the model returns a function_call item and stops. Learn’s wording is exact: “Your app executes the function and returns the output.” So the approval gate sits in your code, before the function runs and before you send back the function_call_output.chat.py). Session 13’s clean-up deleted that agent, so run build_agent.py again first. A rejection still returns an output, so the model can tell the user what happened.# Inside the loop over response.output in your Session 13 function-calling agent.
# TOOLS maps tool names to your Python functions; these names are illustrative.
CONSEQUENTIAL = {"issue_refund", "close_account"}
if item.type == "function_call":
args = json.loads(item.arguments)
if item.name in CONSEQUENTIAL:
print(f"{item.name} wants to run with {args}")
if input("Approve? (y/N): ").strip().lower() == "y":
result = TOOLS[item.name](**args)
else:
result = {"status": "rejected", "reason": "A reviewer declined this action."}
else:
result = TOOLS[item.name](**args) # read-only tools run freely
input_list.append(FunctionCallOutput(
type="function_call_output",
call_id=item.call_id,
output=json.dumps(result),
))strict=True on the function schema, as in Learn’s sample, and a counter that stops the loop after a fixed number of tool rounds.approval_mode="always_require". Recognise it; you need not build with it today.import os
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential())
openai = project.get_openai_client()
MODEL = os.environ["FOUNDRY_MODEL_NAME"]
MAX_ROUNDS = 3
CRITERIA = ("1. Under 120 words. 2. States the refund amount and the date. "
"3. Promises nothing beyond the refund. 4. Polite, plain English.")
TASK = "Write a reply to a customer whose refund of 40 units was approved today."
draft = openai.responses.create(model=MODEL, input=TASK).output_text
for round_no in range(1, MAX_ROUNDS + 1):
critique = openai.responses.create(
model=MODEL,
instructions="You are a strict reviewer. Check the draft against each "
"criterion. Reply APPROVED if every criterion passes; "
"otherwise list each failure on its own line.",
input=f"Criteria:\n{CRITERIA}\n\nDraft:\n{draft}",
).output_text
print(f"--- Round {round_no} critique ---\n{critique}")
if critique.strip().startswith("APPROVED") and len(draft.split()) < 120:
break
draft = openai.responses.create(
model=MODEL,
instructions="Revise the draft to fix every listed failure. "
"Return only the revised draft.",
input=f"Task: {TASK}\n\nDraft:\n{draft}\n\nFailures:\n{critique}",
).output_text
else:
print("Cap reached: route the last draft to a human reviewer.")
print(f"--- Final draft ---\n{draft}")--- Round 1 critique ---
<the failures the reviewer found, one per line>
--- Round 2 critique ---
APPROVED
--- Final draft ---
<the revised reply>Maker-checker (reflection) loops need clear acceptance criteria, an iteration cap, and a fallback when the cap is reached, such as escalation to a human reviewer.
Chain of thought prompting suits non-reasoning models. For reasoning models, request a reasoning summary through the Responses API; extracting raw reasoning by other means isn't supported and can lead to throttling or suspension.
output_tokens_details.reasoning_tokens in a Responses API response, which tells you how many reasoning tokens a request consumed. Session 18 comes back to this.require_approval values and item names; why a function tool’s gate is in your code; the maker-checker conditions; and when chain of thought prompting fits.rg-ai103 for Session 17, where we trace an agent like these.require_approval | parameter | Sets which MCP tool calls need approval: always (the default), never, or named tools. | Learn → |
mcp_approval_request | concept | The response item type that says an MCP tool call is waiting for approval. | Learn → |
mcp_approval_response | concept | The input item type you send back to approve or reject the call. | Learn → |
approval_request_id | parameter | Links your approval response to the request item it answers. | Learn → |
Human in the loop | feature | A Foundry workflow template that asks the user a question and waits before continuing. | Learn → |
Ask a question | portal | The workflow node that pauses and waits for the user’s input or approval. | Learn → |
approval_mode | parameter | Agent Framework’s tool setting; always_require makes a function wait for human approval. | Learn → |
user_input_requests | concept | Where an Agent Framework run lists the tool calls waiting for approval. | Learn → |
maker-checker loop | concept | One agent proposes, another checks against criteria and sends it back until approved or capped. | Learn → |
iteration cap | concept | The limit that stops a refinement loop, with a defined fallback when it is reached. | Learn → |
Chain of thought prompting | concept | Asking a non-reasoning model to work step by step and show every step. | Learn → |
reasoning summary | feature | The supported way to see a summary of a reasoning model’s chain of thought. | Learn → |
output_tokens_details.reasoning_tokens | parameter | Where a Responses API response reports how many reasoning tokens were used. | Learn → |
A workflow where the agent acts alone on low-risk steps but pauses for a person before consequential ones.
A point where a tool call waits for a human decision, which is then recorded, before anything changes.
The maximum number of rounds a loop may run before it stops and falls back, for example to a human reviewer.
Tokens a reasoning model uses to think. They never appear in the answer but are billed as output tokens.
get_shipment and cancel_shipment (illustrative names). Cancellations must be approved by a person; lookups must not slow users down. How do you configure the tool?require_approval="never"require_approval="always"require_approval={"always": ["cancel_shipment"]}require_approval={"always": ["cancel_shipment"]} — The dictionary form requires approval only for the named tool, so lookups run freely while cancellations wait. "always" is safe but slows every lookup, and instructions alone are not an approval control.mcp_approval.py with require_approval (use the dictionary form if your server offers both kinds of tool), or your Session 13 function-calling agent with the gate from gate.py. Run it twice, approving once and rejecting once, and keep an audit record of both decisions.critique_loop.py for an output your task produces. Write at least three acceptance criteria, keep MAX_ROUNDS at 3 or fewer, check anything countable in code, and define what happens when the cap is reached.require_approval mcp_approval_request maker-checker loop iteration cap Chain of thought prompting reasoning summary| The approval gate works: a consequential call pauses, one approval and one rejection are shown, and the rejection is handled sensibly | 30 |
| The self-critique loop has written acceptance criteria, an iteration cap and a fallback, and the critique visibly changes the draft | 25 |
| Uses the exact names from this session’s names box for the tool, parameters and response items | 20 |
| Evidence: terminal output for both runs and the loop, and the portal screenshot with keys cropped; clean-up stated | 25 |
| Total · pass mark 70 | 100 |
Session: 16 (the template has it ready, under the Course line), or the submission is refused. Upload the Word file (.docx), or switch to the Google Doc tab and paste your document’s share link.Marked against the rubric above and returned as a written letter by email; the mark appears on your progress page. Do the self-check above before you submit.When an agent gives a wrong answer in production, “it worked when I tested it” is not an explanation. You need to see what happened: which tool it called, with which arguments, how long each step took, how many tokens it burned, and whether a safety control fired. AI-103 tests exactly that, down to the package names, the environment variable that switches tracing on, where traces are stored and the role needed to read them. At work, tracing turns a vague complaint into a fix you can prove. Today you trace your agent, read its spans, and learn to analyse an error rather than guess at it.
rg-ai103 afterwards and note every new resource. Also a Global Standard model deployment and a short-lived traced agent version.az cognitiveservices account deployment delete). Keep Application Insights connected: Session 19 needs it, Session 24 can use it, and the Session 40 capstone needs tracing again. Session 20 decides whether it stays. Keep rg-ai103.invoke_agent for the agent call, execute_tool for a tool execution, and attributes such as gen_ai.tool.call.arguments.Foundry stores traces in the Application Insights resource connected to the project. To query them you need Log Analytics Reader on that resource, and Privileged Monitoring Data Reader too if the underlying tables are protected.
azure-monitor-opentelemetry, is what sends spans to Application Insights.# The packages Learn lists for client-side tracing to Azure Monitor
# (with the course floor for azure-ai-projects, set in Session 9)
pip install "azure-ai-projects>=2.4.0" azure-identity opentelemetry-sdk \
azure-core-tracing-opentelemetry azure-monitor-opentelemetry
# Content recording is off by default. Leave it off unless the data is test data.
# export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=trueAZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING must be true before you call AIProjectInstrumentor().instrument(). If it is not, instrumentation is not enabled and only a warning is logged. And get the OpenAI client after instrumenting, or the spans will not correlate.trace_function turns the tool into its own span, and agent_reference carries both name and id, which Learn says you need to correlate traces with the agent in the portal.import json, os
os.environ["AZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING"] = "true" # before instrument()
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import PromptAgentDefinition, FunctionTool
from azure.ai.projects.telemetry import AIProjectInstrumentor, trace_function
from azure.monitor.opentelemetry import configure_azure_monitor
from openai.types.responses.response_input_param import FunctionCallOutput
from opentelemetry import trace
project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential())
configure_azure_monitor(
connection_string=project.telemetry.get_application_insights_connection_string())
AIProjectInstrumentor().instrument()
openai = project.get_openai_client() # obtained after instrumentation
@trace_function
def get_leave_balance(staff_id: str) -> dict: # your Session 13 function
return {"staff_id": staff_id, "days_remaining": 12}
tool = FunctionTool(name="get_leave_balance", description="Leave days left for a staff ID.",
parameters={"type": "object", "properties": {"staff_id": {"type": "string"}},
"required": ["staff_id"], "additionalProperties": False}, strict=True)
agent = project.agents.create_version(agent_name="traced-hr-agent",
definition=PromptAgentDefinition(model=os.environ["FOUNDRY_MODEL_NAME"],
instructions="Answer leave questions. Use get_leave_balance when given a staff ID.",
tools=[tool]))
ref = {"agent_reference": {"name": agent.name, "id": agent.id, "type": "agent_reference"}}
with trace.get_tracer(__name__).start_as_current_span("leave-balance-scenario"):
conversation = openai.conversations.create()
r = openai.responses.create(input="How many leave days does E1042 have left?",
conversation=conversation.id, extra_body=ref)
outputs = [FunctionCallOutput(type="function_call_output", call_id=i.call_id,
output=json.dumps(get_leave_balance(**json.loads(i.arguments))))
for i in r.output if i.type == "function_call"]
r = openai.responses.create(input=outputs, conversation=conversation.id, extra_body=ref)
print(r.output_text, "| output tokens:", r.usage.output_tokens)
project.agents.delete_version(agent_name=agent.name, agent_version=agent.version)E1042 has 12 days of annual leave remaining. | output tokens: <a number>Set AZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING to true before AIProjectInstrumentor().instrument(), then export with configure_azure_monitor and the project's Application Insights connection string. Message content is not recorded unless you opt in.
response.usage.output_tokens, and the agent’s Monitor tab charts token usage over time. Remember from Session 16 that reasoning tokens are billed as output tokens even though you never see them.execute_tool span for the booking system, not in the model. The fix is in the booking API, not the prompt.| Signal | Where you read it | What it tells you |
|---|---|---|
| Token usage | Span attributes in Traces; response.usage; the agent’s Monitor tab | What drives cost, and which prompts or tool results are bloated |
| Latency | Span durations; Time to Response, Time Between Tokens and Time to Last Byte metrics | Whether the model, a tool or the network is slow |
| Safety signals | Blocked Volume and Harmful Volume Detected metrics; safety evaluators such as builtin.violence | How often content filters act on your traffic, and whether outputs are unsafe |
| Run success rate | The agent’s Monitor tab | Learn says a rate below 95% warrants investigation |
builtin.task_adherence), Intent Resolution and Task Completion; and process evaluation, such as Tool Call Accuracy, Tool Selection, Tool Input Accuracy, Tool Output Utilization and Tool Call Success.Agent evaluators split into system evaluation (Task Adherence, Intent Resolution, Task Completion) and process evaluation (Tool Call Accuracy, Tool Selection and others). Cluster analysis groups failed samples to reveal recurring errors.
staff_id in the schema but not in the function, or make the tool raise an exception. Run the script and find the failure in the trace before you read the terminal.invoke_agent and execute_tool; the install line; the environment variable and its ordering rule; where traces are stored and the role needed to read them; the latency formula; and the two groups of agent evaluators.AIProjectInstrumentor | class | Turns on GenAI tracing for Foundry SDK and OpenAI Responses calls when you call instrument(). | Learn → |
azure-monitor-opentelemetry | package | The package that exports OpenTelemetry traces to Azure Monitor Application Insights. | Learn → |
configure_azure_monitor | method | Sends your app’s spans to the Application Insights resource named by a connection string. | Learn → |
get_application_insights_connection_string | method | Reads the connection string of the project’s connected Application Insights resource. | Learn → |
AZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING | setting | Must be true before instrument() is called, or no GenAI spans are produced. | Learn → |
OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT | setting | Records message content and tool arguments in spans. Off by default; development use only. | Learn → |
trace_function | method | A decorator that creates a span for each call to your own function. | Learn → |
Log Analytics Reader | role | The role needed on the connected Application Insights resource to query traces. | Learn → |
execute_tool | concept | The span name that records a tool execution inside an agent trace. | Learn → |
Agent Monitoring Dashboard | portal | The agent’s Monitor tab: token usage, latency, run success rate, evaluations and red teaming. | Learn → |
Time to Response | concept | The Azure Monitor latency metric for streaming requests: time to the first response chunk. | Learn → |
Task Adherence | concept | A system evaluator: did the agent follow the rules and constraints in its instructions? | Learn → |
Tool Call Accuracy | concept | A process evaluator: did the agent make the right tool calls with correct parameters? | Learn → |
Cluster analysis | feature | Groups evaluation samples with similar problems so you can spot recurring errors (preview). | Learn → |
The record of one request’s journey through your app and agent, made of nested spans.
One timed operation inside a trace, such as a model call or a tool execution, with its attributes.
Capturing prompts, outputs and tool arguments inside spans. Off by default because traces may hold sensitive data.
Finding the failing step in real runs, naming its root cause, fixing one thing and proving the fix with a new trace.
AIProjectInstrumentor().instrument() and configure_azure_monitor, runs the agent, and no GenAI spans appear. A warning was logged. What is the most likely cause?AZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING was not set to true before instrument() was calledAZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING was not set to true before instrument() was called — Learn says the variable must be true before instrument() is called, or instrumentation isn’t enabled and a warning is logged. Content recording only controls whether message content is captured, not whether spans exist.traced_agent.py to your Session 13 agent (add File Search back if you recreate its vector store), keeping endpoints and deployment names in environment variables.AIProjectInstrumentor configure_azure_monitor AZURE_EXPERIMENTAL_ENABLE_GENAI_TRACING execute_tool Log Analytics Reader Tool Call Accuracy| Tracing works end to end and both runs appear in the Traces tab, with the agent correlated | 25 |
| The error analysis names the failing span and a one-sentence root cause, and the fix is a single change proven by a second trace | 30 |
| Uses the exact package, class, setting and span names from this session’s names box | 20 |
| Evidence: both trace screenshots and both terminal outputs, with no keys or connection strings visible; clean-up stated | 25 |
| Total · pass mark 70 | 100 |
Session: 17 (the template has it ready, under the Course line), or the submission is refused. Upload the Word file (.docx), or switch to the Google Doc tab and paste your document’s share link.Marked against the rubric above and returned as a written letter by email; the mark appears on your progress page. Do the self-check above before you submit.Two things end AI projects that work perfectly in testing: a wall of 429 errors at nine on Monday morning, and an invoice nobody predicted. Both are about capacity. AI-103 expects you to know how quota is assigned, how tokens-per-minute and requests-per-minute limits are enforced, what to do with a 429, when provisioned throughput is the right answer, and where to read what you have spent. At work, you will be asked “can it take the load, and what will it cost?” long before anyone asks about prompts. Today you read your own quota, handle throttling in code and trace spend back to your project.
az cognitiveservices account deployment delete --deployment-name <name> -n <resource> -g rg-ai103. Deleting deployments first also frees their quota allocation. Keep rg-ai103.max_tokens setting (and best_of), and the running count resets each minute. Learn stresses that this estimate is not the token count used for billing.max_tokens inflates the estimate; rejected requests still count towards the limit but are never billed; short bursts break the per-second window; and shared Standard deployments may have their effective limit reduced temporarily, which you can spot in the x-ratelimit-limit-tokens response header.Quota is per subscription, region, model and deployment type, in TPM. A deployment's TPM is its rate limit and sets RPM in proportion. The estimate includes max_tokens, so an oversized max_tokens causes 429s.
# Quota usage in your region: name, current value and limit
az cognitiveservices usage list -l <your-region> -o table
# The capacity (TPM units) and type of one deployment
az cognitiveservices account deployment show \
--deployment-name "<your-deployment-name>" \
-n <your-foundry-resource-name> \
-g rg-ai103 \
--query "sku"<one row per quota: its name, current value and limit>
{
"capacity": 1,
"name": "GlobalStandard"
}{Provider}.{DeploymentType}.{Model}, with currentValue and limit. Look at quota per deployment, not only per subscription: Learn recommends rebalancing allocations across deployments based on observed usage.retry-after-ms header, the recommended wait in milliseconds.openai Python package already retries 429s and transient errors with exponential backoff, and Learn says the default is two retries and that it respects the retry-after headers. You tune it with max_retries. When you write your own loop, set max_retries=0 so the two do not stack.openai.RateLimitError, applied to your deployment through the keyless OpenAI client from Session 5. Learn’s own recipe: a short random delay, double it on each failure, add jitter, and stop after a fixed number of retries.import os, random, time
import openai
from openai import OpenAI
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
token_provider = get_bearer_token_provider(DefaultAzureCredential(),
"https://ai.azure.com/.default")
client = OpenAI(base_url=os.environ["FOUNDRY_OPENAI_BASE_URL"],
api_key=token_provider,
max_retries=0) # our loop retries, so switch off the SDK's own
def with_backoff(func, initial_delay=1.0, base=2.0, max_retries=6):
def wrapper(*args, **kwargs):
delay, attempt = initial_delay, 0
while True:
try:
return func(*args, **kwargs)
except openai.RateLimitError as e: # HTTP 429 only
attempt += 1
if attempt > max_retries:
raise RuntimeError(f"Gave up after {max_retries} retries") from e
delay *= base * (1 + random.random()) # exponential, with jitter
print(f" 429 received; retry {attempt} in {delay:.1f}s")
time.sleep(delay)
return wrapper
@with_backoff
def ask(prompt):
return client.responses.create(model=os.environ["FOUNDRY_MODEL_NAME"],
input=prompt, max_output_tokens=50)
for i in range(20): # a small burst against a deliberately small deployment
print(i, ask(f"Give one English word for the number {i}.").output_text)0 zero
1 one
2 two
429 received; retry 1 in 2.7s
3 three
...
19 nineteenmax_output_tokens and a tighter loop make throttling more likely.| What you see | What Learn says to do |
|---|---|
Occasional 429s that clear with retry-after-ms backoff | Retry. This is normal for shared Standard deployments |
| 429s during development or testing | Often acceptable: they may be deliberate cost guardrails |
| Sustained 429s in production while below approved quota | Escalate with a support request |
| A quota increase that is not reflected in effective limits | Check the allocation at deployment level first, then escalate |
| Frequent 429s on a latency-sensitive or critical workload | Consider provisioned throughput for guaranteed capacity |
On a 429, back off with jitter and honour retry-after-ms; the openai package retries twice by default (max_retries). Sustained 429s below approved quota: escalate. Latency-critical workloads with frequent 429s: consider provisioned throughput.
x-ms-spillover-deployment header.| Standard (pay per token) | Provisioned (PTUs) | |
|---|---|---|
| Capacity | Shared pool, limited by your TPM allocation | Reserved for your deployment |
| Billing | Per token processed | Hourly per PTU deployed, used or not; Azure Reservations can discount the PTU meter |
| When it throttles | Rate limit reached, or a temporary adjustment on the shared pool | Utilization at 100%; spillover can absorb overflow |
| SKUs to recognise | GlobalStandard, DataZoneStandard, Standard | GlobalProvisionedManaged, DataZoneProvisionedManaged, ProvisionedManaged |
Provisioned throughput reserves PTUs and bills hourly whether used or not. Azure Reservations discount the PTU meter but don't guarantee capacity. Spillover routes overflow to a standard deployment in the same resource.
project tag on its usage, so you can filter cost analysis by that tag. At the time of writing, Learn says this works for Models sold by Azure, including Azure OpenAI, but not yet for models served through Azure Marketplace.retry-after-ms and max_retries; the retry-versus-escalate table; PTUs, spillover and what reservations do not do; and the Cost analysis path and the project tag.rg-ai103.Tokens-per-Minute (TPM) | concept | The unit of quota; a deployment’s TPM allocation is also its token rate limit. | Learn → |
Requests-Per-Minute (RPM) | concept | The request rate limit, set in proportion to a deployment’s TPM. | Learn → |
Request quota | portal | The button on Manage → Quota for asking for more standard quota. | Learn → |
retry-after-ms | setting | The 429 response header giving the recommended wait, in milliseconds, before retrying. | Learn → |
max_retries | parameter | The openai client setting for built-in retries with exponential backoff; the default is two. | Learn → |
RateLimitError | class | The openai package exception raised for a 429 response. | Learn → |
az cognitiveservices usage list | cli | Shows quota usages for Cognitive Services, including Foundry, in a region. | Learn → |
Provisioned throughput units (PTUs) | tier | The unit of reserved model processing capacity for provisioned deployments. | Learn → |
Spillover | feature | Sends overflow from a full provisioned deployment to a standard deployment in the same resource. | Learn → |
x-ms-spillover-deployment | setting | The request header that controls spillover for an individual request. | Learn → |
Azure Reservations | feature | One-month or one-year discounts on the PTU meter; they don’t guarantee capacity. | Learn → |
Cost analysis | portal | The Cost Management view of spend, groupable by meter and filterable by tag. | Learn → |
Cost Management Reader | role | A role that lets you view costs and usage data. | Learn → |
The maximum tokens or requests a deployment accepts per minute; beyond it, requests get a 429 response.
Waiting longer after each failed attempt, usually doubling the delay, with random jitter so clients do not retry together.
Model processing capacity reserved for one deployment and billed whether or not it is used.
Everything a workload spends, including tool calls, retries, loops and reasoning tokens, not just the visible answer.
max_tokens. What is the most likely cause?az cognitiveservices usage list. State the limit for your model and deployment type, your current allocation, and whether you would request more.retry.py against a small deployment. Explain your backoff settings, why the built-in retries are switched off, what retry-after-ms is for, and when you would escalate instead of retrying.project tag), name three cost drivers of your workload and one way to reduce each. Keep the explanation to 300–700 words.az cognitiveservices usage list for your region.retry.py, including any 429 retry lines, or a clear note that none occurred and at what capacity.project tag.Tokens-per-Minute (TPM) Requests-Per-Minute (RPM) retry-after-ms max_retries Provisioned throughput units (PTUs) Cost analysis| The TPM estimate is calculated from stated assumptions and the Standard-or-provisioned choice is justified against Learn’s criteria | 30 |
| The retry code works, disables double retries and backs off with jitter; the retry-versus-escalate reasoning is correct | 25 |
| Uses the exact names from this session’s names box for limits, headers, parameters and portal views | 20 |
| Evidence: quota reading, retry output and a Cost analysis screenshot, with no keys visible; clean-up stated | 25 |
| Total · pass mark 70 | 100 |
Session: 18 (the template has it ready, under the Course line), or the submission is refused. Upload the Word file (.docx), or switch to the Google Doc tab and paste your document’s share link.Marked against the rubric above and returned as a written letter by email; the mark appears on your progress page. Do the self-check above before you submit.RAG apps and agents rarely fail loudly. An indexer quietly skips new files, answers drift away from the source documents, content filters start firing more often, and nobody notices until a customer does. AI-103 asks you to monitor both halves of the system: the model (performance, drift, safety events, grounding quality) and the data behind it (ingestion quality, index health, relevance). You need the exact metric names and where each is read. At work, a monitoring runbook is what lets someone other than you keep the system healthy. Today you read real metrics, check an indexer from code and switch on continuous evaluation.
Microsoft.Storage/storageAccounts) with a blob container holding three or four of your own files, and run the Import data wizard over it.max_hourly_runs. Platform metrics are free, but a diagnostic setting that sends logs to Log Analytics adds ingestion charges. Azure AI Search on the dedicated pricing model bills until the service is deleted, and the storage account bills for what it stores. Check the Microsoft Foundry, Azure AI Search, Storage and Azure Monitor pricing pages.continuous_eval.py again with enabled=False. Remove any diagnostic setting you added (Monitoring → Diagnostic settings). Delete the model deployment. Leave the agent, the search service, the storage account and Application Insights for Session 20, which is the full clean-up.| What to monitor | Signal (exact name) | Where you read it |
|---|---|---|
| Model load and errors | Azure OpenAI Requests, split by StatusCode and ModelDeploymentName | Azure portal → Monitor → Metrics |
| Token volume | Processed Inference Tokens (prompt plus generated) | Metrics |
| Latency | Time to Response, Time to Last Byte | Metrics |
| Safety events | Blocked Volume, Harmful Volume Detected | Metrics |
| Grounding quality | Groundedness, Relevance and Retrieval evaluators | Evaluation runs; the agent’s Monitor tab |
| Drift | The same evaluators on a fixed test dataset, on a schedule | Evaluation runs compared over time |
| Ingestion quality | Indexer status, Docs Succeeded, errors and warnings; Document processed count | The search service’s Indexers page; Metrics |
| Index health and query performance | Search Latency, Throttled search queries percentage, Storage usage, Vector Storage usage | Metrics on the search service |
Drift is caught by scheduled evaluation against a fixed test dataset; continuous evaluation samples production traffic. Platform metrics are collected automatically; resource logs need a diagnostic setting.
ModelDeploymentName or StatusCode. In the Foundry portal, Build → Models → a deployment → Monitor gives a per-deployment view.TokenTransaction, Azure OpenAI Requests is AzureOpenAIRequests, Blocked Volume is RAIRejectedRequests, and Harmful Volume Detected is RAIHarmfulRequests.# The resource ID of your Foundry resource
id=$(az cognitiveservices account show \
-n <your-foundry-resource-name> -g rg-ai103 --query id -o tsv)
# Tokens processed and requests made, hourly for the last day, per deployment
az monitor metrics list --resource $id \
--metrics TokenTransaction AzureOpenAIRequests \
--dimension ModelDeploymentName \
--interval PT1H --offset 1d --aggregation Total -o table
# Safety events: calls rejected by, or flagged by, content filtering
az monitor metrics list --resource $id \
--metrics RAIRejectedRequests RAIHarmfulRequests \
--interval PT1H --offset 1d --aggregation Total -o table<a table of timestamps and totals per metric, one series per deployment>RequestResponse and Audit, tick AllMetrics, choose Send to Log Analytics workspace, and save. Logs you ingest are logs you pay for.max_hourly_runs as 100; runs beyond the cap are skipped.import os
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import (
EvaluationRule, ContinuousEvaluationRuleAction,
EvaluationRuleFilter, EvaluationRuleEventType,
)
project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential())
openai = project.get_openai_client()
AGENT_NAME = "<your-agent-name>" # an agent that exists in this project
eval_object = openai.evals.create(
name="Continuous Evaluation",
data_source_config={"type": "azure_ai_source", "scenario": "responses"},
testing_criteria=[{"type": "azure_ai_evaluator", "name": "violence_detection",
"evaluator_name": "builtin.violence"}],
)
rule = project.evaluation_rules.create_or_update(
id="ai103-continuous-eval",
evaluation_rule=EvaluationRule(
display_name="AI-103 continuous evaluation",
description="Evaluates sampled responses from my agent",
action=ContinuousEvaluationRuleAction(eval_id=eval_object.id,
max_hourly_runs=10),
event_type=EvaluationRuleEventType.RESPONSE_COMPLETED,
filter=EvaluationRuleFilter(agent_name=AGENT_NAME),
enabled=True, # run again with False to switch the rule off
),
)
print(f"Rule {rule.id} runs eval {eval_object.id}")
# After you have sent the agent some traffic:
runs = openai.evals.runs.list(eval_id=eval_object.id, order="desc", limit=10)
if runs.data and runs.data[0].report_url:
print(f"Latest report: {runs.data[0].report_url}")Rule ai103-continuous-eval runs eval <eval-id>
Latest report: <a link to the evaluation report>builtin.groundedness), Relevance and Retrieval each score 1–5 with a default pass threshold of 3 at the time of writing, and Document Retrieval needs ground-truth labels. For drift, run them on the same fixed test dataset on a schedule (Recurring evaluations in the Monitor settings panel, in preview) and compare the scores over time.Continuous evaluation is an evaluation rule: ContinuousEvaluationRuleAction with max_hourly_runs, triggered on RESPONSE_COMPLETED and filtered to one agent. The project's managed identity needs the Foundry User role.
SearchIndexerClient.get_indexer_status returns the overall status, the last result and the execution history. Keyless access for indexer operations needs a role such as Search Service Contributor, and the service’s API access control must allow role-based access.import os
from azure.identity import DefaultAzureCredential
from azure.search.documents.indexes import SearchIndexerClient
# AZURE_SEARCH_SERVICE_ENDPOINT is the variable name Learn's Python readme uses.
# Export it with the same URL as SEARCH_ENDPOINT from Session 7.
client = SearchIndexerClient(os.environ["AZURE_SEARCH_SERVICE_ENDPOINT"],
DefaultAzureCredential())
for indexer in client.get_indexers():
status = client.get_indexer_status(indexer.name)
print(f"{status.name}: {status.status}, "
f"{len(status.execution_history)} runs in history")
last = status.last_result
if last:
print(f" last run: {last.status}, ended {last.end_time}")
print(f" items {last.item_count}, failed {last.failed_item_count}, "
f"errors {len(last.errors)}, warnings {len(last.warnings)}")
if last.error_message:
print(f" top-level error: {last.error_message}")<indexer-name>: running, <n> runs in history
last run: success, ended <time>
items <n>, failed <n>, errors <n>, warnings <n>An indexer run can report success while documents failed, if errors stay under Max failed items. Check Docs Succeeded, errors and warnings; the status API returns lastResult and up to 50 runs of executionHistory.
@search.score runs from 0 upwards for BM25 keyword search, from 0.333 to 1.00 for vector search, and follows RRF for hybrid; @search.rerankerScore runs from 0.00 to 4.00 for the semantic ranker. Compare scores only within one method.enabled=False, remove any diagnostic setting you added, and delete the model deployment. Everything else waits for Session 20, where we clean up the whole course so far.Azure OpenAI Requests | concept | The metric counting calls to the API, splittable by deployment and status code. | Learn → |
Processed Inference Tokens | concept | The metric for prompt plus generated tokens; REST name TokenTransaction. | Learn → |
Blocked Volume | concept | The metric counting calls rejected by the content filter. | Learn → |
Harmful Volume Detected | concept | The metric counting calls the content filter detected as harmful, blocked or annotated. | Learn → |
Diagnostic settings | portal | Where you route a resource’s logs and metrics to a destination such as Log Analytics. | Learn → |
Continuous evaluation | feature | Quality and safety evaluation of production traffic at a sampled rate. | Learn → |
Scheduled evaluation | feature | Evaluation on a schedule against test datasets, to detect system drift. | Learn → |
ContinuousEvaluationRuleAction | class | The rule action that runs an evaluation on agent responses, with an hourly cap. | Learn → |
max_hourly_runs | parameter | Caps how many continuous evaluation runs happen per hour; extra runs are skipped. | Learn → |
get_indexer_status | method | Returns an indexer’s current status and execution history from Python. | Learn → |
Docs Succeeded | portal | The count of documents indexed in a run, shown in the indexer’s execution details. | Learn → |
Throttled search queries percentage | concept | The AI Search metric for the share of queries that were throttled. | Learn → |
@search.rerankerScore | concept | The semantic ranker’s relevance score, from 0.00 to 4.00. | Learn → |
Max failed items | setting | The error count below which an indexer run still reports success. | Learn → |
Numeric measurements Azure Monitor collects automatically from a resource, with no set-up and no charge for standard metrics.
Detailed records of a resource’s operations, kept only once a diagnostic setting routes them to a destination.
A gradual change in quality over time. You detect it by re-running the same evaluation on a fixed dataset and comparing scores.
The recent runs of an Azure AI Search indexer, up to 50, each with its status, counts, errors and warnings.
metrics.sh for at least two metrics, run indexer_status.py against your indexer, and run continuous_eval.py with a low max_hourly_runs, then send the agent some traffic.enabled=False, remove any diagnostic setting you added, delete the model deployment, and say so.metrics.sh (or a Metrics explorer screenshot) for at least two metrics.indexer_status.py, or a screenshot of the indexer’s execution details showing Docs Succeeded, errors and warnings.Processed Inference Tokens Blocked Volume Continuous evaluation Scheduled evaluation get_indexer_status Docs Succeeded Throttled search queries percentage| The runbook covers all seven areas, and each row has a correct signal, where it is read, a threshold and a first action | 35 |
| Uses the exact metric, evaluator, class and method names from this session’s names box, including REST names where a command needs them | 20 |
| The explanation separates ingestion from relevance problems and gives a sound method for detecting drift | 20 |
| Evidence: metric output, indexer status and a Monitor tab or report screenshot, with no keys visible; clean-up stated | 25 |
| Total · pass mark 70 | 100 |
Session: 19 (the template has it ready, under the Course line), or the submission is refused. Upload the Word file (.docx), or switch to the Google Doc tab and paste your document’s share link.Marked against the rubric above and returned as a written letter by email; the mark appears on your progress page. Do the self-check above before you submit.You are halfway. Nineteen sessions have covered every objective in the two heaviest domains except security, CI/CD, responsible AI and governance, which Week 5 teaches. Before Practice Exam 1, this session does three honest things. It drills every objective so far against Microsoft’s own wording and turns the gaps into a written weak-spot register. It teaches exam technique from Microsoft’s own pages, not folklore. And it cleans up your subscription, because four weeks of labs leave things running that you have forgotten. Skip the register and you are guessing where your gaps are.
rg-ai103 with its Foundry resource and project for Week 5, or delete the whole group (Resource groups → rg-ai103 → Delete resource group) and recreate it, with its Foundry resource and project, as in Session 2, before Session 21. The project endpoint will then be new, so update FOUNDRY_PROJECT_ENDPOINT.| Objective (Microsoft’s wording) | Session | Say it without notes |
|---|---|---|
| Choose an appropriate model for each task, including large language models (LLMs), small language models, multimodal models, and Foundry Tools | 1 | When does a Foundry Tool beat an LLM? Where do you compare models? |
| Choose the appropriate Foundry services for generative tasks, grounding, vector search, agent workflows, or multimodal processing; Choose an appropriate method for retrieval and indexing | 2 | Which service generates, acts, finds, extracts? Hybrid versus semantic ranker? Classic RAG or agentic retrieval? |
| Choose appropriate memory, tool, and knowledge integration services for agent solutions | 11 | Which built-in tool fits each need? How is state kept? |
| Design Azure infrastructure for AI apps and agent-based solutions; Choose appropriate deployment options | 3 | Basic versus standard agent setup; Serverless API versus Managed compute; Global Standard versus Data Zone Standard |
| Configure model and agent deployments | 4 | Deployment name versus model name; version update policies; agent versions |
| Manage quotas, scaling, rate limits, and cost footprints for model and agent workloads | 18 | What quota is assigned by; what counts towards TPM; 429 handling; PTUs |
| Monitor model performance, drift, safety events, and grounding quality | 19 | Which metric counts filter rejections? How is drift detected? |
| Monitor data ingestion quality, search index health, and relevance performance | 19 | Why can a successful indexer run lose documents? |
The study guide says most questions cover generally available features, and the exam may include preview features if they are commonly used. Learn the current name first, and know which features Learn marks as preview.
| Objective (Microsoft’s wording) | Session | Say it without notes |
|---|---|---|
| Configure an application to connect to a Foundry project; Deploy and consume LLMs, small models, code models, and multimodal models | 5 | Package, client class, credential, endpoint; Responses or Chat Completions? |
| Tune generation behavior, such as prompt engineering and adjusting model parameters | 6 | Which parameters change randomness and length? |
| Implement retrieval-augmented generation (RAG) in an application; Design workflows, tool-augmented flows, and multistep reasoning pipelines | 7, 8 | How is a cited, grounded answer built? How does a tool flow chain steps? |
| Evaluate models and apps, including detecting fabrications, relevance, quality, and safety; Integrate generative workflows into applications by using Foundry SDKs and connectors | 9, 10 | Which evaluator detects fabrication? How does code use a connection, not a key? |
| Define agent roles, goals, conversation-tracking approach, and tool schemas; Build agents that integrate retrieval, function-calling, and conversation memory | 12, 13 | Where do role and goals live? Who executes a function tool? |
| Integrate agent tools, including APIs, knowledge stores, search, content understanding, and custom functions; Implement orchestrated multi-agent solutions; Orchestrate multiple models, flows, or hybrid LLM and rules engines | 14, 15 | Which tool type fits each source? Where do rules sit around a model? |
| Build autonomous or semiautonomous workflows with safeguards and approval flow controls; Implement model reflection, chain-of-thought evaluations, and self-critique loops | 16 | require_approval values; where a function tool’s gate lives; maker-checker conditions |
| Integrate monitoring into deployed agents, evaluate agent behavior, and perform error analysis; Set up observability by implementing tracing, token analytics, safety signals, and latency breakdowns | 17 | The tracing ordering rule; where traces live; the role to read them |
The score is scaled, so 700 of 1,000 may not be 70% of the points. There is no penalty for guessing, so never leave a question blank. After a break, you cannot return to questions you viewed before it.
# 1. Every resource group, then every resource in the course group
az group list -o table
az resource list -g rg-ai103 -o table
# 2. Model deployments on the Foundry resource: delete these first
az cognitiveservices account deployment list \
-n <your-foundry-resource-name> -g rg-ai103 -o table
az cognitiveservices account deployment delete \
--deployment-name "<deployment-name>" \
-n <your-foundry-resource-name> -g rg-ai103
# 3. Resource IDs, then delete any resource you no longer need
az resource list -g rg-ai103 --query "[].id" -o tsv
az resource delete --ids "<resource-id>"
# 4. Foundry resources deleted in the last 48 hours (soft-deleted)
az cognitiveservices account list-deleted -o table
# Only if you are finishing or pausing the course: everything at once
# az group delete -n rg-ai103<a table of resource groups>
<a table of resources in rg-ai103: name, type, location>
<a table of deployments, or nothing if none remain>
<a table of soft-deleted Foundry resources, or nothing>| What you may have | Does it cost while idle? | Decision today |
|---|---|---|
| Global Standard model deployments | Billed per token used | Delete them all; redeploy when a session needs one |
| Any provisioned or fine-tuned deployment | Yes, while deployed | Delete now |
| Azure AI Search service and the storage account behind its indexer (Sessions 7, 10, 19) | Search on dedicated pricing: yes, until deleted. Storage: for what it holds | Delete both unless needed this week |
| Application Insights and its Log Analytics data (Session 17) | Data ingested and retained | Keep if you want the trace history or will trace in Sessions 24 and 40; otherwise delete it and reconnect one in Session 40 |
| Continuous evaluation rule (Session 19) | Yes, while enabled: it calls models | Switch off with enabled=False |
| Agent versions (including the Session 19 agent), conversations, vector stores, memory stores and uploaded files | Vector stores: File Search Storage while they exist (Session 13). The rest: only when used or stored | Delete them all, with the Session 13 cleanup.py and delete_file.sh |
| Project connections (Azure AI Search from Session 10, Application Insights from Session 17) | No separate charge described on Learn’s connections page | Remove any that point to a deleted resource |
Foundry resource and project in rg-ai103 | You pay for the services you use in it | Keep for Week 5, or delete the group |
az cognitiveservices account purge --location <region> --resource-group rg-ai103 --name <resource-name>. It needs a role such as Cognitive Services Contributor at subscription level, and cannot be undone.Delete deployments before deleting a Foundry resource. A deleted resource is soft-deleted and recoverable for 48 hours, its name cannot be reused until then, and provisioned deployments on it keep billing until it is purged.
rg-ai103, recreate it with its Foundry resource and project, as in Session 2, before Session 21, and update FOUNDRY_PROJECT_ENDPOINT.Global Standard | tier | Pay-per-token deployment type that may process data in any Azure region. Confused with Data Zone Standard. | Learn → |
Data Zone Standard | tier | Pay-per-token deployment type that keeps processing inside a data zone such as the EU. | Learn → |
Serverless API | feature | The preferred deployment option, billed by tokens or PTUs. Confused with Managed compute. | Learn → |
Managed compute | feature | Deployment option billed hourly per accelerator SKU, for models not offered serverless. | Learn → |
standard agent setup | setting | Agent data kept in your own Cosmos DB, Storage and AI Search, not Microsoft-managed storage. | Learn → |
Foundry User | role | The least-privilege developer role on a project, formerly Azure AI User. | Learn → |
Hybrid search | feature | Keyword and vector queries in one request, merged with RRF. Not the same as the semantic ranker. | Learn → |
Semantic ranker | feature | Re-ranks results a query has already retrieved; it does not retrieve on its own. | Learn → |
Agentic retrieval | feature | LLM-assisted query planning across sources for agents. Confused with classic RAG. | Learn → |
previous_response_id | parameter | Chains a Responses call to an earlier one. Confused with a conversation object. | Learn → |
Tokens-per-Minute (TPM) | concept | The unit of quota and of a deployment’s rate limit. Confused with billed tokens. | Learn → |
require_approval | parameter | MCP tool approval setting, default always. Function tools are gated in your own code instead. | Learn → |
AIProjectInstrumentor | class | Enables Foundry tracing, only if the tracing environment variable is set first. | Learn → |
builtin.groundedness | setting | Detects fabrication against the given context. Confused with relevance to the query. | Learn → |
A table of objectives you could not explain, each with the exact gap, the names to learn, the Learn page that repairs it and a re-drill date.
A score converted to a 1–1,000 scale. Microsoft says 700 may not equal 70% of the points.
A deleted Foundry resource can still be recovered for 48 hours, and its name cannot be reused during that time.
Permanently removing a soft-deleted resource so its name is free again. It cannot be undone.
inventory.sh before and after your clean-up. For every resource, say whether you kept or deleted it and why, based on idle cost, and whether you purged anything.list-deleted output, showing what remains.Global Standard Data Zone Standard Foundry User Hybrid search Semantic ranker Agentic retrieval| The register covers every Amber and Red objective, each row names a specific gap and a Learn page that really repairs it, and re-drill results are recorded | 35 |
| Uses the exact names from this session’s names box and Microsoft’s objective wording throughout | 20 |
| Evidence: before and after inventory output shows unused resources deleted, with each keep-or-delete decision justified by idle cost | 30 |
| The exam-day plan is specific and every claim about the exam matches Microsoft’s own pages | 15 |
| Total · pass mark 70 | 100 |
Session: 20 (the template has it ready, under the Course line), or the submission is refused. Upload the Word file (.docx), or switch to the Google Doc tab and paste your document’s share link.Marked against the rubric above and returned as a written letter by email; the mark appears on your progress page. Do the self-check above before you submit.Practice Exam 1 — Mid-Course opens now. Sit it before the next session. It is how you find out what has stuck, and practice papers may be resat as often as you like.
Sit Practice Exam 1 →Closed book, no notes, and Coach Ritchie switches himself off while you sit it.