
A model that is right most of the time is not good enough for an app a hospital or a bank will use. The difference usually comes from two dials you control: the prompt and the model parameters. AI-103 tests both, and it tests the trap that catches experienced developers: reasoning models reject parameters that older chat models accept, and they want a different style of prompt. Today you run the same task under different techniques and settings, record what changed, and learn which parameter belongs to which API and which model family. That record is the start of every serious tuning job.
Microsoft.CognitiveServices/accounts/deployments): one non-reasoning chat model and one reasoning model, both Global Standard. No other resource. Session 5 deleted its deployments, so you create these fresh.az cognitiveservices account deployment delete --deployment-name <name> -n <resource> -g rg-ai103. Keep rg-ai103 and the project.instructions; in Chat Completions it is the system message.# Deploy two models on Global Standard, as in Session 4 (take names and versions from list-models).
# One NON-reasoning chat model (Learn's evaluation page uses gpt-4.1-mini as an example)
# and one reasoning model (the SDK quickstart uses gpt-5-mini).
accountName="<your-foundry-resource-name>"
az cognitiveservices account deployment create \
-n $accountName -g rg-ai103 \
--deployment-name "<chat-deployment-name>" \
--model-name "<non-reasoning-model>" \
--model-version "<version-from-list-models>" \
--model-format "<format-from-list-models>" \
--sku-capacity 1 --sku-name GlobalStandard
# Repeat with --deployment-name "<reasoning-deployment-name>" and the reasoning model.
export FOUNDRY_PROJECT_ENDPOINT="https://<resource-name>.services.ai.azure.com/api/projects/<project-name>"
export FOUNDRY_MODEL_NAME="<chat-deployment-name>"
export FOUNDRY_OPENAI_BASE_URL="https://<resource-name>.openai.azure.com/openai/v1/"
# My own variable name (not Microsoft's):
export REASONING_MODEL_DEPLOYMENT="<reasoning-deployment-name>"| Technique | What you do | Why it helps |
|---|---|---|
| Clear instructions, placed first | State the task, the audience and the limits at the start | The model knows the job before it reads the content |
| Few-shot learning | Include one or more input and output pairs as examples | Shows the format and tone more reliably than describing them |
| Repeat instructions at the end | Restate the key rule after the content | Counters recency bias: the end of a prompt can carry more weight than the beginning |
| Prime the output (a cue) | End the prompt with the first words of the answer, such as Update: | Pushes the reply into the form you want |
| Break the task down / chain of thought prompting | Ask for smaller steps, or for the steps to be shown | Helps non-reasoning models on multi-step problems |
| Specify the output structure | Say exactly what shape to return, for example a table or fixed JSON keys | Makes output predictable and easier to parse |
| Provide grounding context | Give the model the data to answer from, and tell it to use only that | Learn calls it one of the most effective ways to get reliable answers |
Learn’s prompt engineering techniques are not recommended for reasoning models such as gpt-5 and the o-series. Give reasoning models a clear goal, firm constraints and an explicit output contract instead of prescribing every intermediate step.
temperature changes how adventurous the sampling is. Learn says it can be set between 0 and 2: higher values such as 0.7 give more random, divergent output, and lower values such as 0.2 give more focused, concrete output. top_p (nucleus sampling) also controls randomness, differently: at 0.1 the model considers only the tokens in the top 10% of probability mass.max_output_tokens capping the reply.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()
# The instructions play the part of the system message
INSTRUCTIONS = (
"You write one-line delivery updates for a logistics firm's customers. "
"Use only the facts given. If a fact is missing, write 'Update pending'. "
"Reply in under 25 words, with no greeting."
)
# One worked example (few-shot), then the real case, then a cue ("Update:")
PROMPT = (
"Facts: parcel LG-104, left Lagos hub Monday, held at customs.\n"
"Update: LG-104 left our Lagos hub on Monday and is held at customs.\n\n"
"Facts: parcel LG-221, reached Abuja depot Tuesday, out for delivery.\n"
"Update:"
)
for temp in (0.0, 1.0, 1.8):
for run in (1, 2):
response = openai.responses.create(
model=os.environ["FOUNDRY_MODEL_NAME"], # a NON-reasoning deployment
instructions=INSTRUCTIONS,
input=PROMPT,
temperature=temp,
max_output_tokens=60,
)
print(f"temperature={temp} run={run}: {response.output_text}")temperature=0.0 run=1: LG-221 reached our Abuja depot on Tuesday and is out for delivery.
temperature=0.0 run=2: <almost certainly the same line>
temperature=1.0 run=1: <similar facts, different wording>
...
temperature=1.8 run=2: <the most varied wording; read it for invented facts>temperature ranges from 0 to 2 and top_p from 0 to 1; adjust one of them, not both. Lower values give more focused, repeatable output; higher values give more varied output.
PROMPT and run at 0.0 again. If the format drifts, you have just measured what few-shot learning was doing for you.temperature, top_p, max_output_tokens, instructions, reasoning and text, but not stop or the penalties. So to use those, you call chat.completions.create through the OpenAI SDK route you built in Session 5.stop: the API reference describes up to 4 sequences where the API will stop generating further tokens, and the returned text does not contain the stop sequence. frequency_penalty and presence_penalty each take a number between -2.0 and 2.0: positive frequency values reduce verbatim repetition, and positive presence values push the model towards new topics.max_tokens is now deprecated in favour of max_completion_tokens, which the reference defines as an upper bound that includes visible output tokens and reasoning tokens.import os
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from openai import OpenAI
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)
messages = [
{"role": "system", "content": "You list practical ideas as a numbered list."},
{"role": "user", "content": "Give ideas to cut paper use in a public agency office."},
]
settings = [
{}, # the model's defaults
{"stop": ["4."]}, # stop before item 4
{"frequency_penalty": 1.5, "presence_penalty": 1.5}, # discourage repetition
]
for extra in settings:
completion = client.chat.completions.create(
model=os.environ["FOUNDRY_MODEL_NAME"], # the NON-reasoning deployment
messages=messages,
max_completion_tokens=200,
**extra,
)
print(extra, "->", completion.choices[0].message.content, "\n"){} -> 1. ... 2. ... 3. ... 4. ... (as many items as fit in 200 tokens)
{'stop': ['4.']} -> 1. ... 2. ... 3. ...
{'frequency_penalty': 1.5, 'presence_penalty': 1.5} -> <a list with fewer repeated words and more varied ideas>4. itself; the third should repeat itself less. If a penalty makes the list odd, set it lower.| Parameter | Where it lives | Non-reasoning models | Reasoning models (GPT-5 series, o-series) |
|---|---|---|---|
temperature, top_p | Responses and Chat Completions | Supported | Not supported |
frequency_penalty, presence_penalty | Chat Completions | Supported, -2.0 to 2.0 | Not supported |
stop | Chat Completions | Up to 4 sequences | The reference says not supported with o3 and o4-mini; check your model |
max_output_tokens / max_completion_tokens | Responses / Chat Completions | Caps the reply | Caps reasoning, visible and formatting tokens together |
| Reasoning effort | reasoning={"effort": ...} in Responses; reasoning_effort in Chat Completions | Not applicable | Values from none upwards; which ones a model accepts varies |
temperature, top_p, presence_penalty, frequency_penalty, logprobs, top_logprobs, logit_bias and max_tokens. Your dials for a reasoning model are the effort level and the output cap.none, minimal, low, medium, high and xhigh; it says gpt-5.1 defaults to none, while models before gpt-5.1 default to medium and don’t support none. The newer reasoning page also lists max. Check your model before you choose.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()
# A clear goal, firm constraints, an explicit output contract. No "think step by step".
GOAL = (
"A hospital pharmacy has three fridges. Fridge A holds 40 vials and is 75% full; "
"B holds 60 and is half full; C holds 25 and is empty. "
"Place 30 new vials, filling the fullest fridge first. Answer in one line."
)
for effort in ("low", "high"):
response = openai.responses.create(
model=os.environ["REASONING_MODEL_DEPLOYMENT"], # a reasoning deployment
input=GOAL,
reasoning={"effort": effort},
max_output_tokens=4000, # one cap for reasoning AND visible tokens
)
used = response.usage.output_tokens_details.reasoning_tokens
print(f"effort={effort} reasoning_tokens={used}: {response.output_text}")
# Now add temperature=0.2 to the call above, run it once, and read the error.effort=low reasoning_tokens=<a number>: 10 vials to Fridge A, 20 to Fridge B.
effort=high reasoning_tokens=<a larger number>: 10 vials to Fridge A, 20 to Fridge B.
<with temperature added: a 400 error saying the parameter is not supported by this model>Reasoning models reject temperature, top_p, the penalties and max_tokens. Tune them with reasoning effort and cap output with max_output_tokens (Responses) or max_completion_tokens (Chat Completions); reasoning tokens count, and are billed, as output.
output_tokens_details.reasoning_tokens, the Responses API field Learn names. Higher effort usually spends more reasoning tokens on the same one-line answer: cost for nothing on a simple task.temperature, top_p and the penalties; which parameters exist only in Chat Completions; max_tokens versus max_completion_tokens versus max_output_tokens; and the two spellings of reasoning effort.system message | concept | Instructions and context given to the model to guide its responses; sent first in chat APIs. | Learn → |
few-shot learning | concept | Including example input and output pairs in the prompt to show the model what you want. | Learn → |
recency bias | concept | The tendency for the end of a prompt to influence output more than the beginning. | Learn → |
Chain of thought prompting | concept | Asking a non-reasoning model to proceed step by step and show the steps; not for reasoning models. | Learn → |
grounding data | concept | Data you give the model to draw its answer from, for reliable, up-to-date responses. | Learn → |
temperature | parameter | Sampling randomness, from 0 to 2; lower is more focused. Not supported by reasoning models. | Learn → |
top_p | parameter | Nucleus sampling, an alternative to temperature; change one or the other, not both. | Learn → |
max_output_tokens | parameter | Responses API cap on generated tokens, including reasoning tokens. | Learn → |
max_completion_tokens | parameter | Chat Completions cap on generated tokens, including reasoning tokens; replaces the deprecated max_tokens. | Learn → |
stop | parameter | Chat Completions: up to four sequences at which generation stops. | Learn → |
frequency_penalty | parameter | Chat Completions, -2.0 to 2.0; positive values reduce verbatim repetition. | Learn → |
presence_penalty | parameter | Chat Completions, -2.0 to 2.0; positive values push the model towards new topics. | Learn → |
reasoning_effort | parameter | Chat Completions setting for how much a reasoning model reasons; Responses uses reasoning={"effort": ...}. | Learn → |
A few words at the end of a prompt that start the answer, so the model continues in the form you want.
A model, such as the GPT-5 series or the o-series, that spends reasoning tokens before answering and takes effort settings instead of sampling settings.
Tokens a reasoning model generates while thinking; you do not see them, but they count towards the output cap and are billed as output.
A precise statement in the system message of the shape the answer must take, such as JSON with fixed keys.
temperature=0.2 and the prompt says “think step by step”. What should they change?temperature or top_p, then stop or a penalty in Chat Completions) and record the effect.system message few-shot learning temperature max_completion_tokens max_output_tokens reasoning_effort| The after prompt is a real improvement, uses at least three named techniques correctly, and avoids chain-of-thought prompting on the reasoning model | 30 |
| Parameters are changed one at a time, each change is recorded with its exact name and value, and the conclusions follow from the runs | 25 |
| Uses the exact parameter and technique names from this session’s names box, including the right name for each API | 20 |
| Evidence: terminal output supports every row of the table, including reasoning token counts and the rejected-parameter error, with no keys or endpoints visible and clean-up stated | 25 |
| Total · pass mark 70 | 100 |
Session: 6 (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.Most AI apps that earn their keep answer questions about an organisation’s own content: policies, manuals, contracts, case notes. A model cannot know those, so you retrieve the right passages and hand them over with the question. That is retrieval-augmented generation, and AI-103 expects you to build it, not just define it: chunks, embeddings, a hybrid query, grounded instructions and citations. It also expects you to know which RAG approaches are current, because one well-known option is being retired. Today you build a small RAG app in Python over your own documents, and you pay for a search service by the hour, so you also delete it.
Microsoft.Search/searchServices) on the Basic tier in rg-ai103, with one index; two model deployments on your Foundry resource (Microsoft.CognitiveServices/accounts/deployments): a chat model and text-embedding-3-small; and two role assignments for your own account on the search service.az search service delete --name <your-search-name> --resource-group rg-ai103 --yes, or in the Azure portal open the search service and select Delete. Then delete both deployments (Build → Models, or az cognitiveservices account deployment delete). Keep rg-ai103, the project and your build_index.py: Session 10 recreates the service in minutes.| Approach | What it is | Status and when to choose it |
|---|---|---|
| Build it yourself (classic RAG) with the SDKs | Your code runs the query, builds the prompt and calls the model | Full control of the pipeline. What you build today |
| Agentic retrieval / Foundry IQ | A model breaks complex input into focused subqueries, runs them in parallel and returns grounding data with citations | Learn’s modern approach for agents and conversational questions; Session 27 |
| Agent tools (file search, Azure AI Search tool) | The agent retrieves as one of its tools | When you build an agent; Weeks 3 and 6 |
| Azure OpenAI On Your Data | Managed grounding of GPT-4o models on your data sources | Deprecated. At the time of writing, Learn says it will be retired on October 14, 2026, and points to Foundry Agent Service with Foundry IQ |
Azure OpenAI On Your Data is deprecated and approaching retirement, with no new models being onboarded. For new grounded solutions, choose your own RAG code over Azure AI Search, agentic retrieval, or Foundry Agent Service with Foundry IQ.
text-embedding-3-small, which Learn lists with 1,536 output dimensions. At the time of writing, Learn’s embeddings page gives 8,192 tokens as the maximum input for current embedding models. The index’s vector field must declare the same number of dimensions as the model produces./openai/v1) for embeddings, because the project endpoint used by the Foundry SDK doesn’t currently route embedding requests. So embeddings.create goes through the same OpenAI client you built in Session 5, while the chat call can stay on get_openai_client().Embed the query with the same model you used to embed the content, and match the vector field’s dimensions to it. A hybrid query sends search_text and a vector query together; citations come from ids you pass with each chunk.
id, the app prints it next to the text in the prompt, and the instructions demand it after each fact. Learn’s RAG quickstart uses the same grounding instructions: answer only with the facts in the sources, and say you don’t know if they are not enough.leave-2 says staff may take five days of compassionate leave. The answer “five days [leave-2]” lets a nurse check the source in one click.az search service and on Session 4. --auth-options aadOrApiKey turns on Microsoft Entra ID for the data plane alongside keys; you will never use the keys. In the portal the same choice is under Settings → Keys.# 1. The search service. Basic tier: billed by the hour for as long as it exists.
az search service create \
--name "<your-search-name>" \
--resource-group rg-ai103 \
--sku basic \
--auth-options aadOrApiKey \
--aad-auth-failure-mode http401WithBearerChallenge
# 2. Two deployments on your Foundry resource (Session 4 steps):
# a chat model as before, and text-embedding-3-small for vectors.
az cognitiveservices account deployment create \
-n "<your-foundry-resource-name>" -g rg-ai103 \
--deployment-name "<embedding-deployment-name>" \
--model-name text-embedding-3-small \
--model-version "<version-from-list-models>" \
--model-format "<format-from-list-models>" \
--sku-capacity 1 --sku-name "<sku-from-list-models>"
pip install azure-search-documents
# FOUNDRY_PROJECT_ENDPOINT and FOUNDRY_OPENAI_BASE_URL stay as in Session 5
export FOUNDRY_MODEL_NAME="<chat-deployment-name>"
# My own variable names (not Microsoft's):
export SEARCH_ENDPOINT="https://<your-search-name>.search.windows.net"
export SEARCH_INDEX="ai103-policies"
export EMBEDDING_DEPLOYMENT="<embedding-deployment-name>"SearchIndexClient creates the index, a SearchField declares vector_search_dimensions and a vector profile, HnswAlgorithmConfiguration names the algorithm, and SearchClient uploads the documents. The only addition is that we compute each vector with your embedding deployment instead of loading precomputed ones.import os
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from azure.search.documents import SearchClient
from azure.search.documents.indexes import SearchIndexClient
from azure.search.documents.indexes.models import (
SearchIndex, SimpleField, SearchableField, SearchField, SearchFieldDataType,
VectorSearch, HnswAlgorithmConfiguration, VectorSearchProfile)
from openai import OpenAI
credential = DefaultAzureCredential()
endpoint, index_name = os.environ["SEARCH_ENDPOINT"], os.environ["SEARCH_INDEX"]
embedder = OpenAI(base_url=os.environ["FOUNDRY_OPENAI_BASE_URL"],
api_key=get_bearer_token_provider(credential, "https://ai.azure.com/.default"))
fields = [
SimpleField(name="id", type=SearchFieldDataType.String, key=True, filterable=True),
SearchableField(name="title", type=SearchFieldDataType.String),
SearchableField(name="content", type=SearchFieldDataType.String),
SearchField(name="content_vector", type=SearchFieldDataType.Collection(SearchFieldDataType.Single),
searchable=True, vector_search_dimensions=1536, # text-embedding-3-small
vector_search_profile_name="my-vector-profile"),
]
vector_search = VectorSearch(
algorithms=[HnswAlgorithmConfiguration(name="my-hnsw-vector-config-1")],
profiles=[VectorSearchProfile(name="my-vector-profile",
algorithm_configuration_name="my-hnsw-vector-config-1")])
SearchIndexClient(endpoint=endpoint, credential=credential).create_or_update_index(
SearchIndex(name=index_name, fields=fields, vector_search=vector_search))
# Your own chunks: one paragraph each, with a stable id you can cite
chunks = [
{"id": "leave-1", "title": "Leave policy", "content": "<a paragraph from your document>"},
{"id": "leave-2", "title": "Leave policy", "content": "<the next paragraph>"},
]
for chunk in chunks:
chunk["content_vector"] = embedder.embeddings.create(
model=os.environ["EMBEDDING_DEPLOYMENT"], input=chunk["content"]).data[0].embedding
result = SearchClient(endpoint=endpoint, index_name=index_name,
credential=credential).upload_documents(documents=chunks)
print(f"Uploaded {len(result)} chunks to {index_name}")Uploaded 2 chunks to ai103-policiestext-embedding-3-small or the field does not say 1536.VectorizedQuery), augments the prompt with labelled sources, and generates with grounded instructions through your project’s OpenAI client. The search part follows Learn’s vector and RAG quickstarts; the generation part follows the Foundry SDK quickstart you used in Session 5.import os
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from azure.ai.projects import AIProjectClient
from azure.search.documents import SearchClient
from azure.search.documents.models import VectorizedQuery
from openai import OpenAI
credential = DefaultAzureCredential()
embedder = OpenAI(base_url=os.environ["FOUNDRY_OPENAI_BASE_URL"],
api_key=get_bearer_token_provider(credential, "https://ai.azure.com/.default"))
search = SearchClient(endpoint=os.environ["SEARCH_ENDPOINT"],
index_name=os.environ["SEARCH_INDEX"], credential=credential)
project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=credential)
openai = project.get_openai_client()
query = "How many days of compassionate leave can I take?"
# 1 Retrieve: a hybrid query sends the keywords AND the query's own embedding
vector = embedder.embeddings.create(
model=os.environ["EMBEDDING_DEPLOYMENT"], input=query).data[0].embedding
results = search.search(
search_text=query,
vector_queries=[VectorizedQuery(vector=vector, k_nearest_neighbors=3, fields="content_vector")],
select=["id", "title", "content"],
top=3,
)
# 2 Augment: label every source with its id so the model can cite it
sources = "\n".join(f"[{doc['id']}] {doc['title']}: {doc['content']}" for doc in results)
# 3 Generate: grounded instructions, then the question and the sources
response = openai.responses.create(
model=os.environ["FOUNDRY_MODEL_NAME"],
instructions=("Answer ONLY with facts from the sources. Cite each fact with its [id]. "
"If the sources don't contain the answer, say you don't know."),
input=f"Question: {query}\n\nSources:\n{sources}",
)
print(sources, "\n---\n", response.output_text)[leave-2] Leave policy: <your paragraph>
[leave-1] Leave policy: <your paragraph>
---
<a short answer using only those paragraphs, with [leave-2] after the fact it came from>In an application RAG flow, Azure AI Search retrieves, your code augments the prompt with the retrieved content, and the model generates. Grounding instructions tell the model to answer only from the sources and to say when it cannot.
text-embedding-3-small and its dimensions; why embeddings use the OpenAI SDK endpoint; SearchIndexClient, SearchClient, VectorizedQuery; the two search roles; and the retirement of Azure OpenAI On Your Data.az search service delete (or Delete on the service in the portal), then delete both deployments. Session 10 recreates the service with the same commands and your build_index.py.Azure OpenAI On Your Data | feature | Managed grounding of GPT-4o models on your data; deprecated and approaching retirement. | Learn → |
text-embedding-3-small | model | An embedding model sold by Azure, producing 1,536-dimension vectors. | Learn → |
embeddings.create | method | The OpenAI SDK call that turns input text into an embedding vector. | Learn → |
Text Split skill | feature | The built-in indexer skill that chunks text by pages or sentences. | Learn → |
azure-search-documents | package | The Python package for Azure AI Search index management and queries. | Learn → |
SearchIndexClient | class | Creates, updates and deletes search indexes. | Learn → |
SearchClient | class | Uploads documents to one index and queries it. | Learn → |
VectorizedQuery | class | A vector query built from an embedding you computed; pass it in vector_queries. | Learn → |
vector_search_dimensions | parameter | The number of dimensions a vector field holds; must match the embedding model. | Learn → |
Search Index Data Contributor | role | Data-plane role to load, change and query documents in an index. | Learn → |
Search Service Contributor | role | Role to create and manage indexes and other objects on a search service. | Learn → |
Dedicated pricing model | tier | Provisioned search capacity, including Basic, billed at a fixed hourly rate while the service exists. | Learn → |
az search service delete | cli | Deletes a search service and its data; the only way to stop Dedicated billing. | Learn → |
A pattern where the app retrieves relevant content and gives it to the model with the question, so the answer is grounded in that content.
A small, self-contained piece of a larger document, sized to fit model limits and carry one idea.
A single search request carrying both keyword text and a vector query, whose results are merged into one ranked list.
A reference in the answer to the specific retrieved source a fact came from, so a reader can check it.
text-embedding-3-small. At query time, what must the app do for the vector part of a hybrid query?build_index.py adapted to your chunks.rag.py to your domain. Run three questions: one fully answered by your documents, one partly answered, and one not answered at all. Keep the printed sources and the answers.build_index.py and of all three rag.py questions, showing the retrieved sources and the cited answers.az search service delete, or a screenshot of the resource group without the search service afterwards.SearchIndexClient SearchClient VectorizedQuery text-embedding-3-small Search Index Data Contributor Azure OpenAI On Your Data Dedicated pricing model| The RAG script works over the student’s own chunks, answers with correct citations, and declines the unanswerable question | 35 |
| The explanation of chunking, embeddings, the hybrid query and the choice of RAG approach is accurate and specific to this code | 20 |
| Uses the exact class, model, role and pricing names from this session’s names box | 20 |
| Evidence: outputs for indexing and all three questions, the tier and role screenshots, and proof the search service was deleted, with no keys or endpoints visible | 25 |
| Total · pass mark 70 | 100 |
Session: 7 (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.A model on its own can only write. The moment an app needs today’s shipment status, a stock level or an approval, the model has to ask your code to fetch or do something, and then carry on with the result. That is a tool-augmented flow, and chained together it becomes a multistep pipeline. AI-103 asks you to design these: when a single call is enough, when to add tools, how the loop works, and which orchestration pattern fits. At work, the design decides whether the flow is safe and traceable or quietly unbounded. Today you build a two-step flow with a real function call, extend it to a guarded loop, and trace every step.
Microsoft.CognitiveServices/accounts/deployments), Global Standard. The function you call is local Python on your machine; no other Azure resource is created.az cognitiveservices account deployment delete --deployment-name <name> -n <resource> -g rg-ai103. Keep rg-ai103 and the project.The model never executes a function. It returns the function name and structured arguments; your application runs the function and sends the result back, so you remain in control of the actions taken.
FOUNDRY_MODEL_NAME at it. If you choose a reasoning model, note an error Learn’s reasoning page shows: gpt-5.6-sol refuses function tools with reasoning effort in Chat Completions and points you to the Responses API, which is the API we use."type": "function", a name, a description the model reads to decide when to call it, and parameters as a JSON schema. Learn’s function calling page limits descriptions to 1,024 characters at the time of writing.DefaultAzureCredential as in Session 5. The model’s request arrives as an output item of type function_call with a call_id and JSON arguments. You answer with a function_call_output item carrying the same call_id, chained with previous_response_id.import json
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"]
# Illustrative data. In real life this is your shipment system's API.
SHIPMENTS = {"LG-221": {"status": "held at customs", "hub": "Lagos", "eta_days": 3},
"LG-104": {"status": "delivered", "hub": "Abuja", "eta_days": 0}}
def get_shipment_status(tracking_id):
return SHIPMENTS.get(tracking_id, {"error": "unknown tracking id"})
tools = [{"type": "function", "name": "get_shipment_status",
"description": "Look up the live status of one parcel by its tracking id",
"parameters": {"type": "object",
"properties": {"tracking_id": {"type": "string"}},
"required": ["tracking_id"]}}]
# Step 1: the model decides whether to call the tool, and with which arguments
response = openai.responses.create(model=MODEL, tools=tools,
input="Where is parcel LG-221, and when will it arrive?")
tool_outputs = []
for item in response.output:
if item.type == "function_call" and item.name == "get_shipment_status":
args = json.loads(item.arguments)
result = get_shipment_status(args["tracking_id"]) # YOUR code runs it
print(f"TRACE 1: {item.name}({args}) call_id={item.call_id} -> {result}")
tool_outputs.append({"type": "function_call_output",
"call_id": item.call_id, "output": json.dumps(result)})
# Step 2: send the result back; the model writes the answer (skipped if no tool was called)
if tool_outputs:
response = openai.responses.create(model=MODEL, previous_response_id=response.id, input=tool_outputs)
print(f"TRACE 2: {response.output_text}")TRACE 1: get_shipment_status({'tracking_id': 'LG-221'}) call_id=call_<id> -> {'status': 'held at customs', 'hub': 'Lagos', 'eta_days': 3}
TRACE 2: Parcel LG-221 is held at customs in Lagos and is expected in about 3 days.SHIPMENTS.function_call item, so the script skips step 2. That is the default tool_choice, auto: the model decides whether to call a tool at all. Learn also documents none, and naming a specific function to force it.arguments as untrusted input. The model wrote them. Validate them the way you would validate a form a stranger filled in.# loop.py: the same imports, client, SHIPMENTS, function and tools as tool_flow.py,
# with Step 1 and Step 2 replaced by this loop.
FUNCTIONS = {"get_shipment_status": get_shipment_status} # the ONLY functions the model may reach
MAX_STEPS = 5 # never let a loop run unbounded
response = openai.responses.create(
model=MODEL, tools=tools, tool_choice="auto",
input="Check parcels LG-221, LG-104 and LG-999. Which ones need a follow-up call?",
)
for step in range(1, MAX_STEPS + 1):
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
break # no more tool requests: the model has answered
outputs = []
for call in calls: # may be several calls in one response
if call.name not in FUNCTIONS:
result = {"error": f"unknown tool {call.name}"}
else:
result = FUNCTIONS[call.name](**json.loads(call.arguments))
print(f"TRACE step {step}: {call.name}({call.arguments}) -> {result}")
outputs.append({"type": "function_call_output",
"call_id": call.call_id, "output": json.dumps(result)})
response = openai.responses.create(model=MODEL, tools=tools,
previous_response_id=response.id, input=outputs)
else:
print("TRACE: reached MAX_STEPS; the answer below may be incomplete")
print(f"TRACE final: {response.output_text}")TRACE step 1: get_shipment_status({"tracking_id":"LG-221"}) -> {'status': 'held at customs', ...}
TRACE step 1: get_shipment_status({"tracking_id":"LG-104"}) -> {'status': 'delivered', ...}
TRACE step 1: get_shipment_status({"tracking_id":"LG-999"}) -> {'error': 'unknown tracking id'}
TRACE final: <LG-221 needs a call about customs; LG-999 needs a call to check the tracking id; LG-104 is delivered>FUNCTIONS is an allow-list: the model can only reach what you put there. MAX_STEPS bounds cost and runaway behaviour. And every step is printed, so you can show exactly what happened, in order.parallel_tool_calls as whether to enable parallel function calling during tool use, defaulting to true; the Responses API reference lists the same parameter.In a tool loop, match each function_call_output to its call_id, keep calling until the model stops requesting tools, and bound the loop. Parallel function calls return several calls in one response.
get_free_beds(ward) for three wards in one response, then reserve_bed(bed_id) in the next. The first is safe to automate. The second changes the real world, and Session 16 adds an approval step before a call like that runs.| Pattern (Architecture Center name) | How it works | Choose it when |
|---|---|---|
| Sequential orchestration | Steps run in a fixed, linear order, each using the previous output; also known as a pipeline or prompt chaining | The process is well defined and each stage has a clear input and output |
| Concurrent orchestration | Several agents work on the same task at the same time, each from its own specialism | Independent analyses can run side by side and be combined |
| Group chat orchestration (including maker-checker) | Agents share a conversation; a maker proposes, a checker reviews against criteria, up to an iteration limit | Work needs review or debate before it is accepted |
| Handoff orchestration | An agent decides whether to handle the task or pass it to a better-suited agent | The right specialist is only clear once the task is understood |
| Magentic orchestration | A manager agent builds a task ledger of goals and subgoals, then tracks it to completion | Open-ended problems with no predetermined plan: the planner/executor shape |
For new code-first multi-agent workflows, Learn points to Microsoft Agent Framework (Python package agent-framework). Foundry’s visual workflows are in preview and scheduled for retirement.
function_call, call_id, function_call_output, previous_response_id; the tool_choice values; the five orchestration patterns; and the retirement of Foundry workflows in favour of Microsoft Agent Framework.rg-ai103 and the project.MAX_STEPS, send me the trace through “Ask R. Wingo” today. That is a design problem worth talking through, not a bug to hide.tool_choice | parameter | Controls whether the model may call tools: auto (default), none, or a named function to force. | Learn → |
function_call_output | parameter | Responses API input item that returns your function’s result, matched by call_id. | Learn → |
call_id | parameter | The identifier on a function_call item that your function_call_output must repeat. | Learn → |
parallel_tool_calls | parameter | Whether to enable parallel function calling during tool use; true by default in Chat Completions. | Learn → |
Sequential orchestration | concept | Agents or steps run in a fixed linear order, each using the previous output. | Learn → |
prompt chaining | concept | Another name for sequential orchestration: one call’s output becomes the next call’s input. | Learn → |
Magentic orchestration | concept | A manager agent builds and tracks a task ledger for open-ended problems. | Learn → |
Handoff orchestration | concept | Agents decide whether to handle a task or transfer it to a better-suited agent. | Learn → |
Human in the loop | feature | Workflow pattern that asks the user a question and waits for input before continuing. | Learn → |
Microsoft Agent Framework | tool | Open-source SDK for agents and code-first multi-agent workflows; Learn’s path for new workflows. | Learn → |
agent-framework | package | The pip package for Microsoft Agent Framework in Python. | Learn → |
A model returning the name and arguments of a function you described, so your code can run it and send back the result.
Repeating model calls and tool executions until the model answers in text or a step limit is reached.
The fixed set of functions your code will run for the model; anything else is refused.
A step-by-step record of each model request, tool call, arguments and result in a flow.
get_card_status function?description and a JSON schema for its parameters. Build the two-step flow from tool_flow.py, then extend it to the guarded loop from loop.py with an allow-list and a step limit.tool_choice function_call_output call_id Sequential orchestration Microsoft Agent Framework| The flow works: the tool is called with correct arguments, results are returned by call_id, the loop is bounded and allow-listed, and the no-tool case answers directly | 35 |
| The design explanation correctly places the flow on the complexity spectrum and names a fitting orchestration pattern with a reason | 20 |
| Uses the exact parameter, pattern and framework names from this session’s names box | 20 |
| Evidence: the trace shows every step for all three inputs and the deployment screenshot is included, with no keys or endpoints visible and clean-up stated | 25 |
| Total · pass mark 70 | 100 |
Session: 8 (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.Every team says its AI app works. Very few can prove it, and fewer still can prove it has not made something up. AI-103 expects you to evaluate models and apps for fabrications, relevance, quality and safety, using Foundry’s evaluators rather than your own impression. It also expects the current names, and this is an area where Microsoft has changed them: the class names you will meet in older material are now the classic path, and the current Foundry path identifies evaluators differently. Today you run a real evaluation over a small dataset, read the results in code and in the portal, and turn a failing score into a fix.
Microsoft.CognitiveServices/accounts/deployments), Global Standard; and one evaluation with one run, stored in your Foundry project. No search service is needed: the dataset is typed into the script.az cognitiveservices account deployment delete --deployment-name <name> -n <resource> -g rg-ai103). Keep rg-ai103 and the project.| Evaluator group | Examples (built-in) | What it answers |
|---|---|---|
| General purpose | Coherence, Fluency | Is the answer well written and logical? |
| RAG | Groundedness, Groundedness Pro (preview), Relevance, Retrieval, Response Completeness (preview), Document Retrieval | Did it stick to the sources, answer the question, and retrieve the right content? |
| Risk and safety | Hate and Unfairness, Sexual, Violence, Self-Harm, Protected Materials, Indirect Attack (XPIA), Code Vulnerability | Is the content harmful or unsafe? |
| Agent | Task Adherence, Intent Resolution, Tool Call Accuracy, and others | Did the agent do the task properly? (Week 4) |
| Textual similarity | Similarity, F1 Score, BLEU, GLEU, ROUGE, METEOR | How close is the answer to a known correct answer? |
Groundedness detects fabrication: content not supported by the provided context. Relevance checks the response against the query. Response Completeness checks coverage against ground truth. Retrieval checks whether the retrieved context suits the query.
azure-ai-evaluation, and it exposes evaluator classes such as GroundednessEvaluator and RelevanceEvaluator, run locally with an evaluate() function. Learn now files that how-to under the Foundry (classic) portal and says it isn’t available for the new Foundry portal.openai_client.evals.create(), start it with evals.runs.create(), and name each evaluator with an identifier such as builtin.groundedness inside a TestingCriterionAzureAIEvaluator.| Classic (Foundry classic portal) | Current (new Foundry) | |
|---|---|---|
| Package | azure-ai-evaluation | azure-ai-projects with the project’s OpenAI client |
| How an evaluator is named | A class: GroundednessEvaluator(model_config) | An identifier: evaluator_name="builtin.groundedness" |
| Where it runs | Locally, with evaluate() | In the cloud, as an evaluation and a run in your project |
| How fields are wired | column_mapping | data_mapping with {{item.field}} templates |
Recognise both forms: azure-ai-evaluation with classes such as GroundednessEvaluator (classic, local), and cloud evaluation with evals.create, testing_criteria and builtin.groundedness identifiers (current Foundry).
initialization_parameters={"model": ...}; the newer agent evaluation how-to and the RAG evaluator page use {"deployment_name": ...}. I follow the newest page. If the service rejects it, try the other and tell me which worked.# Week 1 installed 2.3.0 or later; the newest evaluation how-to needs 2.4.0 or later.
# Upgrade once now: 2.4.0 is the course floor from here on.
pip install --upgrade "azure-ai-projects>=2.4.0" azure-identity
# A judge model deployment for AI-assisted evaluators (Session 4 steps).
# Learn's portal page gives gpt-4.1-mini as an example of a judge model.
export FOUNDRY_MODEL_NAME="<judge-deployment-name>"query, the retrieved context and the app’s response. Learn’s portal page accepts CSV and JSONL files; the SDK also takes rows inline, which is what we do. Row 2 below contains a deliberate fabrication and row 3 a deliberately irrelevant answer, so you can see the evaluators catch them.# eval_data.py: illustrative rows shaped like your Session 7 RAG app's output
SCHEMA = {
"type": "object",
"properties": {"query": {"type": "string"}, "context": {"type": "string"},
"response": {"type": "string"}},
"required": ["query", "context", "response"],
}
ROWS = [
{"query": "How many days of compassionate leave can I take?",
"context": "[leave-2] Staff may take up to five days of compassionate leave per year.",
"response": "You can take up to five days a year [leave-2]."},
{"query": "Can I carry compassionate leave into next year?",
"context": "[leave-2] Staff may take up to five days of compassionate leave per year.",
"response": "Yes, unused days roll over for twelve months [leave-2]."}, # fabricated
{"query": "Who approves leave?",
"context": "[leave-3] Line managers approve leave requests in the HR portal.",
"response": "The weather in Lagos is warm this week."}, # irrelevant
]response and, recommended, context; Relevance needs query and response; Retrieval needs query and context; Response Completeness needs ground_truth and response. These quality evaluators score 1 to 5, with a default pass threshold of 3.evals.create, then evals.runs.create with the rows supplied as file content.import os
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import TestingCriterionAzureAIEvaluator
from openai.types.eval_create_params import DataSourceConfigCustom
from openai.types.evals.create_eval_jsonl_run_data_source_param import (
CreateEvalJSONLRunDataSourceParam, SourceFileContent, SourceFileContentContent)
from eval_data import ROWS, SCHEMA
project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential())
client = project.get_openai_client()
judge = os.environ["FOUNDRY_MODEL_NAME"] # the judge model deployment
testing_criteria = [
TestingCriterionAzureAIEvaluator(
type="azure_ai_evaluator", name="groundedness", evaluator_name="builtin.groundedness",
initialization_parameters={"deployment_name": judge},
data_mapping={"context": "{{item.context}}", "response": "{{item.response}}"}),
TestingCriterionAzureAIEvaluator(
type="azure_ai_evaluator", name="relevance", evaluator_name="builtin.relevance",
initialization_parameters={"deployment_name": judge},
data_mapping={"query": "{{item.query}}", "response": "{{item.response}}"}),
TestingCriterionAzureAIEvaluator( # safety: hosted evaluation model, no judge needed
type="azure_ai_evaluator", name="violence", evaluator_name="builtin.violence",
data_mapping={"query": "{{item.query}}", "response": "{{item.response}}"}),
]
evaluation = client.evals.create(
name="ai103-s9-rag-eval",
data_source_config=DataSourceConfigCustom(type="custom", item_schema=SCHEMA),
testing_criteria=testing_criteria,
)
run = client.evals.runs.create(
eval_id=evaluation.id, name="run-1",
data_source=CreateEvalJSONLRunDataSourceParam(type="jsonl", source=SourceFileContent(
type="file_content", content=[SourceFileContentContent(item=row) for row in ROWS])),
)
print(f"python eval_results.py {evaluation.id} {run.id}")python eval_results.py <eval-id> <run-id>data_mapping lines closely: {{item.context}} means “the context field of each row”. A wrong mapping is the most common reason for a score that makes no sense.eval_run.py printed.import os
import sys
import time
from pprint import pprint
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential())
client = project.get_openai_client()
eval_id, run_id = sys.argv[1], sys.argv[2]
while True:
run = client.evals.runs.retrieve(run_id=run_id, eval_id=eval_id)
if run.status in ("completed", "failed"):
break
time.sleep(5)
print("Waiting for eval run to complete...")
# One output item per row: each evaluator's label, score, passed and threshold
pprint(list(client.evals.runs.output_items.list(run_id=run_id, eval_id=eval_id)))
print(f"Status: {run.status}")
print(f"Report URL: {run.report_url}")Waiting for eval run to complete...
[<row 1: groundedness pass, relevance pass, violence pass>,
<row 2: groundedness FAIL (the roll-over claim is not in the context), relevance pass, violence pass>,
<row 3: relevance FAIL, violence pass>]
Status: completed
Report URL: <a link to this run in the Foundry portal>run.report_url opens the same results in the Foundry portal.ai103-s9-rag-eval. Learn lists the status values as In Progress, Completed, Partial and Failed. Select the evaluation name to see the per-row results.| What failed | What it usually means | The fix to try first |
|---|---|---|
| Groundedness | The answer added facts that are not in the context | Tighten the grounding instructions and require a citation per fact (Session 7); lower temperature on non-reasoning models (Session 6) |
| Relevance | The answer did not address the question | Check retrieval first; if the context was right, fix the instructions |
| Retrieval | The retrieved chunks did not suit the query | Improve chunking, the index fields or the query (hybrid, semantic ranking) |
| A safety evaluator | Harmful content got through | Review the guardrail on the deployment (Session 23) and the system message |
evals.create, evals.runs.create, data_mapping, report_url; and the 1–5 and 0–7 scales.azure-ai-evaluation | package | The Azure AI Evaluation SDK package for local evaluation; documented under the Foundry (classic) portal. | Learn → |
GroundednessEvaluator | class | Classic SDK class that scores how well a response is grounded in context. | Learn → |
RelevanceEvaluator | class | Classic SDK class that scores how well a response addresses the query. | Learn → |
builtin.groundedness | setting | Current evaluator identifier for Groundedness in cloud evaluation. | Learn → |
builtin.relevance | setting | Current evaluator identifier for Relevance in cloud evaluation. | Learn → |
builtin.violence | setting | Risk and safety evaluator identifier; uses hosted models and a 0 to 7 severity scale. | Learn → |
Groundedness Pro | feature | Preview evaluator that checks strict consistency with context using Azure AI Content Safety. | Learn → |
Response Completeness | feature | Preview evaluator for the recall side: does the response miss information in the ground truth? | Learn → |
TestingCriterionAzureAIEvaluator | class | Foundry SDK class that adds a built-in evaluator, with its data mapping, to an evaluation. | Learn → |
testing_criteria | parameter | The list of evaluators an evaluation applies to each row. | Learn → |
data_mapping | parameter | Maps evaluator inputs to dataset fields using templates such as {{item.response}}. | Learn → |
evals.create | method | Creates an evaluation definition through the project’s OpenAI client. | Learn → |
report_url | parameter | Run property linking to the evaluation results in the Foundry portal. | Learn → |
AI red teaming agent | tool | Simulates attacks with Microsoft’s PyRIT framework to find safety and security weaknesses before deployment. | Learn → |
A tool that scores an AI response against one criterion, such as groundedness or violence.
The deployed model an AI-assisted evaluator uses to read and score each row.
Content in a response that the provided context does not support; what groundedness evaluation detects.
Rows of test cases with named fields, such as query, context and response, that an evaluation scores.
query, the retrieved context and the app’s response. Your saved Session 7 output gives you the first rows; write further queries against the same saved sources for the rest. Include at least one row you expect to fail groundedness and one you expect to fail relevance, and say which.eval_run.py adapted to your rows, using at least builtin.groundedness, builtin.relevance and one risk and safety evaluator. Poll and print the results with eval_results.py.rag.py, with your saved sources pasted into the input and your judge deployment as the model. A retrieval fix means recreating the Session 7 search service and index with setup-search.sh and build_index.py, then deleting the service again.azure-ai-evaluation classes relate to what you used.eval_results.py for both runs, showing the eval id, run id, status and per-row results.builtin.groundedness builtin.relevance TestingCriterionAzureAIEvaluator data_mapping evals.create GroundednessEvaluator| The evaluation runs on the student’s own rows with correct data mappings, and the planted failures are detected and explained | 30 |
| A fix is chosen from the results, applied, and its effect shown by a second run and an honest comparison | 25 |
| Uses the exact evaluator identifiers, class, method and package names from this session’s names box, including the classic and current forms | 20 |
| Evidence: terminal output and portal screenshots show both runs and match the table, with no keys or endpoints visible and clean-up stated | 25 |
| Total · pass mark 70 | 100 |
Session: 9 (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.A script that works on your laptop with four endpoints pasted into it is not an application. Production code finds its resources through configuration that someone else can govern, and authenticates without a single key. In Foundry, that is what project connections are for, alongside the two clients the Foundry SDK gives you and the connectors that reach business systems. AI-103 asks you to integrate generative workflows into applications with exactly these pieces, and to know which client does what. Today you connect your project to Azure AI Search, refactor your RAG script to find the service through that connection, and close Week 2.
Microsoft.Search/searchServices, Basic) and its index, plus a chat and a text-embedding-3-small model deployment (Microsoft.CognitiveServices/accounts/deployments).az search service delete --name <your-search-name> --resource-group rg-ai103 --yes (Learn: this is the only way to stop Dedicated billing), and delete both deployments (Build → Models). The connection is only a record of the target and its authentication type; you can leave it on the Connected resources tab, knowing it points to nothing until you recreate a service. Keep rg-ai103 and the project for Week 3.DefaultAzureCredential is the badge.get_openai_client(), is for work built on OpenAI concepts: the Responses API, agents, evaluations and fine-tuning.| What the app needs | Use | Python package |
|---|---|---|
| Connections, project properties, tracing | Project client (AIProjectClient) | azure-ai-projects |
| Responses, agents, evaluations, fine-tuning | OpenAI-compatible client (get_openai_client()) | azure-ai-projects (with openai) |
| Embeddings, or the full OpenAI API surface | OpenAI SDK on /openai/v1 | openai |
| Multi-agent orchestration and hosted agents | Microsoft Agent Framework | agent-framework |
| One prebuilt capability such as Speech or Language | Foundry Tools SDKs | Tool-specific |
Use the Foundry SDK’s project client for Foundry-native operations such as connections; use get_openai_client() for Responses, agents and evaluations; use the OpenAI SDK endpoint for embeddings.
| Connection type (as Learn lists it) | What it reaches | Note |
|---|---|---|
| Azure AI Search | Search indexes over vector and text data | Required for Standard agent deployment |
| Azure Storage | Unstructured data such as documents and images | Required for Standard agent deployment |
| Azure OpenAI | OpenAI models with Azure security | |
| Application Insights | Telemetry for performance and diagnosis | Used for tracing (Session 17) |
| Azure Key Vault | Secrets, stored and accessed securely | |
| API key / Custom key | Any target that needs a key, or keys plus related properties | Keys live in the connection, never in your code |
| Grounding with Bing Search | Real-time web grounding for agents |
ApiKey and AAD, which is Microsoft Entra ID. With Entra ID the connection holds no secret at all: your code signs in as you, or as a managed identity, and the service’s own role assignments decide what you may do.project.connections.get(name) returns one connection and get_default(connection_type) returns the default connection of a type, such as ConnectionType.AZURE_AI_SEARCH. Both take include_credentials, which the reference says defaults to False. Leave it that way; a keyless app has nothing to include.A project connection supplies the target and authentication type for a connected resource. With Microsoft Entra ID authentication the app uses DefaultAzureCredential and role assignments, so no key is stored in code, configuration or the connection.
# Only if you deleted the Session 7 search service. It bills by the hour from now.
az search service create \
--name "<your-search-name>" \
--resource-group rg-ai103 \
--sku basic \
--auth-options aadOrApiKey \
--aad-auth-failure-mode http401WithBearerChallenge
# Then, as in Session 7: re-add your two search roles in Access control (IAM),
# redeploy the chat and embedding models, and rebuild the index.
python build_index.py
# After you add the connection in the portal (next step), note its name.
# SEARCH_CONNECTION_NAME is the name Learn's Azure AI Search tool sample uses.
export SEARCH_CONNECTION_NAME="<your-connection-name>"import os
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
project = AIProjectClient(
endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"],
credential=DefaultAzureCredential(),
)
# What is this project wired to? Credentials are NOT returned unless you ask for them.
for connection in project.connections.list():
print(f"name={connection.name}")
print(f" type={connection.type} default={connection.is_default}")
print(f" target={connection.target}")
print(f" auth={connection.credentials.type}")name=<your-connection-name>
type=CognitiveSearch default=<True or False>
target=https://<your-search-name>.search.windows.net
auth=AAD
... (other connections your project already has)ApiKey. The SDK may print these values as enum names, such as ConnectionType.AZURE_AI_SEARCH. If the auth type is ApiKey, add a new connection with Entra ID and use that one.SEARCH_ENDPOINT from an environment variable you typed by hand. The refactor asks the project instead, using the call shape from Learn’s Azure AI Search tool page, project.connections.get(...), and passes the connection’s target to SearchClient with the same credential as everything else.import os
from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import CredentialType
from azure.search.documents import SearchClient
credential = DefaultAzureCredential() # one identity for everything
project = AIProjectClient(endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], credential=credential)
# The connection says WHERE the search service is. Your identity decides WHETHER you may use it.
search_conn = project.connections.get(os.environ["SEARCH_CONNECTION_NAME"])
if search_conn.credentials.type == CredentialType.API_KEY:
raise RuntimeError(f"{search_conn.name} uses a key; recreate it with Microsoft Entra ID")
search = SearchClient(endpoint=search_conn.target, # no SEARCH_ENDPOINT variable any more
index_name=os.environ["SEARCH_INDEX"], credential=credential)
openai = project.get_openai_client()
query = "How many days of compassionate leave can I take?"
# Keyword query to keep this block short; keep your Session 7 hybrid query in your own refactor
results = search.search(search_text=query, select=["id", "title", "content"], top=3)
sources = "\n".join(f"[{d['id']}] {d['title']}: {d['content']}" for d in results)
response = openai.responses.create(
model=os.environ["FOUNDRY_MODEL_NAME"],
instructions=("Answer ONLY with facts from the sources. Cite each fact with its [id]. "
"If the sources don't contain the answer, say you don't know."),
input=f"Question: {query}\n\nSources:\n{sources}",
)
print(f"Search via connection '{search_conn.name}' ({search_conn.credentials.type})")
print(response.output_text)Search via connection '<your-connection-name>' (AAD)
<a grounded answer with [id] citations, as in Session 7>get_default instead? When the project has one connection of that type marked default and your code should follow whatever the project owner points it at. A named connection is more explicit; a default is easier to swap. Both are keyless when the connection uses Entra ID.FOUNDRY_PROJECT_ENDPOINT does.MCPTool(..., project_connection_id="...") or OpenApiKeyAuthDetails(project_connection_id="..."). The secret never appears in the agent definition.Connectors from the Foundry Tools Catalog run as managed MCP servers in the account’s Connector Namespace. Agent tools such as MCPTool reference credentials through project_connection_id rather than embedding them.
connections.get, get_default, ConnectionType, target and include_credentials; the Connected resources tab and Add connection; the Connector Namespace and project_connection_id.az search service delete and delete both deployments. Keep rg-ai103 and the project; Week 3 builds agents on them.connections.get returns an error, send me the exact message and your role on the project through “Ask R. Wingo” today.Connected resources | portal | The Project details tab where you view and add a project’s connections. | Learn → |
Add connection | portal | The button on the Connected resources tab that starts a new project connection. | Learn → |
Project client | concept | The Foundry SDK client for Foundry-native operations such as listing connections and enabling tracing. | Learn → |
OpenAI-compatible client | concept | The client from get_openai_client() for Responses, agents, evaluations and fine-tuning. | Learn → |
connections.get | method | Returns one project connection by name, including its id and target. | Learn → |
SEARCH_CONNECTION_NAME | setting | The name Learn’s Azure AI Search tool sample uses for the search connection’s name. | Learn → |
Custom key | concept | Connection type that stores keys and related properties securely for a custom target. | Learn → |
Foundry User | role | One of the roles Learn lists as able to add project connections. | Learn → |
Foundry Tools Catalog | portal | Where you browse over 1,000 pre-built connectors to SaaS, data and business systems. | Learn → |
Connector Namespace | service | A managed service per Foundry account that hosts connector runtimes and managed MCP servers. | Learn → |
MCPTool | class | Python class for an MCP server tool on an agent; takes project_connection_id for credentials. | Learn → |
project_connection_id | parameter | Tool setting that references credentials stored in a project connection instead of embedding them. | Learn → |
A named record in a Foundry project holding a resource’s target and authentication type, so code and tools can find and use it.
The URL a connection tells your code to use for the connected service.
A pre-built integration to a SaaS, data or line-of-business system, published in Foundry as a managed MCP server.
An integration where every call is authorised by a Microsoft Entra ID identity and role, with no key stored anywhere.
list_connections.py and keep the output.project.connections.get (or get_default, and say why), uses DefaultAzureCredential for every client, and refuses a key-based connection. Keep your hybrid query if you refactor Session 7.list_connections.py showing the connection type and an auth type that is not ApiKey (targets may be masked).az search service delete or a screenshot of the resource group afterwards.Connected resources Project client OpenAI-compatible client connections.get Foundry Tools Catalog project_connection_id| The refactored script runs, reads its resource target from a project connection, uses DefaultAzureCredential throughout, and refuses a key-based connection | 35 |
| The before-and-after table and explanation correctly assign each call to the right client and explain the keyless design and the connector choice | 20 |
| Uses the exact portal, client, method and parameter names from this session’s names box | 20 |
| Evidence: the connection screenshot, connection listing and script output are included, with no keys, endpoints or targets visible, and clean-up is shown | 25 |
| Total · pass mark 70 | 100 |
Session: 10 (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.Pop Quiz 2 — Generative Applications 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 Pop Quiz 2 →Closed book, no notes, and Coach Ritchie switches himself off while you sit it.