Skip to content

feat(agent): pluggable LLM domain classifier for multi-language tool retrieval - #5751

Open
holden093 wants to merge 7 commits into
odysseus-dev:devfrom
holden093:feat/llm-domain-classifier-reopen
Open

feat(agent): pluggable LLM domain classifier for multi-language tool retrieval#5751
holden093 wants to merge 7 commits into
odysseus-dev:devfrom
holden093:feat/llm-domain-classifier-reopen

Conversation

@holden093

Copy link
Copy Markdown
Contributor

Note: Re-opening of #4355, auto-closed 2026-07-23 during the odysseus-dev org transfer (the dev history rewrite force-closed all open PRs). Same feature, cleanly rebased onto the current dev. Original review thread: #4355.

Summary

_classify_agent_request() uses English-only regex patterns to detect domains (contacts, web, email, …). Non-English queries match no domains, are flagged low_signal, and the agent receives only a handful of always-available tools — it cannot reach resolve_contact, web_search, send_email, or any domain-specific tool. This is reported in #3713, #3766, and #3668.

This PR adds an opt-in LLM-based domain classifier that replaces the regex patterns with a small utility-model call. The model understands every language without maintaining keyword lists.

How it works

Set the environment variable:

ODYSSEUS_DOMAIN_CLASSIFIER=llm

When enabled, every user query is classified by the utility model into zero or more domain categories. The regex classifier is bypassed entirely. When unset, the existing regex path runs unchanged (the LLM is still available as a fallback when no regex domains match).

Model requirements

A capable instruction-following model is required for reliable results. Tested successfully with deepseek-v4-flash across Italian, English, German, French, and Spanish. Smaller / weaker local models may not follow the constrained output format and will produce junk classifications — the feature is opt-in so operators can enable it only when a suitable utility model is available.

Target branch

  • This PR targets dev, not main.

Linked Issues

Closes #3713
Closes #3766
Related: #3668

Type of Change

  • Bug fix
  • New feature (non-breaking — opt-in, no change to default behaviour)
  • Breaking change
  • Refactor / cleanup
  • Documentation only
  • CI / tooling / configuration

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above.
  • I tested with a capable utility model (deepseek-v4-flash) across 5 languages.

How to Test

  1. Ensure a capable model is configured as the utility model
  2. Set ODYSSEUS_DOMAIN_CLASSIFIER=llm in the environment (.env or compose)
  3. Send non-English queries, e.g. "Mi dici il numero di telefono di Massimo?"
  4. Before: agent receives only always-available tools, cannot call resolve_contact
  5. After: _llm_classify_domains returns contacts, tools seed correctly, agent calls resolve_contact

Regression: with the env var unset, English regex classification works as before.

Changes

One file, +98 lines in src/agent_loop.py:

  • _llm_classify_domains() — async function that calls the utility model for domain classification
  • Integration in stream_agent_loop — gated by ODYSSEUS_DOMAIN_CLASSIFIER == "llm"

Visual / UI changes

  • N/A (backend only, no UI touched)
  • I am not an LLM agent submitting a bulk PR.

…retrieval

Add an async _llm_classify_domains() function that calls the utility
model to classify user queries into domain categories.  Gated by the
environment variable ODYSSEUS_DOMAIN_CLASSIFIER=llm.

When the env var is set, the English regex classifier is skipped
entirely and every query is routed through the LLM for language-agnostic
classification.  Without the env var the existing regex path runs
unchanged (the LLM is available as a fallback when no regex domains
match).

The LLM call uses the configured utility model.  A capable instruction-
following model (e.g. deepseek-v4-flash) is required for reliable
results; smaller local models may not follow the constrained output
format.  The feature is opt-in so operators can enable it only when a
suitable utility model is available.

Closes: odysseus-dev#3713, odysseus-dev#3766 (non-English domain classifier)
Always pass _recent_context_for_retrieval() to _llm_classify_domains()
so multi-language follow-ups like "Fai tutto tu", "Sì", "Fallo"
inherit domain from prior user turns instead of losing tool context.

Removes the continuation guard in LLM mode — the LLM now runs every
turn with full context, making regex language limitations irrelevant.

Also updates the classifier prompt to label multi-turn context as
"Recent conversation" and adds a "Classify the latest request"
instruction for clarity.

Closes: odysseus-dev#4355 domain-context-loss on non-English follow-ups
…ompt

Split the prompt into "Conversation history" and "Latest request"
sections when latest_msg is provided. Prevents short keywords like
"internet" from being drowned by domain-heavy conversation context
when the user asks a cross-domain question (e.g. "search the web for
this" while in a contacts-heavy conversation).
@github-actions github-actions Bot added the ready for review Description complete — ready for maintainer review label Jul 25, 2026
holden093 and others added 2 commits July 25, 2026 18:43
Commit 5019845 (the original domain-classifier feature commit) committed an
unresolved conflict (<<<<<<< HEAD / ======= with no closing marker) into
src/agent_loop.py, breaking 'python -m compileall'. Both sides are valid code;
resolution is simply dropping the two stray marker lines, matching the author's
own fix on local-dev.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDcZuLNAo4FVc8WtMpZWVp
dev's src/agent_loop.py annotates with dict[str, Any] at lines 1503/1877 but
only imports AsyncGenerator, List, Dict, Optional, Set (no Any, no
from __future__ import annotations), so importing the module raises
NameError: name 'Any' is not defined — breaking pytest collection for every
test that transitively imports agent_loop. Add Any to the typing import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GDcZuLNAo4FVc8WtMpZWVp
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

1 participant