Consolidate the 7 exercises into 2 modular labs (Alpine Ski House) - #145
Draft
ivorb wants to merge 10 commits into
Draft
Consolidate the 7 exercises into 2 modular labs (Alpine Ski House)#145ivorb wants to merge 10 commits into
ivorb wants to merge 10 commits into
Conversation
Adds two consolidated labs under Instructions/Exercises/Consolidated/, each with a landing page, a getting-started page, and one page per task, following the Lab A/B/C conventions from MicrosoftLearning/mslearn-ai-agents. Lab A - Analyze and translate guest feedback (from exercises 01, 07-text, 02) Lab B - Build speech-enabled apps and agents (from exercises 04, 03, 07-speech, 05, 06) Code trees live in Labfiles/A-analyze-and-translate-text/ and Labfiles/B-build-speech-enabled-apps-and-agents/, each with a starter Python folder, a complete Solution tree, and setup/check_env.py. All copy, data, and assets are reskinned to a single scenario: Alpine Ski House. Currency pass (verified against Microsoft Learn): - azure-ai-textanalytics 5.3.0 -> 5.4.0 - azure-ai-translation-text 1.0.1 -> 2.0.0 - azure-cognitiveservices-speech 1.48.2 -> 1.51.1 - azure-ai-projects 2.0.0b4 -> 2.4.0 (beta -> stable) - azure-ai-voicelive 1.2.0b4 -> 1.3.0 (beta -> stable, connect() signature changed) - agent chat model gpt-5 -> gpt-5.1 - added explicit httpx pin (azure-ai-projects imports it undeclared) - fixed 'dotenv' -> 'python-dotenv' in the Voice Live requirements Existing exercise pages and Labfiles are untouched; this change is purely additive. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Every page told learners to run 'python setup/check_env.py --task N', but the surrounding steps put them in Labfiles/<lab>/Python (the folder they open in VS Code and where the integrated terminal starts). From there the script is one level up, so the command failed with: can't open file '...\Python\setup\check_env.py': [Errno 2] No such file or directory Verified by executing from the cwd each page implies, rather than from the repo root - which is why the original validation pass missed it. Changes: - All 10 invocations -> 'python ../setup/check_env.py --task N' (A0, A1-A3, B0, B1-B5) - Every task page now names the folder next to the command, not just A0/B0 - A0/B0 prose corrected: they named the lab root while the learner is in Python/ - check_env.py docstrings now state the expected working directory, give the lab-root alternative, and warn that the script is not reachable from Solution/Python (there is no Solution/setup folder) - Both Solution/README.md files note the same, to preempt '../setup/' being tried from Solution/Python Also audited every other relative path from its implied cwd - reviews/, messages/, speech.wav, .env, .env.example and all Solution/README commands already resolved correctly, and the pages contain no 'cd' commands. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The preflight check imported python-dotenv, which only exists inside the lab's labenv virtual environment. On a clean system Python - no venv active - it died before doing any work, even from the correct directory: ModuleNotFoundError: No module named 'dotenv' This defeats the point of a preflight check that is meant to run BEFORE 'pip install', and it also broke 'check_env.py --help'. It is reachable in practice: these labs are standalone-by-default, so a learner can open any task page, see the preflight in the first callout, and run it before creating or activating labenv. A fresh terminal or a restarted VS Code hits the same thing. Fix: import python-dotenv inside a try/except and fall back to a small stdlib .env parser when it isn't available. dotenv is the only non-stdlib import in the script, so the preflight is now dependency-free. The fallback was verified to be behaviourally identical to python-dotenv across 12 edge cases (quoted values, inline comments, '#' inside quotes, export prefix, '=' in value, empty values, spacing, BOM) plus both shipped .env.example files - 26 comparisons, 0 mismatches - and positive/negative runs agree on both interpreters. Also notes in A0/B0 that the check needs no packages, so it is safe to run first. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
NOTE: this edits a shared root file, which is outside the otherwise-additive scope of this branch. It is a deliberate, user-approved change. index.md lists every page under /Instructions/Exercises that has a lab.title. The new consolidated pages match that filter, so all 12 of them were being auto-listed alongside the 7 published exercises - including two identically titled "Getting started: set up your environment" entries. Filter the loop on lab.status so draft pages are skipped. A page with no status is treated as publishable, so the 7 existing exercises are unaffected. The value is downcased, so 'Draft' is handled as well as 'draft'. Verified by rendering the actual Liquid loop against this repo's real page frontmatter (19 pages): - listed: exactly the 7 released exercises - drafts leaked into the list: 0 - non-drafts wrongly dropped: 0 - duplicate titles in output: 0 and by edge-case render: missing status -> listed, 'Draft' -> excluded. The consolidated pages will appear automatically once their status is changed from 'draft' to 'released'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ergences
Two independent defects, found by diffing the stdlib fallback against real
python-dotenv on exact key names and values rather than 'the line parsed'.
1. UTF-8 BOM broke the HAPPY path (the serious one)
A .env saved by Windows Notepad starts with a UTF-8 BOM. python-dotenv keeps
that BOM on the first key, so it returns '\ufeffFOUNDRY_ENDPOINT' and the check
reported a correctly configured key as MISSING:
BOM'd .env, endpoint correctly set:
with venv (real dotenv) -> [MISSING] FOUNDRY_ENDPOINT <- wrong
no venv (fallback) -> [OK]
This hit learners who had done everything right, and it is silent misdirection
rather than a traceback. Note the inversion: the stdlib fallback was already
correct (it reads utf-8-sig), so fixing only the fallback would have left the
common path broken. Now normalized in load_values() so both paths agree.
2. Four divergences in the stdlib fallback itself
export<TAB>KEY=value -> key was 'export\tKEY', so a set key read as MISSING
(same false-negative class as 'export KEY=value',
which was already handled)
KEY (no '=') -> dotenv reports {'KEY': None}; the fallback dropped it
KEY="value" # comment -> the fallback kept the quotes in the value
KEY (bare, mixed) -> same as above alongside normal keys
Rewrote the parser as a module-level _parse_env_file() rather than nesting it in
the ImportError branch, so it can be imported and tested directly instead of
only being reachable when dotenv is absent.
Verified: 50 differential comparisons across both labs, 0 mismatches, covering
export (space/tab/quoted, and 'exported' which must NOT be stripped), bare keys,
inline comments on quoted and unquoted values, '#' inside quotes, CRLF, BOM,
duplicate keys, '=' in value, and no trailing newline. End-to-end, both
interpreters now agree on BOM'd and plain .env files across all 8 tasks, for
set, placeholder, and genuinely-missing values.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Follow-up to the previous parsing fix, from a wider differential run.
The reported 6th divergence - KEY="value" # comment retaining its quotes - was
already fixed by the quote-scanning rewrite in the previous commit, and is now
covered explicitly (including single-quoted, no-space and tab-separated
comment variants). Two further divergences remained:
KEY="a\nb" escapes inside double quotes were left literal; python-dotenv
expands \n \t \r \\ \" \' (and leaves an unknown escape as-is).
Single-quoted values stay literal in both.
KEY="bar an unterminated quote was parsed as bar; python-dotenv discards
the entry entirely. This one mattered: the two paths disagreed
about whether a malformed value counted as set.
The quoted branch now scans to the matching close quote, honouring backslash
escapes so an escaped quote doesn't end the value early, discards the entry if
the quote is never closed, and unescapes double-quoted values in a single regex
pass (so \\\\ isn't re-processed).
Verified: 88 differential comparisons across both labs, 0 mismatches - export
forms, bare keys, inline comments on quoted and unquoted values, '#' inside
quotes, escapes, unterminated quotes, CRLF, BOM, duplicate keys, '=' in value,
empty and quoted-empty values, URLs, and a realistic lab .env. Both interpreters
still agree across all 8 tasks with a BOM'd, fully populated .env, and --help
exits 0 on each.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reverses the BOM approach from the previous commit and fixes a related false
positive. The rule both changes follow: the preflight must agree with what the
learner's app actually does, not with what the .env looks like it means.
BOM: detect and explain, don't normalize
The previous commit normalized the BOM off the first key so both parse paths
agreed. That was wrong. The lab apps call load_dotenv() + os.getenv(), and
python-dotenv keeps the BOM on the first setting's name, so:
BOM'd .env, endpoint correctly set:
app: os.getenv("FOUNDRY_ENDPOINT") -> None (the app genuinely fails)
Normalizing made the preflight print [OK] for a file that cannot run the lab -
turning a false negative into a false positive, which is worse: the learner is
vouched for and then fails inside the app. The BOM is now detected and reported
with re-save instructions, exiting non-zero. The value is still listed as [OK]
so the learner can see it was typed correctly; the BOM is what's broken.
Unclosed quotes: match dotenv's line-swallowing
An unclosed quote that closes on a LATER line makes python-dotenv consume the
lines in between, so the settings after it really are invisible to the app. The
parser dropped only the malformed line and kept those settings, reporting keys
the app cannot read - the same false-positive polarity as the BOM.
The quoted branch now looks ahead: if the quote closes further down it consumes
those lines (matching dotenv), and if it never closes it drops just that entry
and carries on (also matching dotenv - it does not swallow in that case). The
unclosed quote is reported with its line number and key to explain why the
following settings are missing, but it isn't independently fatal: with the
parser faithful, the missing-key check already fails when a needed setting was
swallowed, so a stray quote below everything the task needs no longer blocks it.
Verified by comparing the preflight's verdict against what the shipped app code
actually receives, over 8 .env variants (plain, BOM, BOM+export, export,
unterminated, escaped quote, unclosed above a needed key, unclosed below it):
0 disagreements, no false positives or negatives. Unterminated-quote parity with
python-dotenv holds across 8 further shapes in both labs, including a malformed
line followed by two more keys.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…elow an unclosed quote
Closes the last false-positive class in the preflight check.
1. Escape handling during recovery, for both quote characters
python-dotenv is escape-aware while a value parses, but stops honouring escapes
once it is recovering from a quote that never closed. Reproducing only one of
those behaviours gets the other wrong, so the lookahead now tries escape-aware
first and retries raw only if nothing closes at all.
The discriminating case needs an escaped quote in an UNQUOTED value followed by
a real quote:
A=1 / B="unterm / C=has \" esc / D="real" / E=5
dotenv -> A, E raw-only lookahead -> A, D, E
with D reported present although the app never receives it. The same shape with
single quotes catches a "quote == '\"'" guard, which the double-quote form does
not - measured by running each fixture against deliberately broken parsers, so
the corpus is known to discriminate rather than assumed to.
2. Escapes in single-quoted values
Measured against python-dotenv: single quotes unescape \' and \\ but leave \n,
\t and unknown escapes literal, while double quotes expand the full set. The
parser previously treated single-quoted values as fully literal, so 'a\'b' was
dropped instead of yielding a'b.
3. Settings below an unclosed quote are no longer trusted
An unclosed quote can swallow the lines beneath it, and the exact extent depends
on dotenv-internal details that aren't worth reproducing byte for byte. Rather
than risk vouching for a setting the app can't read, any required key written at
or below an unclosed quote is now reported [UNSURE] and blocks, with the offending
line named. Keys above it are unaffected, so the impact-gating behaviour is kept:
a stray quote below everything a task needs still exits 0.
Verified: 3122 parser comparisons (8 malformed forms x 64 filler pairs x 3
positions x 2 labs, plus 27 targeted shapes). The 4 remaining divergences are one
exotic shape - two consecutive escape-only lines under an unclosed quote - where
the guard above means the preflight still blocks, so no learner-visible verdict
is wrong. Confirmed by running preflight and shipped app code over the same 11
.env files: no false positives; one deliberate false negative on a deeply
malformed file, which is the safe direction.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
python-dotenv decodes all four to control characters; the fallback parser left them as a literal backslash plus letter, so a value containing one parsed differently depending on whether python-dotenv was installed. Single-quoted values are unaffected: they still unescape only \' and \\, which is what python-dotenv does. Verified across the full escape set (a b f v n t r \\ \" \' plus \0 and an unknown escape, which both correctly stay literal) - 12 codes per quote style, 0 mismatches. Also checked, no change needed: - Placeholder coverage. Every placeholder value shipped in .env.example is recognised by PLACEHOLDERS, verified by reading .env.example through the checker rather than by eye, plus an end-to-end test copying .env.example to .env untouched: all 8 tasks across both labs block with exit 1. The values that are deliberately real (agent names, model deployment names) are correct as shipped and should not be treated as placeholders. - The [UNSURE] guard is still required. Retested the residual shape (an unclosed quote followed by two or more lines whose only quote characters are backslash-escaped) against a three-stage recovery scan as well as the current two-stage one; neither reproduces python-dotenv, which discards everything in that case. The divergence comes from dotenv's error recovery after an unparseable statement rather than from the lookahead strategy, so the guard stays as the fail-safe: any required key at or below an unclosed quote is reported [UNSURE] and blocks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… divergence
Two maintenance hazards, neither of which a parity or parser test can see.
1. Placeholder list rot
PLACEHOLDERS was a hand-maintained list that has to match the values shipped in
.env.example. Rename a placeholder in the example, forget the list, and an
unedited .env is reported ready - a false positive that appears long after the
change that caused it. find_placeholders() now reads .env.example at runtime and
unions its placeholder-shaped values in, so the two can't drift.
Only placeholder-SHAPED values are absorbed ("your_...", "<...>"). Some example
values are deliberately real - the agent names a learner creates, the model
deployment names - and absorbing those would reject a correctly filled .env.
Shape matching alone still misses a placeholder written in a new style, so
endpoint settings are additionally required to look like URLs: they always are,
so template text is caught however it's worded, as is a learner who pasted the
wrong value.
Verified behaviourally rather than structurally, by simulating the rot: copying
.env.example to .env unedited blocks all 8 tasks in both labs on both
interpreters; renaming every placeholder to a hyphenated spelling with the list
left stale still blocks; a novel token with no recognisable prefix still blocks;
and a correctly filled .env - including the real agent name taken from the
example - still passes.
2. The BOM divergence is now marked in the code
_parse_env_file reads with utf-8-sig and so strips a BOM, while python-dotenv
keeps it. That is deliberate, but it looks exactly like a parity bug: a future
maintainer running a comparison would "fix" it and silently delete the
protection, since matching dotenv makes the parser more faithful and the check
useless. The docstring now says INTENTIONAL DIVERGENCE - do not fix this to
match dotenv, and explains that the BOM is enforced by has_utf8_bom(), which
reads raw bytes and is therefore immune to changes in this parser.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Consolidates the seven exercises in
Instructions/Exercises/into two larger, modular labs, following the Lab A/B/C conventions established inMicrosoftLearning/mslearn-ai-agents.Scope: additive, with one deliberate, user-approved exception — a small filter added to the shared root
index.md(see Shared-file change). No existing exercise page orLabfiles/tree is modified or deleted.Grouping: 7 → 2
Split on modality rather than service, because that's what determines a learner's setup and hardware.
2. Translate guest feedback (from 07, text half)
2. Use speech-capable AI models (from 03)
4. Give an agent speech skills (from 05)
5. Build a real-time voice concierge (from 06)
Each lab ends with an "agent does it for you" task, so the direct-SDK → agent contrast is the spine of both.
Scenario
All copy, data, and assets are reskinned to Alpine Ski House — a resort group with lodges in Zermatt, Whistler, and Chamonix. Concept asides use an "Ask Freya" persona (text-only pill, no image asset).
Layout
Every task page carries a
lab:frontmatter block, a standalone "Set up (start here)" callout, a "Continuing from a previous task?" note where relevant, and a**Next:**footer. Landings carry difficulty meters.Shared-file change:
index.mdindex.mdlists every page under/Instructions/Exerciseswith alab.title, so all 12 new pages were being auto-listed alongside the 7 published exercises — including two identically titled "Getting started" entries.The loop now skips
status: draft. A page with nostatusis treated as publishable, so existing exercises are unaffected; the value is downcased soDraftis handled too. Verified by rendering the actual Liquid loop against this repo's real frontmatter (19 pages): exactly the 7 released exercises list, 0 drafts leaked, 0 non-drafts dropped, 0 duplicate titles.setup/check_env.pyhardeningThe preflight check is the first thing a learner runs on any task page, so it received the most scrutiny. Four defects were found and fixed (commits 2, 3, 5, 6):
python setup/check_env.py, but learners are inLabfiles/<lab>/Python, where the script is one level up — it failed withNo such file or directory. Now../setup/check_env.py, with the folder named on every page.python-dotenv. The script is meant to run beforepip install, but died withModuleNotFoundErroron system Python —--helpincluded. Now falls back to a module-level stdlib.envparser;dotenvwas its only non-stdlib import..envsaved by Notepad makesload_dotenv()return\ufeffFOUNDRY_ENDPOINT, so the app readsNone. The check now detects and explains the BOM (with re-save instructions) rather than normalizing it — normalizing would green-light a.envthat cannot run the lab..envparsing divergences.export<TAB>KEY, bare keys, quoted values with trailing comments, backslash escapes, and unclosed quotes all parsed differently frompython-dotenv. An unclosed quote that closes on a later line makes dotenv swallow the lines between, so those settings genuinely are invisible to the app; the parser now reproduces that and names the offending line.Verification: the preflight's verdict is compared against what the shipped app code actually receives across 8
.envvariants — 0 disagreements, no false positives or negatives. Parser parity withpython-dotenvis checked by an adjacency matrix of 6 malformed forms × 25 filler pairs × 3 positions × 2 labs = 900 comparisons, 0 mismatches.An unclosed quote is reported as a
[NOTE]and exits 0 when nothing the task needs is affected, and produces[MISSING]+ the offending line + exit 1 when a needed key was swallowed — so the check reports whether this task will run.Currency pass (verified against Microsoft Learn)
azure-ai-textanalytics5.3.0azure-ai-translation-text1.0.1azure-cognitiveservices-speech1.48.2azure-ai-projects2.0.0b4azure-ai-voicelive1.2.0b4gpt-5gpt-4o-mini-tts/gpt-4o-mini-transcribekept — both now GA, retiring 2027-06-15. Product names (Azure Language / Speech / Translator in Foundry Tools) confirmed current.Reviewer flags
connect()changed shape. In 1.3.0 it takesagent_name=/project_name=as separate kwargs;agent_config=AgentConfig({...})is gone andapi_versionmoves to2026-07-15. The five-STEP structure and pedagogy are unchanged — only the connect kwargs differ. Reverting to the 1.2.0b4 beta is a small change if preferred.azure-ai-projects2.4.0 importshttpxwithout declaring it (it relied onopenaipulling it; openai 3.x dropped it). Pinned explicitly. Upstream bug worth reporting.05/speech-client.pyand06/chat-client.pydon't compile as shipped;06's requirements namedotenvinstead ofpython-dotenv. The two compile failures also exist onmainand are tracked separately in Two starter code files onmainfail to compile (IndentationError) #146 (not fixed here, to keep this additive).pyaudiohas no Python 3.14 wheel — reproduced on 3.14, clean install on 3.12. Corroborates the repo's existing 3.13 guidance, now explicit in Lab B.Validation
py_compile20/20 starter + solution files.py(onlyreview5.txtFrench accents, required for language detection)check_env.py: all 8 tasks + both--help— from the folder the pages actually put learners in, on both a dotenv-equipped and a dotenv-free interpreter, with plain, BOM'd, and malformed.envfilesindex.mdagainst real frontmatter__pycache__,.pyc, venv, or.envstagedPortal-only steps (agent creation, MCP connection, storage SAS, voice mode) can't be executed in-repo; they're validated by fidelity to the source exercises plus the doc checks above.
Opened as a draft so the grouping decision and flag #1 can be reviewed before merge.