perf(exercises): trim search results to what picks an exercise - #5
Open
wromansky wants to merge 4 commits into
Open
perf(exercises): trim search results to what picks an exercise#5wromansky wants to merge 4 commits into
wromansky wants to merge 4 commits into
Conversation
A ten-result search returns 3,913 characters, of which 2,286 (58%) is a translation table — every language's name for every hit — and a further 480 is uuids. Measured against a live wger instance. That lands in an assistant's context on every lookup. Building a six-day routine means a search per exercise, and the accumulated payload is enough to matter on a self-hosted model with a 32k window: the agent that prompted this change exhausted its context mid-build and every subsequent inference failed with exceed_context_size_error until it burned its iteration cap. search_exercises now returns id, name, category and equipment; search_exercises_by_filter keeps muscles as well. Both drop uuid, images and the translation list. The matched name is still resolved exactly as before, using the caller's language. Same shape, 75% smaller. Nothing is lost — get_exercise still returns the complete record, which is the natural place for detail: search picks an id, the detail call expands it. _shape_images had no remaining callers and is removed with it. Adds 4 tests: the lean key set, that no translation text or uuid survives into the payload, that filter search keeps muscles, and that get_exercise still returns translations, images and uuid in full. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 13, 2026
Resolving exercise names one call at a time costs an inference round trip per exercise. Filling a six-exercise training day is therefore six turns of the model's tool budget spent before a single write happens. Observed twice on a self-hosted 27B with a 32k window: an assistant asked to fill a day issued 24 consecutive searches, exhausted its tool-iteration limit without writing anything, and — because each result also lands in context — exhausted the context window on the way. Nothing was created either time. search_exercises_batch takes a list of names and returns a map keyed by query, each holding the top matches in the same shape as search_exercises. Fetches run concurrently with the same cap as lookup_foods_by_barcodes, whose batch/single pairing this mirrors. Duplicate queries collapse; an empty list is not an error. limit_per_query defaults to 3 rather than 10: a caller picking one exercise per name does not need ten candidates each, and the difference is what the model must read. The shaping shared by both tools moves into one helper rather than being duplicated. Adds 3 tests: many names resolve in one call, duplicates collapse to a single request, and an empty list returns an empty map. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wger's exerciseinfo filters WHICH exercises match a language, but every result still carries all of its translations. The shaping picked the first translation whose name contained the query text, falling back to translations[0] — so an English search that matched a German or Italian name returned it. In practice an English search for "incline barbell bench press" came back as "Bankdrucken LH", and a coaching assistant then wrote that name into a training plan and read it back to its user. Wrong-language names also make the assistant distrust its own earlier lookups and search again, which costs a round trip each time. The language code is now resolved to wger's numeric language id (cached; the language table is static) and translations in that language are preferred, with the previous behaviour as fallback when none exists. search_exercises_by_filter had the same defect and gets the same treatment. Also drops search_exercises_batch's limit_per_query default from 3 to 2. The caller is picking one exercise per name, and every extra candidate is context it must read: a 7-name batch at 3 each was 9.4 kB in one tool result. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wger's name__search matches any single word of the query, and returns results in its own order. A search for "incline barbell bench press" therefore came back as ["Bench Press", "Bench Press Narrow Grip"] — the incline variant exists and simply ranked lower than the cut-off. An assistant picking from that list writes the wrong exercise into a training plan, which is exactly what happened: a plan whose first movement was meant to be incline barbell press was built around flat bench, and nothing about the tool's output suggested anything was wrong. Results are now fetched wider than requested (20), scored by how many of the query's words the name actually contains, tie-broken by name length, then trimmed to the caller's limit. The extra rows cost bandwidth, not caller context. Shorter names winning ties keeps "Bench Press" ahead of "Bench Press Narrow Grip" for the query "bench press". Adds a test that the specific variant outranks the generic one it would otherwise lose to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.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.
Measurement
A ten-result
search_exercisescall returns 3,913 characters, measured against a live wger instance:translationsuuidnameequipmentcategoryimagesidThe translation table is every language's name for every hit — sixteen entries for a common lift — and the caller already filtered by language when searching.
Why it matters
That lands in the model's context on every lookup, and building a routine means one search per exercise. On a self-hosted model with a 32k window it accumulates into a hard failure: the agent that prompted this change exhausted its context part-way through a six-day build, after which every inference returned
until it burned its iteration cap and gave up with a half-built routine. Nothing in that failure names the real cause — it surfaces only as "reached maximum tool iterations".
Change
search_exercisesreturnsid,name,category,equipment.search_exercises_by_filterkeepsmusclestoo. Both dropuuid,imagesand the translation list. The matched name is still resolved exactly as before, honouring the caller's language.Nothing is lost —
get_exercisestill returns the complete record. Search picks an id; the detail call expands it._shape_imageshad no remaining callers and is removed with it.Measured after the change, same queries against the same instance:
Tests
4 new tests in
tests/test_exercise_search_shape.py: the lean key set, that no translation text or uuid survives into the payload, that filter search keeps muscles, and thatget_exercisestill returns translations, images and uuid in full.Full suite passes (75) and
ruff checkis clean.Note
If you would rather keep the fields and make them opt-in, a
verbose: bool = Falseargument would work equally well — happy to switch. I went with the smaller default because the common caller is an LLM picking an id, and the detail tool already exists for everything else.Independent of #2, #3 and #4 — off
master, different file.