Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Building a Simple Teaching Agent

Building a simple teaching agent

Open In Colab

The previous three sections described the ingredients: MIR tools, learner memory, and pedagogy as the policy. This notebook assembles a minimal teaching agent (TA) from those ingredients: a piano transcription model, a score alignment, three learners, and an agent loop in about fifty lines.

The setting is a small practice studio. You log in as one of three learners, hand the TA that learner’s newest recording, and watch it transcribe the audio, compare it with the score, give feedback at the learner’s level, and write back what it learned for next week.

The learners and their recordings come from the Expert-Novice dataset (Jiang, 2023): 83 recordings of seven simple piano arrangements by 21 amateur pianists. Four piano instructors rated each recording from 1 to 5 and wrote a short comment on it.

  • Learner profiles come from the dataset. A profile holds the player’s self-identified level and the instructor comments on their other recordings, which the TA reads as notes from earlier sessions.

  • This week’s comments are held back. The instructor comments on the recording the TA analyses stay out of its context, so you can compare them with what the TA says.

What a domain-specific agent is

Strip away framework vocabulary and a tool-using agent is four artefacts (Chu et al., 2025):

  1. A model, reached through an API.

  2. A system prompt that encodes the domain policy, here the pedagogy of Pedagogy as the agent’s policy, plus the context the model must always have.

  3. Tools: JSON schemas describing functions the model may request, plus your implementations of those functions.

  4. A loop that sends the conversation, executes whatever tools the model requested, appends the results, and repeats until the model stops requesting.

Everything a framework advertises on top (planning, memory, orchestration) is built from these four.

We use the Gemini models that Colab offers free through google.colab.ai, so the notebook needs no API key. That library takes one text prompt and returns text, with no tool parameter, so the loop below does the tool calling itself: the prompt lists the tools, the model replies with a JSON object, and our code runs the tool it names. Provider APIs do the same parsing on their side under the name function calling.

transkun runs on the GPU when one is available. On Colab, choose a GPU under Runtime, Change runtime type. The agent uses Colab’s built-in Gemini models, which are free within monthly limits and need no API key, so the agent cells run only in Colab.

The data

The Expert-Novice dataset is on Zenodo under CC BY-NC-SA 4.0: the recordings, the seven scores in MusicXML, each player’s self-identified level, and the ratings and comments. Its augmented version on GitHub adds, for 34 recordings, a MIDI track that marks the regions where the playing goes wrong, annotated from the raters’ comments (Morsi et al., 2024). The cell below clones the GitHub repository and fetches single recordings from the Zenodo archive without downloading all 590 MB of it.

The studio database

A first instinct for “give the model context” is retrieval-augmented generation: embed everything, retrieve by similarity. For learner state, that is not the best approach. A TA’s context is small, structured, and keyed by identity: one profile, one memory list, a handful of recordings, all belonging to the logged-in user. The natural access pattern is a lookup keyed by the login, not a nearest-neighbour search over documents.

Each learner file holds two of the four kinds of state that Memory for longitudinal coaching argued a TA needs: identity with level, and notes from earlier sessions. The dataset records no style preferences and no constraints from a teacher, so these profiles have none. The notes are the four instructors’ ratings and comments on each of the player’s other recordings. The dataset does not record the order of a player’s recordings, so we treat the other pieces as earlier sessions.

We picked three players at different levels with different problems:

  • Player 13, intermediate. The instructors found wrong rhythms in all four earlier pieces and a heavy left hand in two of them. This week: The Entertainer.

  • Player 19, advanced. The instructors call the earlier pieces accurate and comment on pedalling, left-hand articulation, legato, and the final ritardando. This week: The Entertainer.

  • Player 3, beginner. Played the right hand an octave too high in The Blues. This week: Over the Waves.

The learner profile

A profile is one JSON file per learner, studio_db/learners/<learner>.json. login puts the whole file into the system prompt, and end_session appends to its memory list. The recordings and scores sit in the same database but outside the profile: the TA reaches them only through the tools.

FieldWhat it holdsSourcePlayer 13
namedisplay nameplayer number in the dataset"Player 13"
instrumentinstrumentfixed, the dataset is piano only"piano"
levelthe level the player reportedself-identified_skill_levels.txt"Intermediate (self-identified)"
memorynotes from earlier sessions, oldest firstsee below4 entries

A memory entry has one of two shapes, depending on who wrote it:

Written byFieldsExample
the dataset, before the first sessionpiece, from: "four piano instructors", ratings_1_to_5, commentsLavender’s Blue, ratings [3, 3, 1, 1], “LH too loud. Eighth note rhythms not correct. Pitches OK”
the TA, at end_sessiondate, from: "TA", noteone consolidated note per session

The instructor comments on this week’s take do not go into the profile. They stay in HELD_OUT for the comparison after each session.

{
 "name": "Player 13",
 "instrument": "piano",
 "level": "Intermediate (self-identified)"
}
memory: 4 entries, the second one:
{
 "piece": "Lavender's Blue",
 "from": "four piano instructors",
 "ratings_1_to_5": [
  3,
  3,
  1,
  1
 ],
 "comments": [
  "LH too loud. Eighth note rhythms not correct. Pitches OK",
  "Need to keep rhythm steady. Work on playing eighth notes evenly. Nice solid chords.",
  "wrong rhythms in right hand.  Left hand too loud and heavy.",
  "Wrong rhythms 8th note measures. LH too loud."
 ]
}

The recordings

Each learner has one recording on disk, this week’s take, named <learner>_<piece>. The first 20 seconds of Player 13’s take:

Loading...

Tools: transcription and score alignment

A tool has two halves. The schema is what the model sees: a name, a natural-language description, and typed parameters. The function is what runs when the model requests that name. Descriptions are prompt engineering: the model decides when to call each tool purely from that text.

Two of the three tools below do MIR work. transcribe_recording runs transkun (Yan and Duan, 2024), a piano transcription model that also estimates how hard each key was struck (MIDI velocity) and when the sustain pedal was down. The instructors comment on the balance between the hands and on pedalling, and a pitch-only transcription cannot see either. check_against_score aligns the transcribed notes to the score with the note matcher of parangonar (Peter et al., 2023) and reports per bar: wrong, missed and extra notes, how long the bar took compared with the median bar, the loudness of each hand, and the pedal.

make_tools(user) binds the three functions to the logged-in learner.

Before wiring these into an agent, look at what they actually return. This is the entirety of what the model will know about the playing:

{'piece': 'TheEntertainer', 'time_signature': '2/2', 'tempo_marking': 'moderato', 'played_beats_per_minute': 94, 'beat': 'half note', 'notes_correct': '162/169'}
Loading...

The augmented dataset marks where this take goes wrong. The annotation is a copy of the transcription MIDI with one extra track: each mistake region is a single note of pitch 0 that lasts as long as the region. The annotator’s notes (mistake_notes.pdf in the repository) list this take under two rhythm mistakes: drag, where a hesitation lengthens a note and shifts everything after it, and misread, where notes are played at twice or half their written length. The cell below draws the regions over the transcription and colours the bar numbers that the report flags:

<Figure size 1400x450 with 1 Axes>

Nine of the ten annotated regions contain a bar the report flags. In eight of them the flag is a bar that took 1.3 to 1.9 times as long as the median bar: the hesitations and added beats the instructors complain about. The report also flags bars 1 and 2, where Player 13 played the opening an octave lower than written. The instructors noticed this, but the annotation does not mark it. Bars 8, 12 and 31 run long without an annotation, and the region in bars 29 to 30 has no flag. Real MIR evidence arrives noisy, which is why the pedagogy below tells the model to weight consistent patterns over isolated anomalies, the evidence-with-uncertainty point of Tool calling for a practice agent.

Structure, not tools: profile and memory

The profile and memory notes are injected into the system prompt when the learner logs in. That is the context engineering: the model starts every turn already knowing who it teaches.

The harness owns the memory: end_session makes one extra model call asking for a consolidated note, then appends it to the learner file itself. This is the assess, update state, act loop of ScaffoldLM (Li et al., 2026) with the update step guaranteed by code rather than requested by prompt.

The loop and the session boundary

This is the entire mechanism. generate_text is stateless and takes one string, so call_model writes the system prompt, the whole conversation, and the tool protocol into that string on every call. When the reply names a tool, run_turn runs it, appends the result, and calls the model again. When the reply is an answer, the turn is over. A reply that is not valid JSON, or that names a tool that does not exist, goes back to the model as an error message. end_session closes the loop from the memory side.

Session one: Player 13

Player 13 rates their own level as intermediate, and the instructors found wrong rhythms in every one of their earlier pieces. Nothing in the message below tells the agent any of that; the profile arrived through login, the evidence arrives through the tools.

Watch the printed tool calls: the agent chose them itself, nothing in our loop scripts the order. A good reply leads with the hesitations, the long bars that recur through the piece, instead of reciting forty bars, and links them to the rhythm problems in the memory notes. Because the same problem appears in every earlier piece, it should change tactics instead of repeating “count carefully”. Model outputs vary run to run, so your transcript will read differently.

Compare the reply with what the four instructors wrote about this take, which the TA never saw:

The Entertainer, ratings [2, 2, 1, 1]
 - Octave error m. 1-2. Meter errors throughout in LH, adding extra beats in both RH and LH. Pitches are correct but almost always at the wrong time
 - Notes are accurate. But rhythm needs reworking. Counting is incorrect.
 - Didn't begin in the proper octave.  Too many rhythmic inaccuracies and hesitations.  Poor.  Wrong notes.
 - No 8va. Rhythm wrong end line 2. Then lots of hesitations and rhythmic inaccuracies throughout.

Follow-ups work without re-analysis, since the conversation is stateful within a session:

Closing the session writes the note the next session will read:

The last entry was written by the model, but its persistence was guaranteed by the harness. Next week’s login folds it into the system prompt, which is the whole difference between a reactive session bot and a longitudinal teaching agent in the sense of Memory for longitudinal coaching. Note the compression choice: one curated note per session rather than the raw transcript, the inspectable-memory pattern from DeepTutor (Zhao et al., 2026).

Same architecture, different learner

Player 19 rates their own level as advanced. The report for their take of the same piece looks different: bars of even length, and a handful of single missed or extra notes, mostly in the left hand. They play about 96 half notes per minute. The score marks only Moderato, so the report cannot say whether that is too slow. The instructors’ comments on their earlier pieces are about pedalling, articulation and the ending, not about notes. The code path is identical; only the injected profile and the audio changed.

The Entertainer, ratings [4, 4, 2, 2]
 - Very close, errors in m.21-22  and also the last line
 - Though there were missed notes, good job at keeping the tempo going. Need to fix a few wrong notes. Some notes were missing from left hand as well.
 - A bit lumpy and heavy in approach, not feel the arc of the phrasing.  Wrong notes here and there.
 - Too slow ( not cut time). Wrong note line 4. No P page 2. Wrong notes p. 2 line 2 and 3 and 4.

The register should shift: one rhythm problem and a concrete drill for Player 13, detail on tempo and the left-hand slips for Player 19, from the same system prompt scaffold and the same loop. Personalisation lives in the data, not in the code, which is why deployed practice products spend their engineering on the profile and memory schema rather than on prompt variants per user.

Exercises

  1. Player 3’s octave. Player 3 plays the right hand of Over the Waves an octave too high, the same mistake the instructors noted in The Blues. The report shows it as wrong notes exactly twelve semitones apart in two thirds of the bars. Log in as p03, ask for feedback, and check whether the TA names the octave error instead of listing thirty bars of wrong notes, and whether it connects it to the memory note. Then compare with show_instructors("p03").

  2. A real human. Record yourself playing one of the seven pieces (scores in EN-augmented-data/Scores), save the wav as studio_db/recordings/yourname_TheBlues.wav, copy the score into studio_db/scores/, add a profile, and see how the pipeline copes with your room, your piano and your tempo.

  3. Check the alignment. The dataset includes an audio-to-score alignment for every recording: one .txt file per recording with the audio time of each score position, in the Zenodo archive and in EN-augmented-data/Recordings_and_Alignments. Compare it with the bar start times in the report and find the bars where the automatic alignment goes wrong.

  4. Grade the TA. Build a profile for every player who recorded one piece, run the TA on each recording, and compare its main point with the held-out instructor comments. This is a small version of the evaluation harness discussed below.

  5. Forgetting. After five memory entries, have end_session consolidate them into one summary entry. Decide what survives, guided by Memory for longitudinal coaching.

What made it domain-specific

The agent code contains no music, but the agent is steeped in it: the model’s pretrained prior supplies the concepts and vocabulary, the system prompt injects the doctrine and the learner, and the tools return the evidence. Where the fit to the domain lives is more specific, and practitioner discussions of production agents converge on the same five places:

  • Data: the learner schema, the scores, and the recordings. The TA is exactly as good as the state you keep; most of the engineering in a real product goes here.

  • Tools: the three functions, which decide what the model can observe. The MIR stack of Tool calling for a practice agent enters the agent only through this surface.

  • Context engineering: what reaches the prompt each turn. The profile arrives via login, evidence arrives curated through tool returns rather than as raw audio frames, and end_session controls what gets written back.

  • Tone: the register shift between Player 13 and Player 19, driven by the level in the profile rather than separate prompts.

  • Guardrails: the login boundary enforced in code where the model cannot reach it, and abstention on unreliable evidence.

The same discussions rank a sixth element above fine-tuning: a verification loop against domain ground truth. The transcription and alignment stack plays that role here. The TA never trusts its own listening; it checks the take against note-level evidence, for the same reason a legal agent should verify a claim against the statute rather than recall it. Fine-tuning enters last, once knowledge access and verification are solved. The piece this notebook lacks is an evaluation harness that scores teaching quality across learners. The held-out instructor comments give such a harness a reference to score against; Pedagogy as the agent’s policy closed on the observation that longitudinal benchmarks for music practice barely exist.

How much can we trust the TA’s domain knowledge

Everything the TA said about rhythm, fingering, and practice strategy came from its pretrained prior, uncited and unverified. How far that prior extends is measurable. MusicTheoryBench, introduced with ChatMusician, tests college-level written theory in a knowledge split and a reasoning split (Yuan et al., 2024). GPT-4 answered a little under sixty percent of the knowledge split and stayed near chance on multi-step reasoning, far below trained musicians on both. And neither split measures pedagogical judgement, the capability this chapter actually needs: knowing the rules of counterpoint is not knowing how to fix Player 13’s hesitations in a ragtime.

The practical stance mirrors expert domains such as law and medicine: trust the prior for concepts and vocabulary, ground everything that could be looked up. In increasing order of cost:

  • Injected doctrine: rubrics, grade syllabi, and methodology playbooks placed in the system prompt or loaded on demand. The four rules above are this layer in miniature.

  • Retrieval over a pedagogy corpus: the canonical treatises are public domain and number in the dozens, so a TA can quote its sources with simple retrieval and no vector infrastructure. Clinical and legal agents live on this pattern at far larger scale, because a retrieved claim carries a citation the user can check and a recalled claim does not.

  • Fine-tuning and continued pretraining: reshaping the model itself on domain text, as ChatMusician does with theory texts and ABC notation (Yuan et al., 2024). It buys domain fluency and style more reliably than facts, and a tuned model still cannot cite its sources, so production agents reach for it only after the cheaper layers are exhausted.

Where a claim must be right, the TA should not be recalling it; it should be reading it.

Where frameworks fit

Nothing above needed a framework, and for understanding agents you should build the raw loop at least once: every trace you will ever debug is this message list. Provider APIs move part of run_turn to the server: you pass the tool schemas as a parameter and get structured tool calls back, so the JSON parsing and the error messages for broken replies disappear. Frameworks are the same loop plus the production concerns this notebook skipped: streaming, retries, rate-limit handling, tracing and evaluation, human approval gates, parallel tool execution, and prompt caching for the stable system prompt and schemas. Current options, roughly in order of distance from the raw loop: PydanticAI for typed tools, LangChain’s create_agent on the LangGraph runtime, and smolagents for agents that act by writing code. One tier further out sit visual orchestrators such as n8n and role-scripted multi-agent frameworks such as CrewAI: prototyping surfaces and product glue rather than a different mechanism, since everything they run is still this loop. The Model Context Protocol standardises the tool boundary itself, so a transcription tool exposed once as an MCP server becomes callable from any of these and from end-user products.

Further reading

References
  1. Jiang, Y. (2023). Expert and Novice Evaluations of Piano Performances: Criteria for Computer-Aided Feedback. Proceedings of the 24th International Society for Music Information Retrieval Conference (ISMIR), 367–374. 10.5281/zenodo.10265301
  2. Chu, Z., Wang, S., Xie, J., Zhu, T., Yan, Y., Ye, J., Zhong, A., Hu, X., Liang, J., Yu, P. S., & Wen, Q. (2025). LLM Agents for Education: Advances and Applications. Findings of the Association for Computational Linguistics: EMNLP 2025, 13782–13810. 10.18653/v1/2025.findings-emnlp.743
  3. Morsi, A., Zhang, H., Maezawa, A., Dixon, S., & Serra, X. (2024). Simulating Piano Performance Mistakes for Music Learning. 10.5281/ZENODO.14337238
  4. Peter, S. D., Cancino-Chacón, C. E., Foscarin, F., McLeod, A. P., Henkel, F., Karystinaios, E., & Widmer, G. (2023). Automatic Note-Level Score-to-Performance Alignments in the ASAP Dataset. Transactions of the International Society for Music Information Retrieval, 6(1), 27–42. 10.5334/tismir.149
  5. Li, Z., Zhu, Q., Wang, M., Li, J., & Huang, H. (2026). Planning-Guided Tutoring with Assessment-Driven Memory for Pedagogical LLM Tutors. Proceedings of the 64th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers), 7165–7188. 10.18653/v1/2026.acl-long.325