Lesson 25: What reaches the transpose skill
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Lesson 10 called the closure that TransposeSkill tells a model to use, with the arguments its SKILL.md prescribes. This lesson asks about the step before: which questions reach the skill. The chatbot’s intent router scores each skill by its best match among its description and example prompts, adds +0.06 for each routing hint whose pattern matches, and routes only when the best score reaches MinConfidence (SemanticIntentRouter.cs lines 129-257). The course has no model to compute the embeddings, so it runs the two parts that need none: the hints, and what the chatbot does with a transposition when nothing can embed it.
All GA links point to commit a826864, the course’s pin, unless they name another. On GA’s main at f4f5b4a (2026-10-01), TransposeSkill, its SKILL.md, the hint rules, SkillMdPlugin and the skill registrations are unchanged, but both routers changed for a question they can’t embed: the intent router asks each intent whether it matches without embeddings, and the agent router falls back on its keywords instead of throwing. Both came from GA pull requests #686 and #688, which fixed findings of this course and of music-theory-ga. GaMain now starts main’s chatbot host the way GaAi starts the pinned one, with the same closed Ollama port, no API key, and lesson 3’s index built with main’s code. The output comes from:
dotnet run --project code/ga-ai/GaAi -c Release -- l25dotnet run --project code/ga-ai/GaMain -c Release -- l25How a question reaches the skill
Section titled “How a question reaches the skill”DefaultRoutingHintProvider has one rule for transposition: a word that starts with “transpos”, or “shift”, “bring” or “move” followed, one to six words later, by “up”, “down”, or “to” and a capital letter:
(new Regex(@"\btranspos\w+\b|\b(shift|bring|move)\b\s+\S+(?:\s+\S+){0,5}?\s+(?:up|down|to\s+(?-i:[A-G]))\b", RegexOptions.IgnoreCase | RegexOptions.Compiled), "skill.transpose"),TransposeSkill hands the question to a model, and the class it derives from answers no to every CanHandle:
/// <inheritdoc /> /// <remarks> /// Tool-driven skills only route via the <see cref="Intents.SemanticIntentRouter"/>; /// the legacy <c>CanHandle</c> regex shadow is intentionally disabled. /// </remarks> public bool CanHandle(string message) => false;On main, when the question can’t be embedded, the intent router gives it to the first intent, in registration order, that matches it without embeddings. For a skill’s intent, that is the skill’s CanHandle (OrchestratorSkillIntent.cs line 29):
// Offline degradation: when the embedding backend cannot score the query, the // first intent (registration order) whose high-precision keyword predicate // matches claims it at the threshold confidence, so deterministic skills still // answer without Ollama. Returns null when none matches (LLM path as before). private IntentMatch? KeywordFallback(string query, IReadOnlyList<IIntent> intents) { foreach (var intent in intents) { if (!intent.MatchesWithoutEmbeddings(query)) continue;
logger.LogInformation( "SemanticIntentRouter: embeddings unavailable; keyword fallback picked {IntentId} for query={Query}", intent.Id, SanitizeForLog(query));
return new IntentMatch(intent, MinConfidence, KeywordFallbackSource) { Ranking = [new RoutingCandidate(intent.Id, MinConfidence, 0f, MinConfidence, KeywordFallbackSource)], }; }
return null; }The hints a transposition gets
Section titled “The hints a transposition gets”The program asks DefaultRoutingHintProvider about 26 phrasings: the skill’s 13 example prompts, the one phrasing of its SKILL.md that isn’t among them (line 80), the four transposition prompts of GA’s corpus, and eight of the course’s. It also looks for the SKILL.md’s triggers in each one, as lowercase substrings, the way SkillMdDrivenSkill.CanHandle does (SkillMdDrivenSkill.cs lines 54-65):
== Transpose phrasings: the intents whose routing hint fires, and the triggers of transpose's SKILL.md they contain (at the pin)source phrasing hints, +0.06 each SKILL.md triggersexample transpose this progression down a half step skill.transpose transpose, down aexample transpose this progression up a whole step skill.transpose transpose, up aexample transpose C-Am-F-G to G major skill.transpose transposeexample shift this progression up a half step skill.transpose up aexample bring D minor down to A minor skill.transpose noneexample Transpose Cmaj7 up a perfect fourth skill.interval, skill.transpose transpose, up aexample Move this F chord down a minor third skill.interval, skill.transpose down aexample What's Dm7 up a whole step? none up aexample Transpose G7 to Eb skill.transpose transposeexample Shift Am7 up a fifth skill.transpose up aexample raise the key by two semitones none noneexample lower the key by a half step none noneexample transposing the chorus down a tone skill.transpose down aSKILL.md Cmaj7 in the key of G none in the key ofcorpus Transpose C E G to D skill.transpose transposecorpus Transpose C major to E skill.transpose transposecorpus Transpose this progression to capo 3 skill.capo, skill.transpose transposecorpus Transpose A minor to C minor skill.transpose transposecourse Transpose Dm7 down a half step skill.transpose transpose, down acourse Move Am up two frets skill.transpose nonecourse Shift the song to the key of E none nonecourse Put Cmaj7 in the key of A none in the key ofcourse Raise Bb7 by a minor third skill.interval nonecourse Lower G by a tritone skill.interval nonecourse Play this song a whole step down skill.alternatetunings nonecourse Take F#m up a fourth none up a26 phrasings: the transpose hint fires on 16; another intent's hint on 6, 3 of them without the transpose hint; no hint on 7; a SKILL.md trigger in 18- The transpose hint misses 3 of the skill’s 13 example prompts. “What’s Dm7 up a whole step?”, “raise the key by two semitones” and “lower the key by a half step” have none of its verbs, and neither has the SKILL.md’s “Cmaj7 in the key of G”. The course’s “Shift the song to the key of E” has the verb, but ends on “of E”, not “to E”.
- Two example prompts boost
skill.intervalas much asskill.transpose. “Transpose Cmaj7 up a perfect fourth” and “Move this F chord down a minor third” name an interval, which the interval rule reads (lines 74-76); with +0.06 on both intents, the hints leave the gap between them where the embeddings put it. “Raise Bb7 by a minor third” and “Lower G by a tritone” boostskill.intervalalone, and “Play this song a whole step down” boostsskill.alternatetuningsalone, whose rule takes “whole step down” for a tuning (lines 159-161).
On main, the table is the same.
What the hint and the triggers also claim
Section titled “What the hint and the triggers also claim”The program asks the same of every other intent’s example prompts:
== The other intents' example prompts that the transpose hint or transpose's SKILL.md triggers claim (at the pin)intent example prompt hint SKILL.md triggersskill.chordinfo what notes make up a G7 chord no up askill.modes What are modes of limited transposition yes noneskill.progressionmood Brighten up a minor key tune no up askill.diatonicchords All chords in the key of D no in the key ofskill.settheoryequivalence Are pitch class sets 0,1,3 and 0,2,3 equivalent under tr… yes noneskill.settheoryequivalence Are pitch classes 0,2,4 and 1,3,5 transpositionally equi… yes noneskill.voiceleading best way to move from G7 to C yes noneskill.grothendieckdelta harmonic cost to move from C to G yes noneskill.grothendieckparse what does Transpose(C ⊗ G) mean yes transposeskill.grothendieckparse parse Transpose ∘ Invert yes transposeskill.grothendieckparse parse pullback(Cmaj7, Transpose, Gmaj7) yes transposeskill.grothendieckparse parse functor Transpose: Chords -> Chords yes transposeskill.grothendieckparse parse equalizer Transpose Invert yes transpose407 example prompts of 34 other intents: the transpose hint fires on 10, a SKILL.md trigger is in 8- The transpose hint fires on 10 example prompts of other intents. “transposition” and “transpositionally” in two set-theory prompts and in “What are modes of limited transposition”; “Transpose”, a functor’s name, in five of the parse skill’s; and “move from G7 to C” and “move from C to G”, where “to” and a capital letter end the phrase. Lessons 23 and 24 printed some of them in their hint columns.
- The triggers are substrings. “up a” is in “what notes make up a G7 chord” and in “Brighten up a minor key tune”, and “in the key of” in “All chords in the key of D”.
On main, the same 13 rows come out of 412 example prompts of 35 intents.
The skills behind the name
Section titled “The skills behind the name”SkillMdPlugin registers every SKILL.md that has triggers as a SkillMdDrivenSkill (SkillMdPlugin.cs lines 83-92), so two skills carry the name:
== The skills registered under the name Transpose, and the intent the router can pick for each (at the pin)skill name intent CanHandle acceptsSkillMdDrivenSkill transpose none 18 of the 26 phrasingsTransposeSkill Transpose skill.transpose 0 of the 26 phrasingsIOrchestratorSkill registrations 50, of them built from a SKILL.md 18, behind an intent 0- Only
TransposeSkillhas an intent, and itsCanHandleaccepts none of the 26 phrasings. The SKILL.md skill accepts 18, but none of the 18 skills built from a SKILL.md is behind an intent: the router reads only intents, and the orchestrator no longer callsCanHandle, as lesson 4 showed.
On main, the table is the same.
Without embeddings
Section titled “Without embeddings”With Ollama’s port closed, the program asks the host’s SemanticIntentRouter to route each phrasing, then posts it to /api/chatbot/chat. At the pin:
== Without embeddings: the intent SemanticIntentRouter picks for each phrasing, and what POST /api/chatbot/chat answers (at the pin)phrasing router picks chat: agent (routing method) first line of the answertranspose this progression down a half step none HTTP 500transpose this progression up a whole step none HTTP 500transpose C-Am-F-G to G major none HTTP 500shift this progression up a half step none HTTP 500bring D minor down to A minor none HTTP 500Transpose Cmaj7 up a perfect fourth none HTTP 500Move this F chord down a minor third none HTTP 500What's Dm7 up a whole step? none HTTP 500Transpose G7 to Eb none HTTP 500Shift Am7 up a fifth none HTTP 500raise the key by two semitones none HTTP 500lower the key by a half step none HTTP 500transposing the chorus down a tone none HTTP 500Cmaj7 in the key of G none HTTP 500Transpose C E G to D none HTTP 500Transpose C major to E none HTTP 500Transpose this progression to capo 3 none HTTP 500Transpose A minor to C minor none HTTP 500Transpose Dm7 down a half step none HTTP 500Move Am up two frets none HTTP 500Shift the song to the key of E none HTTP 500Put Cmaj7 in the key of A none HTTP 500Raise Bb7 by a minor third none HTTP 500Lower G by a tritone none HTTP 500Play this song a whole step down none HTTP 500Take F#m up a fourth none HTTP 50026 phrasings; the router picks none 26; the chat endpoint answers with HTTP 500 26- Every phrasing ends in HTTP 500. The intent router returns nothing when the question’s embedding fails (
SemanticIntentRouter.cslines 115-127), and the agent router behind it doesn’t catch its own failed embedding (SemanticRouter.cslines 70-73), as lesson 4 traced; three of the corpus prompts ended the same way in lesson 8.
On main:
== Without embeddings: the intent SemanticIntentRouter picks for each phrasing, and what POST /api/chatbot/chat answers (on main)phrasing router picks chat: agent (routing method) first line of the answertranspose this progression down a half step none voicing (keyword) Found 10 voicings matching tags [half]:transpose this progression up a whole step none voicing (keyword) Found 10 voicings matching tags [whole]:transpose C-Am-F-G to G major none voicing (keyword) Found 10 voicings matching chord G + mode major +…shift this progression up a half step none voicing (keyword) Found 10 voicings matching tags [half]:bring D minor down to A minor none voicing (keyword) Found 10 voicings matching chord D + mode minor +…Transpose Cmaj7 up a perfect fourth none voicing (keyword) Found 10 voicings matching chord Cmaj7 + tags [fo…Move this F chord down a minor third skill.chordinfo skill.chordinfo (orchestrator-skill-semantic) F major chord contains F, A, and C.What's Dm7 up a whole step? skill.chordinfo skill.chordinfo (orchestrator-skill-semantic) D minor 7 chord contains D, F, A, and C.Transpose G7 to Eb none voicing (keyword) Found 10 voicings matching chord G7:Shift Am7 up a fifth none voicing (keyword) Found 10 voicings matching chord Am7 + tags [fift…raise the key by two semitones none fallback-direct (error-fallback-unavailable) Our reasoning service is currently unavailable, s…lower the key by a half step none fallback-direct (error-fallback-unavailable) Our reasoning service is currently unavailable, s…transposing the chorus down a tone none voicing (keyword) Found 10 voicings matching tags [the, tone]:Cmaj7 in the key of G none fallback-direct (error-fallback-unavailable) Our reasoning service is currently unavailable, s…Transpose C E G to D none voicing (keyword) Found 10 voicings matching chord C:Transpose C major to E none voicing (keyword) Found 10 voicings matching chord C + mode major:Transpose this progression to capo 3 none voicing (keyword) I couldn't find a chord name, mode, or style tag …Transpose A minor to C minor none voicing (keyword) Found 10 voicings matching chord A + mode minor +…Transpose Dm7 down a half step none voicing (keyword) Found 10 voicings matching chord Dm7 + tags [half…Move Am up two frets none fallback-direct (error-fallback-unavailable) Our reasoning service is currently unavailable, s…Shift the song to the key of E none fallback-direct (error-fallback-unavailable) Our reasoning service is currently unavailable, s…Put Cmaj7 in the key of A none fallback-direct (error-fallback-unavailable) Our reasoning service is currently unavailable, s…Raise Bb7 by a minor third none voicing (keyword) Found 10 voicings matching chord Bb7 + mode minor…Lower G by a tritone skill.chordsubstitution skill.chordsubstitution (orchestrator-skill-semantic) Harmonic substitutions for **G** (ranked by ICV d…Play this song a whole step down none fallback-direct (error-fallback-unavailable) Our reasoning service is currently unavailable, s…Take F#m up a fourth none voicing (keyword) Found 10 voicings matching chord F#m + tags [four…26 phrasings; the router picks none 23, skill.chordinfo 2, skill.chordsubstitution 1; the chat endpoint answers with voicing (keyword) 16, fallback-direct (error-fallback-unavailable) 7, skill.chordinfo (orchestrator-skill-semantic) 2, skill.chordsubstitution (orchestrator-skill-semantic) 1- The intent router routes 3 of the 26, never to
skill.transpose. “Move this F chord down a minor third” and “What’s Dm7 up a whole step?” go toChordInfoSkill, which gives the notes of the chord before the transposition; “Lower G by a tritone” goes toChordSubstitutionSkill, which lists substitutes for G. The routing method of the three saysorchestrator-skill-semantic, whichOrchestratorSkillIntentwrites on every answer (line 54), though no embedding chose them. - The other 23 go to the agent router, which now catches the failed embedding and falls back on keywords (
SemanticRouter.cslines 72-84). Each agent with a keyword list scores the share of its keywords the question contains; the voicing agent has no list and scores 0.1:
var keywords = new Dictionary<string, string[]> { [AgentIds.Tab] = ["tab", "tablature", "fret", "string", "ascii", "parse", "e|", "a|", "d|"], [AgentIds.Theory] = ["chord", "scale", "key", "mode", "interval", "pitch", "harmonic", "function", "cadence", "theory"], [AgentIds.Technique] = ["finger", "position", "play", "technique", "stretch", "barre", "slide", "bend"], [AgentIds.Composer] = ["compose", "create", "generate", "reharmonize", "variation", "arrangement"], [AgentIds.Critic] = ["evaluate", "critique", "review", "improve", "suggest", "better"] };
var lowerQuery = query.ToLowerInvariant(); var scores = new List<(GuitarAlchemistAgentBase Agent, float Score)>();
foreach (var agent in _agents) { if (keywords.TryGetValue(agent.AgentId, out var agentKeywords)) { var matchCount = agentKeywords.Count(k => lowerQuery.Contains(k)); var score = (float)matchCount / agentKeywords.Length; scores.Add((agent, score)); } else { scores.Add((agent, 0.1f)); // Default low score } }- 16 phrasings get a voicing search. A question with none of the keywords goes to the voicing agent, whose search reads “half”, “whole” or “fourth” as tags: “Shift Am7 up a fifth” gets Am7 voicings. The 7 with “key”, “fret” or “play” go to an agent that needs a model, and the answer is the fallback’s “Our reasoning service is currently unavailable”. “fret” and “play” lift the tab or technique agent above 0.1; “key” lifts the theory agent only to 0.1, the voicing agent’s score, and the theory agent keeps the tie because it is registered before the voicing agent (
ServiceCollectionExtensions.cslines 94-99) and the sort keeps equal scores in that order (SemanticRouter.csline 373). Which agent each one reached isn’t printed; this is read from the code.
The skill itself
Section titled “The skill itself”Called directly, without a model:
== TransposeSkill called directly with "Transpose Cmaj7 up a perfect fourth" (at the pin)confidence 0.00 | I encountered an error processing your request. Please try again. evidence: Source: skills/transpose/SKILL.md evidence: Closure: domain.transposeChord (via ga_dsl_eval) evidence: warning: ga_dsl_eval was NOT invoked — answer is LLM-only, not deterministic- The answer is an error, and the evidence calls it LLM-only.
SkillMdDrivenSkillcatches the failed model call and answers with its error message (SkillMdDrivenSkill.cslines 204-226); the wrapper finds noga_dsl_evalcall and adds “answer is LLM-only”, the misleading line lesson 8 found inDiatonicChordsSkill’s log, reported as GA issue #764. Even routed, the skill can’t transpose offline: the honest answer to a transposition without a model is that it can’t be answered now.
On main, the answer and the evidence are the same.
Where the course stops
Section titled “Where the course stops”- The embeddings aren’t computed. A hint is +0.06 on a cosine score; whether it changes the skill the router picks depends on scores the course can’t compute. The lesson measures the hints and the paths without embeddings, not the routing with them.
- The phrasings are a sample: the skill’s and its SKILL.md’s, GA’s four corpus prompts, and eight of the course’s own.
- The chat endpoint is asked through
WebApplicationFactory, with the Ollama URL on a closed local port and an empty API key, and the tables print the first line of each answer.
Reported upstream
Section titled “Reported upstream”- Reported after this lesson was written, in GA issue #809: the example prompts the transpose hint misses and the other intents’ prompts it fires on, the keyword fallback that can’t reach
skill.transposeand the voicing agent’s default win, and the SKILL.md triggers no intent routes to.
Exercises
Section titled “Exercises”- Why does “What’s Dm7 up a whole step?” get no transpose hint? Rewrite it so it gets one.
- “best way to move from G7 to C” is a voice-leading example prompt. Which part of the transpose rule matches it?
- On
main, without embeddings, why can the intent router never pickskill.transpose? - On
main, without embeddings, why does “Transpose C major to E” get a voicing search, while “Move Am up two frets” gets “Our reasoning service is currently unavailable”?
Solutions
- It has no word starting with “transpos” and none of “shift”, “bring” and “move” (line 257). “Shift Dm7 up a whole step” would get it, like “Shift Am7 up a fifth” in the table.
- “move”, then “from” and “G7”, then “to C”: the rule reads one to six words of any kind after the verb, then “to” and a capital letter.
- It asks each intent whether it matches without embeddings, which for a skill’s intent is the skill’s
CanHandle(OrchestratorSkillIntent.csline 29), andTransposeSkill’s returns false (SkillMdDrivenWrapperBase.csline 85). The SKILL.md skill, whose triggers accept 18 of the 26 phrasings, has no intent. - The intent router picks nothing for either, so both reach the agent router’s keywords. “Transpose C major to E” has none of the keywords of the five agents that have a list, so each scores 0 and the voicing agent keeps its 0.1. “Move Am up two frets” contains “fret”, one of the tab agent’s nine keywords, so the tab agent scores about 0.11; it needs a model, and the fallback that replaces its answer has none either. Worked from the keyword lists; the program prints only the answering agent.
Key takeaways
Section titled “Key takeaways”- Test a hint rule on the skill’s own example prompts first: here 3 of 13 get nothing.
- A boost that two intents both get breaks no tie between them.
- A substring trigger such as “up a” also matches “make up a G7 chord”.
- A keyword fallback reaches only the skills whose
CanHandlecan say yes: a skill that always says no can’t be reached without embeddings. - A default score for an agent with no keywords makes it the answer to every question without keywords: without a model, a clear “unavailable” beats a confident voicing search.
Sources
Section titled “Sources”- GuitarAlchemist/ga at
a826864:Common/GA.Business.ML/Agents/Intents/DefaultRoutingHintProvider.cs,Common/GA.Business.ML/Agents/Intents/SemanticIntentRouter.cs,Common/GA.Business.ML/Agents/SemanticRouter.cs,Common/GA.Business.ML/Agents/Skills/TransposeSkill.cs,Common/GA.Business.ML/Agents/Skills/SkillMdDrivenWrapperBase.cs,Common/GA.Business.ML/Agents/Skills/SkillMdDrivenSkill.cs,Common/GA.Business.ML/Agents/Plugins/SkillMdPlugin.cs,skills/transpose/SKILL.md,Tests/Apps/GaChatbot.Api.Tests/Corpus/prompts.yaml. - GuitarAlchemist/ga at
f4f5b4a:SemanticIntentRouter.cswith its keyword fallback,Common/GA.Business.Core.Orchestration/Intents/OrchestratorSkillIntent.cs,SemanticRouter.cswith its caught embedding failure, and the chatbot host. - The course’s programs:
code/ga-ai/GaAi/Lesson25.cs,code/ga-ai/GaMain/Program.cs,code/ga-ai/GaMain/MainChatHost.cs,code/ga-ai/Shared/TransposeRoutingProbe.cs.