Skip to content

fix(agent): match non-English keywords in tool-RAG domain classifier#3779

Closed
max-freddyfire wants to merge 1 commit into
odysseus-dev:devfrom
max-freddyfire:fix/domain-classifier-multilingual
Closed

fix(agent): match non-English keywords in tool-RAG domain classifier#3779
max-freddyfire wants to merge 1 commit into
odysseus-dev:devfrom
max-freddyfire:fix/domain-classifier-multilingual

Conversation

@max-freddyfire

Copy link
Copy Markdown
Contributor

Summary

The tool-RAG domain classifier _classify_agent_request (src/agent_loop.py) only matched English keywords, so a non-English agent request matched no domain, was flagged low_signal, and tool retrieval was skipped entirely (low_signal = not continuation and not domains; the low-signal branch pins the tool set to ALWAYS_AVAILABLE). Live repro: Norwegian "Vis meg de fem siste e-postene i innboksen min." got only ask_user/manage_memory/update_plan and the agent told the user to "check your email client or webmail provider directly", while the English phrasing was offered every email tool. This PR extends each domain regex (email, notes/calendar/tasks, documents, web, ui, files, settings, cookbook) with high-precision Swedish/Norwegian/Danish/German/Spanish/French/Italian keywords. Gating semantics, ordering, and English behavior are unchanged; smalltalk stays low-signal in every language.

Target branch

  • This PR targets dev, not main. All PRs land in dev; main is curated by the maintainer at each release. If your PR is on main by accident, click "Edit" on this PR and change the base.

Linked Issue

Fixes #3713
Fixes #3766 (same bug reported from the Italian side; Italian is covered)

Related: #3605/#3606 (missing images domain in the same classifier — intentionally not touched here; it should get the same multilingual treatment once merged), #3771 (orthogonal: restores web tools for genuinely low-signal turns; no diff overlap), #3668/#3683 (sibling English-only pattern in _INTENT_RE).

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)
  • New feature (non-breaking — adds new behaviour)
  • Breaking change (changes or removes existing behaviour)
  • Refactor / cleanup (behaviour unchanged)
  • Documentation only
  • CI / tooling / configuration

Details

Each domain has(...) call gets one additional pattern argument with the non-English keywords, e.g. email:

if has(r"\b(emails?|mails?|gmail|inbox|...)\b",
       r"\b(e-?post\w*|innboks\w*|inkorg\w*|indbakke\w*|posteingang\w*|mejl\w*|correos?|courriels?|boîte de réception|bandeja de entrada|posta in arrivo)\b"):
    domains.add("email")

Tokens were checked one by one against English and cross-language collisions:

  • termin(?!a[lt])\w* (German "Termin") does not match "terminal"/"terminate".
  • notiz(?!ie)\w* (German "Notiz") does not match Italian "notizie" (news) — found via live testing, pinned by a dedicated guard test.
  • German search verbs use suche\w*, never bare such.
  • modell(?:e|en|er|ene|erna|o|i)? does not match British "modelled"/"modelling".
  • meteo does not match "meteor"; French/Spanish "activate" verbs are left out (active\w*/activa\w* would match English "active"/"activate").

A false positive costs a few extra tool schemas offered to the model (same as existing loose English keywords like buy → notes), never an action.

Checklist

End-to-end verification on a live instance (qwen3:30b-thinking via Ollama), same Norwegian request before/after:

# dev (before)
[agent-intent] latest='Vis meg de fem siste e-postene i innboksen min.' ... low_signal=True domains=[]
[tool-rag] Low-signal agent message; skipping retrieval and using always-available tools only
[agent-intent] selected_tools=['ask_user', 'manage_memory', 'update_plan']

# with this PR
[agent-intent] latest='Vis meg de fem siste e-postene i innboksen min.' ... low_signal=False domains=['email']
[agent-intent] selected_tools=['archive_email', 'ask_user', 'bulk_email', 'delete_email', 'list_email_accounts', 'list_emails', ...]

and the agent goes from refusing ("check your email client ... directly") to actually calling list_emails. The Italian repro from #3766 was also run live:

[agent-intent] latest='Cerca su internet le notizie di oggi sul prezzo di bitcoin.' ... low_signal=False domains=['web']
[agent-intent] selected_tools=[..., 'web_fetch', 'web_search']

How to Test

python -m pytest tests/test_tool_rag_multilingual_domains.py -q   # 66 passed (49 fail without the src change)
python -m pytest tests/test_agent_loop.py tests/test_tool_rag_keyword_hints.py tests/test_fenced_example_not_executed_for_native_models.py -q   # 68 passed
python -m py_compile src/agent_loop.py                            # ok

The new test file exercises _classify_agent_request directly (it is deterministic string matching): 52 non-English requests across seven languages and eight domains must classify into the same domain as their English equivalent with low_signal=False; English phrasings are pinned to their current classification; greetings in all languages must stay low_signal=True; a guard test pins the notizie/Notiz cross-language collision.

Manual check: in Agent Mode, send Vis meg de fem siste e-postene i innboksen min. and watch the [agent-intent] log line flip from low_signal=True domains=[] to low_signal=False domains=['email'].

Visual / UI changes — REQUIRED if you touched anything that renders

N/A — backend tool-selection logic and tests only. No CSS, HTML, SVG, or static/js/ files changed.

  • Screenshot or short clip of the change in the running app, attached below. Mobile screenshot too if the change affects mobile.
  • Style match: no visual styling changes.
  • No new component patterns.
  • I am not an LLM agent submitting a bulk PR. If you are, please open an issue describing the problem first — bulk auto-generated PRs that don't match the project's visual style are closed on sight, even when the underlying fix is correct.

This is a single-purpose, AI-assisted fix following the issue-first flow: the bug was root-caused and confirmed on #3713 (with credit to the original reporter) before opening this PR.

Screenshots / clips

N/A — no visual rendering changes.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jun 10, 2026
_classify_agent_request only matched English domain keywords, so a
non-English agent request ("Vis meg de fem siste e-postene i innboksen
min.") matched no domain, was flagged low_signal, and tool retrieval was
skipped entirely: the model received only the always-available tools and
told the user it could not do the task, while the English equivalent was
offered every email tool.

Extend each domain keyword regex with high-precision Swedish / Norwegian /
Danish / German / Spanish / French / Italian nouns and verbs (email,
notes/calendar/tasks, documents, web, ui, files, settings, cookbook).
Gating semantics, ordering, and English behavior are unchanged; smalltalk
stays low-signal in every language. Tokens are chosen to avoid English
collisions (termin(?!a[lt]) blocks terminal/terminate, suche\w* avoids
"such", the modell suffix alternation avoids British "modelled", meteo
does not match "meteor").

Regression tests exercise the classifier directly per language and domain,
pin English classification, and pin smalltalk as low-signal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vdmkenny

Copy link
Copy Markdown
Collaborator

Thanks for the detailed repro and the test coverage. The stall you are describing is real, but I am going to close this approach rather than merge it, for the same reason as #3802.

Hardcoding per-language keyword and phrase lists into src/agent_loop.py is not maintainable. Every additional language means another set of verbs, nouns, and phrasing heuristics baked into the classifier, and those lists drift and go stale fast. The bigger problem is cross-language collision: matching an open-ended multilingual keyword set invites false positives from tokens that are innocuous in one language but a trigger in another, so the gate fires on input that was never that intent. The PR already has to hand-tune patterns to avoid colliding with common English words, which is exactly the fragility I want to avoid, and it gets worse with each language added.

The direction we want for multilingual intent is a proper i18n layer rather than inline keyword grab-bags: detect or read the user's configured language, and match intent against English plus that one language only, sourced from a real i18n module instead of regex literals scattered through the agent code. That keeps the matching set small and predictable and removes the collision problem entirely.

So this is a close on the approach, not on the goal. An i18n-backed version would be a welcome follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

2 participants