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
SequentialAgentpipeline (query_resolution). - Position: First agent in the pipeline. Runs unconditionally on every turn.
- Caller: Called by the
SequentialAgentpipeline (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 intoctx.state["global"]by theresolver_classifier_after_agentcallback before the downstreamdatabase_specialistagent 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).
- Base Instruction Template: The core instruction text defined in
- 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 fromconfig/domain_vocabulary.jsonat server start. (Inserted exactly 1 time in the instruction template).{surface_analysis_vocabulary}: List of canonical condition-type labels (e.g.,stain,corrosion) loaded fromadk_agents_config.yaml. (Inserted exactly 1 time in the instruction template).{multi_image_cap}: Maximum images per surface analysis turn (configured viamulti_image_capinadk_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 containingprevious_turn(the classifier’s own prior output),active_filters(current active filters from global state),history(last 10 resolved queries), andsql_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: defaultis 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: ResolvedQueryResponseis set. This instructs the model to output JSON matching the Pydantic schema.
- Conversation History: Pulled in automatically by ADK because
- Callback-mutated (present, changed, or removed because a callback touched it):
- None. No
before_agentorbefore_modelcallbacks are registered forresolver_classifierthat mutate the request.
- None. No
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
stainandcorrosionare 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.
- Out-of-vocabulary surface conditions (cracks, debris, moisture, fractures : only
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 populateprevious_turnin{previous_context})ctx.state["global"]["active_filters"](to populateactive_filtersin{previous_context})ctx.state["global"]["resolved_query_history"](to populatehistoryin{previous_context})ctx.state["global"]["sql_tool_result"](to populatesql_tool_resultin{previous_context})ctx.state["global"]["user_db_access"](read from global state and bridged intogfor downstream agents; not used in the resolver_classifier instruction template itself — the{user_db_access}placeholder only appears in thedatabase_specialistinstruction)
Writes
ctx.state["resolver_classifier"]: The full validatedResolvedQueryResponseJSON dict.ctx.state["global"]["resolved_query"]: Sourced fromdata["resolved_query"].ctx.state["global"]["active_filters"]: Deep-merged additively fromdata["active_filters"](preserves prior turn filters).ctx.state["global"]["active_dataset"]: Sourced fromdata["active_filters"]["active_dataset"](if present).ctx.state["global"]["query_type"]: Sourced fromdata["query_type"].ctx.state["global"]["scope_decision"]: Sourced fromdata["is_in_scope"]anddata["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.stateand manually persisted tosession.dbviapersist_session_statein theafter_agentcallback. - Temp Prefixes: Ephemeral keys starting with
temp:(liketemp:_llm_last_invocation_id) or__(double underscore) are used in callbacks but are stripped before persistence tosession.dbto prevent leaking transient state or secrets.
Output Schema
- Schema:
ResolvedQueryResponse(defined insrc/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 stripsadditionalPropertiesfrom the JSON schema to prevent Gemini constrained decoding crashes). - Since
resolver_classifierdoes 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.
- 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
Noneand writes a system-error sentinel to state.
- Schema Validation: Validates the parsed dict against
ResolvedQueryResponse. If validation fails, it writes a system-error sentinel to state to prevent downstream crashes. - State Updates: Writes the validated dict to
ctx.state["resolver_classifier"]and updates theglobalstate keys (resolved_query,active_filtersvia additive deep merge,active_dataset,query_type, andscope_decision). - Manual Persistence: Resolves the session ID safely via
_resolve_session_idand commits the updated state tosession.dbviapersist_session_state.
on_model_error_callback
Handles raised Gemini exceptions (network, auth, quota, 5xx) after SDK-level retries are exhausted.
- 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). - 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).
- 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). - Graceful Degradation: If all models are exhausted or a terminal error occurs, it returns a graceful
LlmResponsewith a user-friendly error message, swallowing the exception.
Planner
- Planner: None.
resolver_classifierdoes not use a planner.
ADK Version Verified Against
- Verified against:
google-adk >= 1.26.0(as specified inpyproject.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: defaultin 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.
- The exact token limit or window size for