Class: Assistant::QueryClassifier
- Inherits:
-
Object
- Object
- Assistant::QueryClassifier
- 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
-
.classify(query, permitted_services:) ⇒ Result
Classify a query end-to-end: tool services AND model tier in one call.
Class Method Details
.classify(query, permitted_services:) ⇒ Result
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.
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? = Assistant::ToolRouter.(permitted_services) return EMPTY_RESULT if .empty? raw = call_classifier(query, ) 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.}") EMPTY_RESULT rescue StandardError => e Rails.logger.warn("[QueryClassifier] Unexpected failure: #{e.}") EMPTY_RESULT end |