Class: Assistant::QueryClassifier

Inherits:
Object
  • Object
show all
Defined in:
app/services/assistant/query_classifier.rb

Overview

Single AI-powered classifier that decides both which tool services to enable
AND which model tier should handle the turn. Runs one cheap LLM call against
the tool_routing model and returns a Result struct consumed by both
ToolRouter and ChatService.

Replaces the regex-based ChatService.auto_select_candidate for the auto path
and shares the single LLM round-trip that ToolRouter was already making.

Usage:
result = Assistant::QueryClassifier.classify(
"Review and analyze the Valentine Electric opportunity and draft an email",
permitted_services: %w[content app_db sales_management]
)
result.tools # => ["app_db", "sales_management", "content"]
result.model_tier # => :pro
result.reason # => "Multi-part research + email composition"

On any classifier failure (timeout, parse error, API down) returns a Result
with tools=[] and model_tier=nil so callers can fall back to their existing
defaults (content for tools, regex picker for model).

Defined Under Namespace

Classes: Result

Constant Summary collapse

EMPTY_RESULT =
Result.new(tools: [], model_tier: nil, reason: nil).freeze
MODEL_TIERS =
%w[flash pro].freeze
AI_TIMEOUT_SECONDS =

Last-resort wall-clock guard. RubyLLM/Faraday already enforce per-request
HTTP timeouts, but their internal retry loop (up to 5 attempts with
exponential backoff) can extend a stuck call well past any single-request
budget. Timeout.timeout caps the total time the classifier can block the
request thread, so a flaky upstream never delays the user's turn. Matches
the same constant on ToolRouter's previous classifier call.

20

Class Method Summary collapse

Class Method Details

.classify(query, permitted_services:) ⇒ Result

Note:

Never raises — all exceptions (timeout, rate limit, parse error, API
failure) are rescued internally and return EMPTY_RESULT so callers can
fall back to their own defaults without wrapping the call in rescue.

Classify a query end-to-end: tool services AND model tier in one call.

Parameters:

  • query (String)

    the user's message

  • permitted_services (Array<String>)

    services the user's role allows

Returns:



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'app/services/assistant/query_classifier.rb', line 52

def self.classify(query, permitted_services:)
  return EMPTY_RESULT if query.blank? || permitted_services.empty?

  tool_menu = Assistant::ToolRouter.build_tool_menu(permitted_services)
  return EMPTY_RESULT if tool_menu.empty?

  raw = call_classifier(query, tool_menu)
  parse(raw, permitted_services)
rescue Timeout::Error
  Rails.logger.warn('[QueryClassifier] Timed out, returning empty result')
  EMPTY_RESULT
rescue RubyLLM::RateLimitError
  Rails.logger.warn('[QueryClassifier] Rate limited, returning empty result')
  EMPTY_RESULT
rescue RubyLLM::Error => e
  Rails.logger.warn("[QueryClassifier] RubyLLM error: #{e.class}#{e.message}")
  EMPTY_RESULT
rescue StandardError => e
  Rails.logger.warn("[QueryClassifier] Unexpected failure: #{e.message}")
  EMPTY_RESULT
end