Resolver Classifier

Overview

resolver_classifier is the first agent in the SequentialAgent pipeline (query_resolution). It acts as the primary semantic bridge between raw user queries and the downstream database specialist. Its sole responsibility is semantic classification and query resolution: it determines user intent, extracts structured filters, resolves conversational ambiguity and relative dates, enforces domain scope boundaries, and plans downstream sub-tasks.


Role and Position

  • Role: Root-level sub-agent within the SequentialAgent pipeline (query_resolution).
  • Position: First agent in the pipeline. Runs unconditionally on every turn.
  • Caller: Called by the SequentialAgent pipeline (query_resolution) when the pipeline is executed.
  • Callee: None. It does not call other agents. Its output is written to ctx.state["resolver_classifier"] and merged into ctx.state["global"] by the resolver_classifier_after_agent callback before the downstream database_specialist agent runs.

Tools

  • Tools: None. No tools are bound to resolver_classifier.

LLM Context Design

This section details every item that ends up in the actual request sent to the LLM, categorized by its lifecycle and source.

Context Lifecycle Categorization

  • Static (fixed at code-authoring time, identical every call):
    • Base Instruction Template: The core instruction text defined in adk_agents_config.yaml (excluding placeholders).
    • Few-Shot Examples: The 17 few-shot examples embedded directly in the instruction text (Examples 1, 2, 2b, 3–12, 13a, 13b, 14, 14b).
  • Dynamic · session (varies by session/user state but stable within a session):
    • {available_datasets}: List of accessible dataset slugs, pulled from session state (global.available_datasets). If empty, defaults to a list of common test datasets in unit tests. (Inserted exactly 1 time in the instruction template).
    • {domain_vocabulary}: Formatted markdown of domain terms loaded from config/domain_vocabulary.json at server start. (Inserted exactly 1 time in the instruction template).
    • {surface_analysis_vocabulary}: List of canonical condition-type labels (e.g., stain, corrosion) loaded from adk_agents_config.yaml. (Inserted exactly 1 time in the instruction template).
    • {multi_image_cap}: Maximum images per surface analysis turn (configured via multi_image_cap in adk_agents_config.yaml). (Inserted exactly 11 times in the instruction template).
  • Dynamic · per-turn (recomputed every turn):
    • {current_time}: Current UTC timestamp for relative date resolution, recomputed on every turn. (Inserted exactly 3 times in the instruction template).
    • {previous_context}: JSON object containing previous_turn (the classifier’s own prior output), active_filters (current active filters from global state), history (last 10 resolved queries), and sql_tool_result (SQL results from the prior turn). Recomputed on every turn. (Inserted exactly 1 time in the instruction template).
    • User Message: The current turn’s raw user query.
  • ADK-injected / default (added automatically by the ADK framework itself):
    • Conversation History: Pulled in automatically by ADK because include_contents: default is set. This includes user messages, assistant responses, tool calls, and tool responses from previous turns in the session.
    • Output Schema Formatting Instructions: Automatically appended by ADK to the end of the prompt because output_schema: ResolvedQueryResponse is set. This instructs the model to output JSON matching the Pydantic schema.
  • Callback-mutated (present, changed, or removed because a callback touched it):
    • None. No before_agent or before_model callbacks are registered for resolver_classifier that mutate the request.

History Inclusion (include_contents)

  • Setting: include_contents: default
  • Behavior: Instructs the ADK framework to automatically include the conversation history (user messages, assistant responses, tool calls, and tool responses) in the LLM request. The history is formatted as a sequence of turns.

Instruction Template and Placeholders

The instruction template is defined in src/kavai/config/adk_agents_config.yaml. The factory (factory.py) substitutes placeholders into the instruction text at agent build time:

Placeholder Source What it injects Insertion Count
{surface_analysis_vocabulary} adk_agents_config.yaml Canonical condition-type labels (e.g., stain, corrosion) 1
{multi_image_cap} adk_agents_config.yaml Maximum images per surface analysis turn (configured via multi_image_cap) 11
{domain_vocabulary} config/domain_vocabulary.json Industrial inspection domain terms 1
{current_time} Runtime Current UTC timestamp for relative date resolution 3
{available_datasets} Session state List of dataset slugs accessible to the user 1
{previous_context} Session state (factory.py) JSON object containing:
- previous_turn: The classifier’s own prior output (prev_self)
- active_filters: Current active filters from global state
- history: Last 10 resolved queries
- sql_tool_result: SQL results from the prior turn
1

Prompt Scope & Classification Rules

The agent classifies every in-scope query into one of these query_type values:

query_type When to use
metadata_lookup Technical file metadata (GPS, ISO, focal length), dataset-level attributes, listing distinct categories or locations
record_filtering Gallery/list of records filtered by location, time range, or modality
anomaly_retrieval Images containing specific annotated defects (corrosion, insulation gaps, hotspots)
aggregation COUNT, MAX, MIN, AVG operations
deterministic_linking Spatial or temporal correlation of gas surveys with image datasets
general Ambiguous or unclassifiable in-scope requests
surface_analysis RGB image surface condition analysis (stain, corrosion) : invokes rgb_surface_analyzer downstream

Scope Boundaries

  • In-Scope: All query types listed above, subject to the vocabulary gate for surface_analysis.
  • Out-of-Scope (always rejected):
    • Out-of-vocabulary surface conditions (cracks, debris, moisture, fractures : only stain and corrosion are supported).
    • Causal reasoning (“why did this corrode?”).
    • Predictive modeling (RUL, failure forecasting).
    • Professional repair advice.
    • Thermal or gas analysis (reserved for future tickets).
    • Bulk/unbounded analysis (“analyze all images in the dataset”).
    • Vague analysis without a specific image referent.

Strict Modality Rule

Sensor modalities (RGB, Thermal, Gas, MultiModal) are set to true only when explicitly named by the user. Generic words like “images”, “photos”, “pictures” do NOT imply RGB=true. * Sensor trigger words: "RGB", "color camera", "visible", "visuals", "thermal", "infrared", "IR", "gas", "gas readings", "gas sensor". If such a word appears, set the corresponding modality to true. * For aggregation queries (e.g., “how many images”), all sensor modalities MUST be false unless a sensor trigger word is present (e.g., “how many thermal images”). * For anomaly_retrieval queries (e.g., “find insulation gaps”, “list corrosion defects”), all sensor modalities MUST be false unless a sensor trigger word is present. Anomaly type names (corrosion, stain, insulation gap, hotspot, leak) are NOT sensor trigger words — do not infer RGB=true from an anomaly name alone. * For record_filtering queries, all sensor modalities MUST be false unless a sensor trigger word is present. * For metadata_lookup, all sensor modalities MUST be false regardless of whether a sensor name appears (we are looking up info ABOUT the sensor, not filtering records by it). * For deterministic_linking, toggle MultiModal to true and only the modalities whose sensor trigger words appear. * For ambiguous queries (is_ambiguous=true), all sensor modalities MUST be false — never guess modality when the dataset or context is unclear.


State I/O

Reads

  • ctx.state["global"]["available_datasets"] (to populate {available_datasets})
  • ctx.state["resolver_classifier"] (to populate previous_turn in {previous_context})
  • ctx.state["global"]["active_filters"] (to populate active_filters in {previous_context})
  • ctx.state["global"]["resolved_query_history"] (to populate history in {previous_context})
  • ctx.state["global"]["sql_tool_result"] (to populate sql_tool_result in {previous_context})
  • ctx.state["global"]["user_db_access"] (read from global state and bridged into g for downstream agents; not used in the resolver_classifier instruction template itself — the {user_db_access} placeholder only appears in the database_specialist instruction)

Writes

  • ctx.state["resolver_classifier"]: The full validated ResolvedQueryResponse JSON dict.
  • ctx.state["global"]["resolved_query"]: Sourced from data["resolved_query"].
  • ctx.state["global"]["active_filters"]: Deep-merged additively from data["active_filters"] (preserves prior turn filters).
  • ctx.state["global"]["active_dataset"]: Sourced from data["active_filters"]["active_dataset"] (if present).
  • ctx.state["global"]["query_type"]: Sourced from data["query_type"].
  • ctx.state["global"]["scope_decision"]: Sourced from data["is_in_scope"] and data["scope_reason"] as {"status": "in_scope"|"out_of_scope", "reason": scope_reason, "scope_reason": scope_reason}.

Scope and Persistence

  • Scope: Session-scoped. All state is written to ctx.state and manually persisted to session.db via persist_session_state in the after_agent callback.
  • Temp Prefixes: Ephemeral keys starting with temp: (like temp:_llm_last_invocation_id) or __ (double underscore) are used in callbacks but are stripped before persistence to session.db to prevent leaking transient state or secrets.

Output Schema

  • Schema: ResolvedQueryResponse (defined in src/kavai/foundation_services/utility/models/resolver_models.py).
  • How it changes the request:
    • ADK automatically appends formatting instructions to the end of the prompt, forcing the model to output a JSON object matching the schema.
    • The schema is wrapped using make_gemini_compatible_model (which strips additionalProperties from the JSON schema to prevent Gemini constrained decoding crashes).
    • Since resolver_classifier does not call any tools, there is no interaction between the output schema and tool calling.

ResolvedQueryResponse Schema Fields

Field Type Default Description
is_ambiguous bool False True if the query requires user clarification before it can be resolved.
is_in_scope bool True True if the request is within the supported industrial inspection domain.
clarification_msg Optional[str] None Clarification question presented to the user. Required when is_ambiguous=True.
resolved_query str "" Standalone, fully-qualified version of the user request with entities and relative dates resolved.
active_filters ActiveFilters see below Consolidated structured filters extracted from the query.
sub_tasks List[str] [] Discrete sub-tasks required to fulfill the request. Guides database_specialist.
scope_reason str "Decided by agent logic." Natural language justification for the scope decision.
query_type str "general" One of the 7 canonical values listed in §4.4.
modality str "general" Primary modality: "image", "data", "analysis", or "general".
target_image_ids Optional[List[str]] None 1–multi_image_cap image UUIDs or filenames. Populated only for surface_analysis. None for all other query types.

ActiveFilters

Field Type Default Description
active_dataset Optional[str] None Selected dataset slug.
active_physical_location Optional[str] None Physical location filter (e.g., "Unit 4").
active_time TimeRange TimeRange() ISO-8601 start_date / end_date.
active_sensor_modality SensorModality all False Strict booleans: RGB, Thermal, Gas, MultiModal.
active_anomalies List[str] [] Anomaly labels to filter by (e.g., ["Insulation Gap"]).

Callbacks

Callback Used? Behavior
before_agent ❌ N/A
before_model ❌ N/A
before_tool ❌ N/A (no tools)
after_tool ❌ N/A (no tools)
after_model ❌ N/A
after_agent ✅ resolver_classifier_after_agent
on_model_error ✅ on_model_error_callback

resolver_classifier_after_agent

Runs after the agent emits its response. Parses the JSON output, validates it against the Pydantic schema, and writes to session state.

  1. Robust Parsing: Implements a 3-tier parsing strategy:
    • Tier 1: Strict JSON parse.
    • Tier 2: Markdown-fenced block extraction (```json).
    • Tier 3: Balanced-braces greedy extraction. If all tiers fail, the callback returns None and writes a system-error sentinel to state.
  2. Schema Validation: Validates the parsed dict against ResolvedQueryResponse. If validation fails, it writes a system-error sentinel to state to prevent downstream crashes.
  3. State Updates: Writes the validated dict to ctx.state["resolver_classifier"] and updates the global state keys (resolved_query, active_filters via additive deep merge, active_dataset, query_type, and scope_decision).
  4. Manual Persistence: Resolves the session ID safely via _resolve_session_id and commits the updated state to session.db via persist_session_state.

on_model_error_callback

Handles raised Gemini exceptions (network, auth, quota, 5xx) after SDK-level retries are exhausted.

  1. Invocation Reset: Resets retry and model-swap counters at the start of each new pipeline invocation using a temporary invocation ID (temp:_llm_last_invocation_id).
  2. App-Level Retries: If the error is retryable, it increments the retry counter and directly retries the request with the same model (up to 2 retries).
  3. Model Swap: If retries are exhausted or the error is non-retryable but swap-eligible, it swaps the model to the next one in the fallback chain: gemini-3.1-flash-lite (default) → gemini-3.5-flash (first fallback) → gemini-2.5-pro (last resort).
  4. Graceful Degradation: If all models are exhausted or a terminal error occurs, it returns a graceful LlmResponse with a user-friendly error message, swallowing the exception.

Planner

  • Planner: None. resolver_classifier does not use a planner.

ADK Version Verified Against

  • Verified against: google-adk >= 1.26.0 (as specified in pyproject.toml).

Generation Config

Parameter Value Source / Notes
Model gemini-3.1-flash-lite Pinned in adk_agents_config.yaml
Temperature Default (1.0) Left to ADK/Gemini defaults
Max Output Tokens Default Left to ADK/Gemini defaults
Safety Settings Default Left to ADK/Gemini defaults
Stop Sequences None Left to ADK/Gemini defaults

Open Questions / Unverified Items

  • Unverified items:
    • The exact token limit or window size for include_contents: default in ADK 1.26.0+ is not explicitly documented in the codebase, but it is assumed to be managed dynamically by the ADK framework.
    • Whether the ADK framework performs any prompt-level optimization or compression on the conversation history before sending it to the model.