fix(gooddata-eval): persist skill_routing credit across conversation turns - #1762
fix(gooddata-eval): persist skill_routing credit across conversation turns#1762Tomkess wants to merge 3 commits into
Conversation
…turns _activated_skills() only looked at the current turn's tool calls, so skill_routing was recomputed from scratch each turn. The platform keeps a skill active once set_skills is called, so an agent correctly omits a redundant set_skills call on a later turn that reuses the same skill -- but the scorer forced skill_routing=False whenever no skill was activated *this* turn, regardless of whether it was already active. Found via scripts/authoring/debug_conversation.py replaying analyst-explores-dynamic-currency-conversion (gdc-mic-ai-evaluation repo): turns t4/t5 both ran create_adhoc_visualization/create_metric successfully against an already-active skill, yet scored FAIL solely on this. Track activated skills in a running set across the whole conversation instead of resetting it every turn.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe conversation now preserves activated skills across turns. Skill routing checks all skills activated so far. Tests cover both previously activated skills and skills that were never activated. ChangesPersistent skill routing
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The PR persists skill activation across conversation turns while retaining the false case for skills that were never activated. No actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1762 +/- ##
==========================================
+ Coverage 80.65% 81.58% +0.93%
==========================================
Files 272 275 +3
Lines 19369 19867 +498
==========================================
+ Hits 15622 16209 +587
+ Misses 3747 3658 -89 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
The bug is real and the debug provenance in the test docstring (t4/t5 of Is
activated_skills_so_far |= set(activated)
skill_routing = turn.expected_skill in activated_skills_so_farThat's correct if the platform treats
If replace semantics apply, the state to track is "last set wins, carried over when a turn issues no call of its own": if activated:
active_skills = set(activated)
skill_routing = turn.expected_skill in active_skillsThe two implementations disagree on real multi-skill fixtures, so it's worth confirming with the platform side rather than inferring. If additive is confirmed, the current code is right as-is and it'd be good to say so in the comment — that's the assumption the whole fix rests on. Reporting now self-contradicts.
{"turn_id": "t5", "skill_routing": true, "activated_skills": []}which reads as a bug to anyone triaging a report — the credited skill is nowhere in the list the same record shows. Either report the set the credit was drawn from, or add a field distinguishing "activated this turn" from "active during this turn". (This is also the payload #1757 serializes, so the two PRs meet here.)
|
|
@hkad98 good catch, and I don't want to guess on this one. The additive-vs-replacing question determines whether the current fix is correct or just moves the bug — I can't verify Once that's settled: if additive is confirmed, I'll add a comment here stating that assumption explicitly (agreed, the code should say why it's safe, not just that it works). If it's replacing, I'll switch to the "last set wins, carried over when a turn issues no call" version you sketched. The reporting self-contradiction ( |
|
Agreed — holding is the right call here, and I'd rather have this open pending an answer than merged on a guess. Nothing further from me on the accumulation logic until the semantics land. Two things to make the answer easier to get, since whoever knows 1. The actual question to ask. Concretely: after 2. A related inconsistency to confirm at the same time — the argument key. skills.extend(args.get("skill_names") or args.get("skills") or [])but the visualization evaluator only reads one: # evaluators/visualization.py:78
skill_names = args.get("skill_names", [])Our own tests use both — Also fine to fix the |
… differ in scope Addresses the reporting half of hkad98's review: with skill_routing now cumulative while TurnResult.activated_skills stays per-turn, a reused-skill turn reports skill_routing=True alongside activated_skills=[], which reads as a scoring bug to anyone triaging a report. The two fields measure different scopes on purpose -- activated_skills is "what THIS turn declared", skill_routing is "was expected_skill active by this point in the conversation". Documented on TurnResult and at the computation site, and pinned by an assertion on the existing persistence test so the combination is recorded as intended output. Deliberately not changing what activated_skills CONTAINS: whether the cumulative set is the right value to report depends on whether set_skills is additive or replacing, which is still open. This makes today's output legible under either answer without pre-committing to one.
|
@hkad98 addressed the reporting half in The two fields measure different scopes on purpose, and that wasn't written down anywhere — which is why the output reads as a bug:
So I deliberately did not change what Still need that answer before touching the accumulation logic or the field's contents — that's the one thing here I can't settle from the repo. 475 passed, lint clean. |
|
@hkad98 checked the gen-ai service implementation — it replaces, it does not accumulate. Your suspicion was right and the original fix here was wrong. So the running union produced exactly the false PASS you described. Concretely, with the old code: t1 Fixed in if activated:
active_skills = set(activated)
skill_routing = turn.expected_skill in active_skillsThe original false-FAIL fix still holds — a turn reusing an already-active skill without re-declaring keeps its credit; only turns whose skill was actively replaced lose it. Also took the second option you offered on reporting: added New test One thing I noticed while checking, not fixed: (Ping me directly if you want the specific implementation references I checked — keeping them out of this repo's comments.) |
set_skills REPLACES the active skill set rather than adding to it -- verified against the gen-ai service's skill registry, and stated in the tool's own description. The running union this PR originally used was therefore wrong in the mirror case hkad98 predicted: a skill dropped by a later set_skills call stayed credited, so a turn expecting it and declaring nothing scored PASS against a skill that was no longer active. That is strictly worse than the false FAIL this PR set out to fix -- it reports a broken conversation as working. Now tracks "last declared set wins, carried over on turns that declare nothing", which matches the platform. The original false-FAIL fix still holds: a turn reusing an already-active skill without re-declaring it keeps its credit. Also reports the set the credit was drawn from as TurnResult.active_skills (and in detail["turns"]), so skill_routing=True next to activated_skills=[] is self-explanatory instead of reading as a scoring bug. New test asserts the deactivation case and fails under the old union logic (verified by re-injecting it).
287e1de to
c5215eb
Compare
Summary
_activated_skills()only looked at the current turn's tool calls, soskill_routingwas recomputed from scratch every turn.set_skillskeeps a skill active until it's replaced, so an agent correctly omits a redundant call on a later turn that reuses the same skill — but the scorer forcedskill_routing=Falsewhenever no skill was activated this specific turn, regardless of whether it was already active.Found via
scripts/authoring/debug_conversation.py(gdc-mic-ai-evaluation repo) replayinganalyst-explores-dynamic-currency-conversion: turns t4/t5 both rancreate_adhoc_visualization/create_metricsuccessfully against an already-active skill, yet scored FAIL solely because of this.Fix: replace-with-carry-over, not accumulate
set_skillsREPLACES the active set rather than adding to it — verified against the gen-ai service's skill registry, and stated in the tool's own description. So:A turn that declares nothing inherits the previous turn's set; a turn that declares something drops whatever it left out.
Reporting
skill_routingis conversation-scoped whileactivated_skillsstays per-turn, so a reused-skill turn showsskill_routing=Truealongsideactivated_skills=[]. That reads as a scoring bug to anyone triaging a report, soTurnResult.active_skills(also indetail["turns"][]) now carries the set the credit was actually drawn from. Documented onTurnResult.Test plan
test_..._skill_routing_persists_across_turns— a skill activated in turn 1 and reused without re-declaring in turn 2 is credited on both.test_..._skill_routing_false_when_skill_never_activated— guards against being too lenient; a skill no turn ever activates still fails routing.test_..._skill_routing_false_after_a_later_call_deactivates_it— the case the union version got wrong: t1 activatesmetric, t2 replaces it withvisualization, t3 expectsmetricand declares nothing → correctly FAILs. Verified meaningful by re-injecting the union logic and watching this test fail, then restoring.ruff check/ruff format --checkclean.Known follow-up (not in this PR)
_activated_skills()reads the requested skill names from the tool call's arguments, but the service drops names it doesn't recognise and pulls in declared dependencies — and the tool's result echoes back the authoritative post-replacement set. Reading the result instead would be truer to what actually became active. Pre-existing and a behaviour change, so left separable.