Report Generator

Overview

report_generator is the third and final agent in the SequentialAgent pipeline (query_resolution). It acts as the “FINAL AUTHORITY” and data presentation specialist for the Kavion AI Assistant. Its sole responsibility is to synthesize retrieved database records, metadata, and analytical results into a clear, structured, and highly readable Markdown report. It operates under a strict “NO ANALYSIS ALLOWED” mandate, presenting facts, counts, and sample images exactly as retrieved without drawing subjective conclusions, making recommendations, or performing causal reasoning.


Role and Position

  • Role: Root-level sub-agent within the SequentialAgent pipeline (query_resolution).
  • Position: Third and final 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.
  • Input: Reads consolidated state from ctx.state (including resolver_classifier, database_specialist, and rgb_surface_analyzer outputs).
  • Output: Its output is written to ctx.state["report_generator"] and processed by the report_generator_after_agent callback, which assembles the final FAT schema payload (StructuredCrewResponse) and writes it back to ctx.state["report_generator"].

Tools

  • Tools: None. No tools are bound to report_generator. It is a pure data presentation and synthesis agent.

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 8 few-shot examples embedded directly in the instruction text (Examples 1 to 8).
  • Dynamic · session (varies by session/user state but stable within a session):
    • {active_filters}: Sourced from global.active_filters in session state. (Inserted exactly 1 time in the instruction template).
    • {resolver_classifier}: Sourced from resolver_classifier in session state. (Inserted exactly 1 time in the instruction template).
    • {database_specialist}: Sourced from database_specialist in session state. (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 18 times in the instruction template).
  • Dynamic · per-turn (recomputed every turn):
    • 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: ReporterSynthesisSchema 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 report_generator 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
{active_filters} Session state (factory.py) JSON representation of active filters 1
{resolver_classifier} Session state (factory.py) JSON representation of classifier output 1
{database_specialist} Session state (factory.py) JSON representation of database specialist output 1
{multi_image_cap} adk_agents_config.yaml Maximum images per surface analysis turn (configured via multi_image_cap) 18

Ultra-Lean Schema V3 Reporting Contract

The agent operates under the Ultra-Lean Schema V3 reporting contract. It is strictly mandated to output ONLY the markdown_report field within a JSON object matching the ReporterSynthesisSchema.

  • No Hand-Authored UI Tags: The agent MUST NOT output any UI tags (such as [[image-gallery:...]] or [[surface-analysis:...]]). These are injected deterministically in Python by the report_generator_after_agent callback using the smart_router.py dispatch table.
  • No Raw UUIDs: The agent MUST NOT output raw database UUIDs in the report text. It should use filenames or human-readable identifiers.
  • No Findings Objects: The agent MUST NOT output a separate findings list or object. All findings must be integrated directly into the narrative Markdown report.
  • No Top-Level Title: The first non-blank line of the Markdown report MUST NOT begin with a single # (use ## or lower).

State I/O

Reads

  • ctx.state["resolver_classifier"] (to read is_in_scope, is_ambiguous, query_type, target_image_ids, resolved_query)
  • ctx.state["database_specialist"] (to read retrieved_rows, errors, is_complete, agent_result, and rgb_surface_analyzer.results for surface analysis turns)
  • ctx.state["global"]["active_filters"] (to read active_filters for multi-turn context retention)
  • ctx.state["temp:real_execute_sql"] (to read the real SQL rows captured by database_specialist_after_tool)

Writes

  • ctx.state["report_generator"]: Overwritten by the report_generator_after_agent callback with the assembled FAT payload (StructuredCrewResponse).

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:real_execute_sql or temp:_llm_last_invocation_id) are used in callbacks but are stripped before persistence to session.db to prevent leaking transient state or secrets.

Output Schema

  • Schema: ReporterSynthesisSchema (defined in src/kavai/foundation_services/utility/models/responses/structured.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 report_generator does not call any tools, there is no interaction between the output schema and tool calling.

ReporterSynthesisSchema Schema Fields

Field Type Default Description
markdown_report str N/A (Required) The complete narrative response/report in Markdown.

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 ✅ report_generator_after_agent
on_model_error ✅ on_model_error_callback

report_generator_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: Greedy regex fallback (matches (\{.*\}) via re.search).
  2. Context and Specialist Data Retrieval:
    • Reads database_specialist output from ctx.state["database_specialist"].
    • Reads the real execute_sql rows captured by database_specialist_after_tool from ctx.state["temp:real_execute_sql"] (falls back to database_specialist.retrieved_rows if no SQL was executed or in unit tests).
    • Reads anomalies and is_complete from database_specialist.
  3. Smart Routing:
    • Invokes smart_route(ctx.state, raw_rows) from smart_router.py to determine the UI layout (markdown_report or general) and the UI tag to append (e.g., [[image-gallery:slug]], [[gas-readings:slug]], [[dataset-list:all]], [[surface-analysis:slug]]).
    • If layout is markdown_report, the main chat bubble text is set to a generic status message: "Analysis complete. See the detailed report in the side panel.", and the full markdown report (with the appended UI tag) is written to the markdown_report field.
    • If layout is general, the main chat bubble text is set to the raw markdown report, and markdown_report is set to None (no side-panel).
  4. Analysis Turn Handling:
    • Detects if the turn contains sensor-analysis output via has_analysis_output(ctx.state).
    • If it is an analysis turn, it builds fat_analysis_results using build_analysis_results_from_state(ctx.state).
    • Populates single-image fields (analysis_target and bounding_regions) from the first result for backward compatibility.
    • Builds fat_images list:
      • If actual image records are present in raw_rows, it uses them.
      • Else if fat_analysis_results is present, it builds fat image rows using _build_fat_image_row(img_id, target, analysis_dict).
      • Else if fat_analysis_target is present, it builds a single fat image row.
    • If it is NOT an analysis turn, fat_images is set to raw_rows if layout is image_gallery or markdown_report, else [].
  5. Final Payload Assembly (FAT Schema):
    • Assembles the final StructuredCrewResponse dict:
      • is_complete: is_complete
      • text: final_text
      • response_type: final_layout
      • images: fat_images
      • image_list: []
      • datasets: raw_rows if final_layout == "dataset_list" else []
      • gas_readings: raw_rows if final_layout == "gas_readings" else []
      • findings: []
      • markdown_report: final_markdown
      • components: []
      • metadata: {"dataset_slug": active_slug}
      • anomalies: anomalies
      • results: {}
      • bounding_regions: fat_bounding_regions
      • analysis_target: fat_analysis_target
      • analysis_results: fat_analysis_results
  6. State Override:
    • Overwrites ctx.state["report_generator"] with the assembled FAT payload.

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. report_generator 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.