Skip to main content

Google Agent Development Kit (ADK): A hands-on tutorial

Google’s Agent Development Kit (ADK) is a modern, modular Python framework for building, orchestrating, and tracing sophisticated AI-powered agents and workflows - supporting diverse models and tools and designed for easy integration, extensibility, and production observability.
Created on June 26|Last edited on July 8
The Agent Development Kit (ADK) from Google is a modern framework designed to streamline the development and deployment of AI-powered agents. ADK addresses the challenges inherent in building sophisticated, autonomous agentic systems by blending principles from traditional software engineering, such as modularity, reusability, and maintainability, with the emerging needs of agentic AI architectures.
Agent Development Kit is distinguished by a model-agnostic, deployment-flexible design, meaning it works with a wide range of AI models and infrastructures. While it is optimized for Gemini and the Google Cloud ecosystem (like Vertex AI), it can also integrate with various third-party AI frameworks. Its architecture is fundamentally event-driven, promoting clear separation between agent decision-making, workflow orchestration, and system-level concerns such as persistence and security.
Today we'll look at some best practices when building agents, build some agents with Google's ADK, and evaluate them with W&B Weave.


Table of contents



What is an agent? 

First, let's level set on terminology. An AI agent is an intelligent system designed to achieve specific goals through planning, utilizing external tools, retaining memory, and adapting over time. Unlike traditional automation, which follows rigid, predefined rules, AI agents dynamically process information, make decisions, and refine their approach based on the feedback they receive.
While chatbots primarily engage in conversations and require user input at every step, AI agents operate independently of user input. They don’t just generate responses; they take action, interact with external systems, and manage multi-step agentic workflows without constant supervision.
Some key components of AI agents include:
  • Tools: Connect to APIs, databases, and software to extend functionality.
  • Memory: Store information across tasks to improve consistency and recall.
  • Continual learning: Adapt and refine strategies based on past performance.
  • Orchestration: Manage multi-step processes, break down tasks, and coordinate with other agent

The basics of ADK

Building with ADK starts by defining agents, which are the primary intelligent entities within the system. Key agent types include:
  • LLM agents: These leverage large language models for reasoning, language understanding, and dynamic routing within workflows. They serve as the "brains" for flexible decision-making and are critical for natural language interfaces and complex logic.
  • Sequential agents: Execute a predefined, step-by-step workflow. Useful for linear pipelines where each task follows the previous in a strict order
  • Parallel agents: Coordinate concurrent execution of multiple sub-tasks, increasing throughput for tasks that can be processed simultaneously.
  • Loop agents: Automate repetitive processing or refinement. For example, they might iteratively generate outputs until a certain condition is met (e.g., producing five images or repeating until quality criteria are satisfied).
  • Custom agents: Developers can create specialized agents for unique requirements that don't fit existing patterns.
A hallmark feature of ADK is its support for multi-agent architectures, allowing agents to be composed hierarchically and even used as "tools" by one another, facilitating scalable delegation and rich problem decomposition.

State, memory, session, and tools

ADK’s approach to context is one of its core strengths:
  • Session: Represents a single ongoing interaction (or conversation) between a user and the agent(s). Sessions encapsulate the sequence of "events" (user inputs, agent responses, tool invocations) and provide continuity through a given exchange. SessionService managers persist and retrieve sessions, with both volatile (in-memory) and persistent (database, Vertex AI) options.
  • State: The mutable scratchpad tied to a session (or user). State stores live, key-value data are relevant only to the current interaction (e.g., user choices, conversation flags, shopping cart contents). State updates are triggered by events and are managed to ensure atomicity and consistency.
  • Memory: Long-term, cross-session knowledge storage. Memory enables agents to search for information or context beyond a single session, allowing for the retrieval of prior interactions or the ingestion of external knowledge corpora. Memory is accessed through tools and may use keyword or semantic search (e.g., with Vertex AI RAG).
  • Tools: Pluggable actions or interfaces agents can call to accomplish tasks. These range from:
    • Pre-built tools (like Search or Code Exec)
    • Custom functions (developer-defined workflows or API calls)
    • Third-party integrations (LangChain, CrewAI, etc.)
    • Other agents (AgentTools), supporting recursive delegation
    • Google Cloud or OpenAPI tools for accessing cloud or external services
The abstraction of "tools" empowers agents to act, reason, and interface with the external world (APIs, databases, other agents) without hardwiring those behaviors directly into their core logic.

Artifacts and callbacks

  • Artifacts refer to non-textual or binary outputs generated during agent operation (such as images, documents, files, or other media). ADK offers artifact management through ArtifactService, allowing agents to store and retrieve large objects separately from event/state data, which is critical for workflows involving multimodal data or deliverables.
  • Callbacks are user-defined hooks that plug into the agent execution lifecycle. For example, a developer may specify before_agent_callback or after_model_callback functions to customize logging, apply additional validation, or inject custom logic at key points during agent operation. Callbacks increase observability and extensibility and allow for specialized control without altering the underlying agent codebase.
Overall, Google’s Agent Development Kit provides a unified, highly modular framework for building next-generation agentic AI applications. By emphasizing structured workflows (via agents), robust context and memory management (via session/state/memory), extensibility (through tools and agent composition), and production-readiness (with artifacts, persistence, and callbacks), ADK bridges the worlds of traditional software engineering with cutting-edge AI. Whether building simple chatbots or orchestrating complex, multi-agent business processes, ADK offers the tools, abstractions, and best practices necessary for scalable, maintainable agent development.
To get started with Google ADK, you'll just need Python 3.8 or newer. Open your terminal or command prompt and run the following command:
pip install google-adk weave google-genai
This will install the basic Agent Development Kit.
(Note that we will use Vertex AI in this tutorial.) For more information on setting up VertexAI, see the article we recently wrote.

Building a basic writing agent with Google ADK

In this section, we'll build a simple but powerful multi-agent pipeline using Google’s Agent Development Kit (ADK). With ADK, you can create intelligent AI workflows by composing together various kinds of agents, each handling a specific task and passing information to the next. This modular approach not only keeps the logic clean and maintainable but also allows each agent to specialize in its area of expertise.
The example we’ll use covers a common scenario: researching a topic on the web, writing a short summary based on those findings, having another agent critique that draft for quality and factual accuracy, and finally, revising the text based on the feedback. While this could be built as a single large language model prompt, structuring it as a chain of collaborating agents means each step becomes more transparent, reusable, and easier to debug or extend.
We’ll keep things focused by leveraging ADK’s built-in agent types (like LLM agents and sequential orchestration), and plug in Google Search as a tool for research. Thanks to ADK’s web CLI, you don’t need to worry about managing sessions or web servers; you simply define your agents and the system handles the rest, letting you interact through a convenient browser interface.
Let’s get started by defining the agents and chaining them together in code:
import sys
import base64
import asyncio

# ====== AGENTS CODE (always imported/defined) ======
from google.adk.agents import LlmAgent, SequentialAgent
from google.adk.agents.callback_context import CallbackContext
from google.genai.types import Part, UserContent
from google.adk.tools import google_search
from google.adk.runners import InMemoryRunner

import os
os.environ["GOOGLE_CLOUD_PROJECT"] = "your_project"
os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True"


research_agent = LlmAgent(
name="Researcher",
model="gemini-2.5-flash",
instruction="""
If 'greeted' is not set in state, greet the user with "👋 Hello! I'm your assistant. Hope all is well.".
Then, conduct thorough web research on the subject given in the input.
Gather key facts, recent information, and relevant details about the topic.
Summarize your research findings in a comprehensive but organized manner.
This research will be used by other agents to write content, so be thorough and factual.
""",
output_key="research",
tools=[google_search],
)

generator_agent = LlmAgent(
name="DraftWriter",
model="gemini-2.5-flash",
instruction="""
Using the research findings stored in state['research'], write a short, factual paragraph about the subject.
Base your writing on the research provided - don't make up facts.
Be concise and clear. Your output should be plain text only.
""",
output_key="draft_text",
)

reviewer_agent = LlmAgent(
name="Critic",
model="gemini-2.5-flash",
instruction="""
Analyze the paragraph stored in state['draft_text'].
Also reference the research in state['research'] to check for factual accuracy.
Always give a detailed critique, even if the text is factually correct, comment on style, clarity, possible improvements, or anything that could make it better. If there are factual problems, mention those. Output a short critique, not just 'valid'.
""",
output_key="critique",
)

revision_agent = LlmAgent(
name="Rewriter",
model="gemini-2.5-flash",
instruction="""
Your goal is to revise the paragraph in state['draft_text'] based on the feedback in state['critique'].
You can also reference the research in state['research'] to ensure accuracy and completeness.
Output only the improved paragraph, rewritten as needed.
""",
output_key="revised_text",
)

def greet_on_first_message(callback_context: CallbackContext):
if not callback_context.state.get("greeted"):
callback_context.state["greeted"] = True
return None

root_agent = SequentialAgent(
name="ResearchWriteCritiqueRewrite",
before_agent_callback=greet_on_first_message,
sub_agents=[
research_agent,
generator_agent,
reviewer_agent,
revision_agent
]
)

# =========== ONLY DO THIS FOR CLI MODE ===========
if 'web' not in sys.argv:
# --- OTEL Weave tracing ---
WANDB_BASE_URL = "https://trace.wandb.ai"
PROJECT_ID = "byyoung3/adk_demov2"
WANDB_API_KEY = "your_api_key"
if not WANDB_API_KEY or not PROJECT_ID:
raise ValueError("Set WANDB_API_KEY and PROJECT_ID as variables!")

from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk import trace as trace_sdk
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry import trace

OTEL_EXPORTER_OTLP_ENDPOINT = f"{WANDB_BASE_URL}/otel/v1/traces"
AUTH = base64.b64encode(f"api:{WANDB_API_KEY}".encode()).decode()
OTEL_EXPORTER_OTLP_HEADERS = {
"Authorization": f"Basic {AUTH}",
"project_id": PROJECT_ID,
}
exporter = OTLPSpanExporter(
endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
headers=OTEL_EXPORTER_OTLP_HEADERS,
)
tracer_provider = trace_sdk.TracerProvider()
tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(tracer_provider)

runner = InMemoryRunner(agent=root_agent)

async def create_session():
return await runner.session_service.create_session(
app_name=runner.app_name,
user_id="terminal_user"
)
session = asyncio.run(create_session())

print("==== Research, Write, Critique, Rewrite Terminal ====")
print("Type a subject or question to research (or 'quit' to exit):")
while True:
try:
user_input = input('\nEnter subject: ').strip()
except EOFError:
break
if user_input.lower() == 'quit':
print("Goodbye!")
break

content = UserContent(parts=[Part(text=user_input)])

try:
for event in runner.run(
user_id=session.user_id,
session_id=session.id,
new_message=content
):
if not event or not getattr(event, "content", None):
print("> [ERROR: No event or no content in event!]")
continue
for part in getattr(event.content, "parts", []) or []:
if getattr(part, "text", None):
print('> ', part.text)
except Exception as e:
print(f"ERROR: {e}")

print("Session ended.")

Gemini 2.5 Pro is engineered for deep, complex reasoning and understanding nuanced instructions, making it the go-to choice for tasks requiring intricate analysis and creative generation. Gemini 2.5 Flash, on the other hand, is optimized for incredible speed and efficiency, delivering rapid responses for high-frequency, on-demand tasks. For this demonstration, we chose Gemini 2.5 Flash because it offers a more generous rate limit within the Google Cloud free tier. This allows for more extensive experimentation and development without immediately incurring costs, making it an excellent and accessible starting point for exploring the powerful capabilities of the latest Gemini family of models
💡
Let’s take a closer look at what this multi-agent pipeline is actually doing behind the scenes. When a user submits any topic or question, whether through the web interface or the terminal,$ the request is handled by the root_agent, which orchestrates the workflow in a strictly defined sequence. The very first interaction triggers a friendly greeting thanks to the greet_on_first_message callback, which is a callback that is run before every message sent by the agent and is utilized only during the first message sent.
The pipeline’s flow begins with the Researcher agent. Acting as an LLM-powered research assistant, this agent leverages integrated web tools to gather current information on the provided topic, distilling a concise, organized summary into the shared session state. Following this, the DraftWriter agent takes these findings and composes an accurate, fact-based paragraph, serving as an initial draft grounded in real research rather than fabricated content.
To ensure quality and accuracy, the Critic agent then reviews this draft for style, clarity, and alignment with the research. It generates constructive feedback, which is essential for the pipeline’s robust writing process. Finally, the Rewriter agent uses this critique (along with the original research) to revise and improve the paragraph, producing a final version that incorporates both factual corrections and stylistic enhancements.
A notable technical detail is the use of the output_key parameter with each agent. This ensures that every agent writes its output to a specific key in the shared session state (such as research, draft_text, or critique). This structure enables subsequent agents to easily retrieve results from earlier steps, maintains an explicit information flow, and simplifies debugging and extensibility.
Behind the scenes, the pipeline leverages the Weave OTEL (OpenTelemetry) integration with Google ADK. This setup provides end-to-end observability: as each agent executes its task, detailed traces, including tool calls, LLM invocations, and intermediate state, are logged automatically to Weights & Biases via Weave. This means that every step of every session can be visualized, debugged, and analyzed in the W&B dashboard, helping you trace agent decisions and interactions throughout the workflow.
You can deploy and run the agent in two ways:
  • Web interface: Move into the directory of the agent (the level of the agent/ folder), and enter the adk web command to launch a browser-based chat experience, making it accessible to end-users through an interactive UI.
  • Terminal interface: Simply run python agent.py in the command line. This will activate Weave/OTEL logging, display a friendly prompt, and let you converse with the agent directly in your terminal, with all agent logic and traces automatically sent to your W&B dashboard for exploration.
By chaining specialized agents and capturing their every move with Weave OTEL tracing, you get a modular, transparent, and highly debuggable pipeline that mirrors collaborative human workflows, making ongoing development and troubleshooting dramatically easier.
The Weave screenshot below shows how our multi-agent writing pipeline is visualized within Weave. The pipeline consists of four roles: Researcher, Draft Writer, Critic, and Rewriter. On the left, you can see the invocation tree, each agent_run represents one role, and each call_llm shows the model interaction for that role.
On the right, Weave displays the full prompt and response for the Researcher agent. This includes tool metadata, user instructions, and the final output generated by Gemini 2.5 Flash. The output includes key facts and structured content about the requested topic, demonstrating how the model synthesizes factual summaries for the other agents to build on.


Building an AI insurance agent assistant

Filing an insurance claim can be a confusing and stressful process for customers, especially in the aftermath of an unexpected incident. Claimants are often required to gather a variety of documents, complete detailed forms, and upload photos or other supporting evidence, all while ensuring that nothing important is overlooked. However, it’s common for customers to be unsure about which details or files are required, or how to capture evidence that meets insurance standards. Forms might be incomplete, documents may be missing, and photos sometimes fail to show the full context or clarity needed.
As a result, insurance companies often need to contact customers for additional information, corrections, or higher-quality images. This back-and-forth not only delays the resolution of claims but also increases frustration for everyone involved. Even with online portals, the process can feel opaque and overwhelming, leading to avoidable errors and longer wait times before claims are approved.
To address these challenges and make the process easier and more transparent, we’ll build an AI-powered assistant designed specifically to help customers check their submissions before sending anything to an insurance agent. With this assistant, customers can upload their claim PDFs and any incident photos, and the system will automatically review what’s provided, flagging missing details, suggesting clarifications, and verifying whether the evidence meets standard requirements.
All this feedback is combined into a clear summary, helping customers correct mistakes and fill in gaps before final submission. By making it easier to provide complete and accurate information the first time, the assistant speeds up the claim process and reduces unnecessary stress for both customers and agents alike.
The code below demonstrates how such an assistant can be created, by combining a sequence of specialized AI agents focused on document analysis, photo verification, and helpful report generation, all working together to support customers in preparing strong, thorough insurance claims.
import os
import shutil
import uuid
import asyncio
import PyPDF2
import base64
from flask import Flask, request, render_template_string, jsonify
import asyncio
import os
import base64

from google.adk.agents import Agent, LlmAgent, SequentialAgent
from google.adk.runners import InMemoryRunner
from google.genai import types
from litellm import completion
import base64
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk import trace as trace_sdk
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry import trace

from google import genai
from google.genai import types
from PIL import Image
from io import BytesIO



import os
# switch to Gemini
os.environ["GOOGLE_CLOUD_PROJECT"] = "your_project"
os.environ["GOOGLE_CLOUD_LOCATION"] = "us-central1"
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True"


def init_weave_otel_adk(
wandb_api_key: str,
wandb_project_id: str,
base_url: str = "https://trace.wandb.ai"
):
"""
Initialize OpenTelemetry tracing for Weave + Google ADK.

Args:
wandb_api_key (str): Your W&B API key.
wandb_project_id (str): Your W&B entity/project (e.g. "myteam/myproject").
base_url (str): The Weave trace API base URL (default is "https://trace.wandb.ai").
"""
otel_endpoint = f"{base_url}/otel/v1/traces"
auth = base64.b64encode(f"api:{wandb_api_key}".encode()).decode()
headers = {
"Authorization": f"Basic {auth}",
"project_id": wandb_project_id,
}
exporter = OTLPSpanExporter(
endpoint=otel_endpoint,
headers=headers,
)
tracer_provider = trace_sdk.TracerProvider()
tracer_provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(tracer_provider)

PROJECT_ID = "byyoung3/adk_demo" # e.g. youruser/yourproject
WANDB_API_KEY = "your_api_key" # from https://wandb.ai/authorize

init_weave_otel_adk(wandb_api_key=WANDB_API_KEY, wandb_project_id=PROJECT_ID)
import weave; weave.init("adk_demov4")


# ==== Setup directories (cleared every run) ====
PDF_DIR = "pdfs"
IMG_DIR = "imgs"

for dir_path in [PDF_DIR, IMG_DIR]:
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
os.makedirs(dir_path, exist_ok=True)



@weave.op
async def analyze_pdf_claim_tool(tool_context) -> dict:
print("RUNNING PDF", flush=True)
pdfs = [os.path.join(PDF_DIR, f) for f in os.listdir(PDF_DIR) if f.lower().endswith('.pdf')]
if not pdfs:
return {"status": "no_pdf", "message": "No PDF uploaded."}
latest = max(pdfs, key=os.path.getmtime)

try:
with open(latest, "rb") as f:
reader = PyPDF2.PdfReader(f)
all_text = "\n".join(page.extract_text() or '' for page in reader.pages)
print("all Text: " + all_text, flush=True)
if not all_text.strip():
return {"status": "error", "message": "No readable text in PDF."}
except Exception as e:
return {"status": "error", "message": f"Failed to read PDF: {e}"}

prompt = (
"You are an insurance expert. Review the following insurance claim document and list several questions "
"an insurance company should ask the claimant to clarify missing, ambiguous, incomplete, or suspicious information.\n\n"
"Claim Document:\n"
"-----------------------\n"
f"{all_text[:8000]}"
"\n-----------------------\n"
"Questions to ask the claimant:"
)

try:
client = genai.Client()
resp = await asyncio.to_thread(
client.models.generate_content,
model="gemini-2.5-flash",
contents=[prompt]
)
ans = getattr(resp, "text", None) or "No response."
print(ans, flush=True)
return {
"status": "success",
"claim_filename": os.path.basename(latest),
"questions": ans,
}
except Exception as e:
return {"status": "error", "message": f"Error from Gemini: {e}"}



@weave.op
async def analyze_one_image(pil_image, fname, criteria, client):
try:
buf = BytesIO()
pil_image.save(buf, format="PNG")
img_bytes = buf.getvalue()
except Exception as e:
return {
"filename": fname,
"error": f"Failed to process image: {e}",
}

parts = [
types.Part.from_bytes(data=img_bytes, mime_type="image/png"),
criteria
]
try:
resp = await asyncio.to_thread(
client.models.generate_content,
model="gemini-2.5-flash",
contents=parts
)
ans = getattr(resp, "text", None) or "No response."
return {
"filename": fname,
"analysis": ans,
}
except Exception as e:
return {
"filename": fname,
"error": f"Error analyzing image: {str(e)}",

}
@weave.op
async def analyze_imgs_tool(tool_context) -> dict:
current_call = weave.get_current_call()
call_id = getattr(current_call, 'id', None)

print("RUNNING IMG (parallel)", flush=True)
criteria = (
"As an insurance claims photo analyst, verify if this photo meets ALL of the following:\n"
"1. The image is sharp and clear (no blur, good lighting).\n"
"2. There are NO photo filters applied, and the image is not cropped to hide context.\n"
"3. The photo shows surroundings to prove location (for example, the full car, not just a dent).\n"
"For each point, state YES or NO and why.\n"
"Then, give a one-line verdict: Would this photo be acceptable for an insurance claim? (Yes/No and reason)."
)

# --- Collect all PIL images and associated filenames
files = sorted([
f for f in os.listdir(IMG_DIR)
if f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp'))
])
if not files:
return {"status": "no_images", "message": "No images available to analyze."}

pil_images = []
for fname in files:
fpath = os.path.join(IMG_DIR, fname)
try:
pil_img = Image.open(fpath).convert("RGB")
pil_images.append((fname, pil_img))
except Exception as e:
print(f"Failed to load image {fname}: {e}", flush=True)

client = genai.Client()

# --- Now analyze_one_image takes (PIL image, filename)


# Gather all results concurrently, passing (PIL image, filename)
results = await asyncio.gather(
*(analyze_one_image(pil_img, fname, criteria=criteria, client=client) for fname, pil_img in pil_images)
)

print(results, flush=True)
return {"status": "success", "analyses": results, 'call_id': call_id}

# EXAMPLE: Flask wrapper (for HTTP error codes) for PDF endpoint
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/analyze_pdf_claim', methods=['POST'])
async def analyze_pdf_claim():
result = await analyze_pdf_claim_tool({})
# HTTP STATUS CODES
if result["status"] == "success":
return jsonify(result), 200
elif result["status"] == "no_pdf":
return jsonify(result), 400
else:
return jsonify(result), 500

@app.route('/analyze_images', methods=['POST'])
async def analyze_images():
result = await analyze_imgs_tool({})
if result["status"] == "success":
return jsonify(result), 200
elif result["status"] == "no_images":
return jsonify(result), 400
else:
return jsonify(result), 500


# ==== Individual Agents ====
pdf_agent = Agent(
model="gemini-2.5-flash",
name="pdf_claim_analyzer",
instruction="use the analyze_pdf_claim_tool and generate follow-up questions for unclear or missing information. Just call the tool to start. It will handle the analysis",
description="Analyzes PDF insurance claim documents.",
tools=[analyze_pdf_claim_tool],
output_key="pdf_feedback"
)

photo_agent = Agent(
model="gemini-2.5-flash",
name="photo_claim_analyzer",
instruction="1. Read the photos using the analyze_imgs_tool. Next, Analyze insurance claim photos for quality and compliance with evidence standards.",
description="Analyzes insurance claim photos for acceptability.",
tools=[analyze_imgs_tool],
output_key="photo_feedback"
)

# --- Revision Agent ---
summarizer_agent = LlmAgent(
name="Rewriter",
model="gemini-2.5-flash",
instruction="""
Your goal is to combine the reports from state['photo_feedback'] and state['pdf_feedback'].
Create a comprehensive insurance claim analysis report that includes:
1. PDF document analysis and recommended questions
2. Photo quality assessment and acceptability
3. Overall claim assessment and recommendations

Output only the improved combined report, rewritten as needed.
""",
output_key="data_summary"
)

coordinator_agent = SequentialAgent(
name="insurance_claim_pipeline",
sub_agents=[pdf_agent, photo_agent, summarizer_agent]
)

# Create runner and session
runner = InMemoryRunner(agent=coordinator_agent)

# Create session synchronously at startup
async def create_session():
return await runner.session_service.create_session(
app_name=runner.app_name, user_id="insurance_user"
)

session = asyncio.run(create_session())

# ==== FLASK UI ====
app = Flask(__name__)

COMBINED_HTML = """
<!DOCTYPE html>
<html>
<head>
<title>Insurance Claim Analyzer</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family:sans-serif; background:#f3f5f9;}
.main { max-width:650px; margin:40px auto; background:white; border-radius:8px; box-shadow:0 3px 12px #ddd; padding: 30px; }
.upload-section { margin-bottom: 30px; padding: 20px; background:#f8f9fa; border-radius: 6px; }
input[type=file] { margin-bottom:18px; width: 100%; }
.result { background:#f3f7fa;margin:20px 0;padding:18px; border-radius:7px; font-size:1.05em;}
.err { color:red;}
.imglist img { max-width:80px; margin:4px; border-radius:6px;}
.analysis-section { margin:15px 0; padding:15px; border-left: 3px solid #4a9fd6; background:#fafbfc; }
button { padding:10px 22px; margin:5px; border:none; background:#4a9fd6; color:white; border-radius:6px; cursor:pointer;}
button:disabled { background:#ccc; cursor:not-allowed; }
.file-info { font-size:0.9em; color:#666; margin:5px 0; }
</style>
</head>
<body>
<div class="main">
<h2>🔍 Insurance Claim Analyzer</h2>
<p>Upload PDF claim documents and/or photos. The AI will analyze both types of evidence and provide a comprehensive assessment.</p>
<div class="upload-section">
<h3>📄 PDF Claims</h3>
<form id="pdfForm" enctype="multipart/form-data" onsubmit="return uploadPdf();">
<input id="pdfFile" type="file" accept="application/pdf">
<button id="pdfBtn" type="submit">Upload PDF</button>
</form>
<div id="pdfStatus" class="file-info"></div>
</div>

<div class="upload-section">
<h3>📸 Claim Photos</h3>
<form id="photoForm" enctype="multipart/form-data" onsubmit="return uploadPhotos();">
<input id="photoFiles" type="file" accept="image/*" multiple>
<button id="photoBtn" type="submit">Upload Photos</button>
</form>
<div id="preview" class="imglist"></div>
<div id="photoStatus" class="file-info"></div>
</div>

<div style="text-align:center; margin:20px 0;">
<button id="analyzeBtn" onclick="analyzeAll()" style="background:#28a745; font-size:1.1em;">
🔍 Analyze All Uploaded Files
</button>
</div>

<div id="results"></div>
</div>

<script>
// PDF Upload
function uploadPdf(){
let f = document.getElementById('pdfFile');
let btn = document.getElementById('pdfBtn');
let status = document.getElementById('pdfStatus');
if(!f.files.length) return false;
btn.disabled = true;
status.innerHTML = "<i>Uploading PDF...</i>";
let data = new FormData();
data.append('claimpdf', f.files[0]);
fetch('/upload_pdf', { method:'POST', body:data })
.then(r=>r.json())
.then(obj=>{
btn.disabled = false;
if(obj.success){
status.innerHTML = "✓ PDF uploaded successfully";
status.style.color = 'green';
}else{
status.innerHTML = "✗ " + (obj.error||"Upload failed");
status.style.color = 'red';
}
}).catch(e=>{
btn.disabled = false;
status.innerHTML = "✗ Error: "+e;
status.style.color = 'red';
});
return false;
}

// Photo Upload
document.getElementById('photoFiles').onchange = function(ev){
let preview = document.getElementById('preview');
preview.innerHTML = '';
for(let f of ev.target.files){
let img = document.createElement('img');
img.src = URL.createObjectURL(f);
preview.appendChild(img);
}
};

function uploadPhotos(){
let f = document.getElementById('photoFiles');
let btn = document.getElementById('photoBtn');
let status = document.getElementById('photoStatus');
if(f.files.length==0) return false;
btn.disabled = true;
status.innerHTML = "<i>Uploading photos...</i>";
let data = new FormData();
for(let i=0;i<f.files.length;i++) data.append('photos', f.files[i]);
fetch('/upload_photos', { method:'POST', body:data })
.then(r=>r.json())
.then(obj=>{
btn.disabled = false;
if(obj.success){
status.innerHTML = `✓ ${obj.count} photos uploaded successfully`;
status.style.color = 'green';
}else{
status.innerHTML = "✗ " + (obj.error||"Upload failed");
status.style.color = 'red';
}
}).catch(e=>{
btn.disabled = false;
status.innerHTML = "✗ Error: "+e;
status.style.color = 'red';
});
return false;
}

// Analysis
function analyzeAll(){
let btn = document.getElementById('analyzeBtn');
let results = document.getElementById('results');
btn.disabled = true;
results.innerHTML = "<div style='text-align:center; padding:20px;'><i>🔄 Analyzing claim data... (this may take up to 30 seconds)</i></div>";
fetch('/analyze')
.then(r=>r.json())
.then(obj=>{
btn.disabled = false;
if(obj.error){
results.innerHTML = `<div class='err'>Error: ${obj.error}</div>`;
} else {
results.innerHTML = `<div class='analysis-section'><h3>📋 Complete Analysis</h3><div>${obj.analysis}</div></div>`;
}
}).catch(e=>{
btn.disabled = false;
results.innerHTML = `<div class='err'>Error: ${e}</div>`;
});
}
</script>
</body>
</html>
"""

@app.route('/')
def main_page():
return render_template_string(COMBINED_HTML)

@app.route('/upload_pdf', methods=['POST'])
def upload_pdf():
f = request.files.get('claimpdf')
if not f or not f.filename.lower().endswith('.pdf'):
return jsonify({"error":"No PDF file uploaded."})
pdfbytes = f.read()
if len(pdfbytes) > 10*1024*1024:
return jsonify({"error":"PDF too large (10MB max)."})
fname = uuid.uuid4().hex + ".pdf"
with open(os.path.join(PDF_DIR, fname), 'wb') as out:
out.write(pdfbytes)
return jsonify({"success": True})

@app.route('/upload_photos', methods=['POST'])
def upload_photos():
files = request.files.getlist('photos')
if not files:
return jsonify({"error":"No files uploaded."})
count = 0
for f in files:
ext = os.path.splitext(f.filename)[-1].lower()
if ext not in ('.jpg','.jpeg','.png','.gif','.webp'):
continue
imgbytes = f.read()
if len(imgbytes) > 10*1024*1024:
continue
fname = uuid.uuid4().hex + ext
with open(os.path.join(IMG_DIR, fname), 'wb') as out:
out.write(imgbytes)
count += 1
if count==0:
return jsonify({"error":"No valid image files found."})
return jsonify({"success": True, "count": count})



@app.route('/analyze', methods=['GET'])
def analyze_all():
global session
try:
from google.genai.types import Part, UserContent
content = UserContent(parts=[Part(text="Analyze all available insurance claim documents and photos")])
# Collect all text output from sequential agent
all_text = ""
for event in runner.run(
user_id=session.user_id,
session_id=session.id,
new_message=content
):
for part in event.content.parts:
if part.text: # Check if text is not None
all_text += part.text + "\n"
if not all_text.strip():
return jsonify({"error": "No analysis results were generated. Please ensure files are uploaded."})
return jsonify({"analysis": all_text.strip()})
except Exception as e:
return jsonify({"error": f"Server error: {str(e)}"})

if __name__ == "__main__":
app.run(debug=True, port=5000)
After uploading their supporting documents or photos through the simple web interface, customers can use the “Analyze All Uploaded Files” feature to receive immediate, clear, and tailored feedback on their claim submission. Under the hood, this is where the orchestration of specialized AI agents begins.
First, the assistant checks for a submitted PDF (typically the formal claim statement). The PDF is read, and its contents are analyzed using a large language model that simulates the reasoning of an insurance expert. It looks for missing, vague, or ambiguous sections and generates a list of follow-up questions that a real claims agent would likely ask, guiding the customer to spot and address any critical gaps.
Each image is assessed to determine whether it’s sharp, clear, unedited, and provides enough situational context (not just a close-up of a dent, but also a sense of the surroundings). The AI provides a breakdown for each image, explaining whether it meets the necessary standards for processing a claim, and why or why not.

A third, summary agent then brings these two streams of feedback together, combining the document analysis and the photo review into a single, readable report. This comprehensive summary lets the customer quickly see where their evidence is solid and where improvements are needed before submitting it to the insurance company.
By checking for issues upfront, such as missing documentation, poor image quality, or unclear narrative, the system helps users save time and avoid frustrating delays that would otherwise result from insurance agents needing to request additional information. It guides customers to provide all the required information on their first try, making the insurance process easier, clearer, and more efficient for everyone involved.
Because the workflow is modular (using clearly defined agents for each step), it’s also easy to expand. If new requirements or claim types arise in the future, additional specialized agents can be plugged in, keeping the system up-to-date without major changes to the rest of the pipeline. This modular design ensures a smooth customer experience and supports the evolving needs of both insurance providers and their clients.

Utilizing Weave Tracing with Google ADK

In this project, the primary method we use to capture detailed traces of our agent workflows is by enabling OpenTelemetry (OTEL) integration with Weave. Setting up OTEL using the init_weave_otel_adk function allows all agent calls, tool invocations, model completions, and their corresponding inputs and outputs to be automatically sent to your Weave dashboard. This integration leverages the structured traces and spans that Google ADK emits for every pipeline run, providing comprehensive visibility into how every claim is processed at each step.
The Weave interface screenshot below shows how every step in the pipeline is recorded. On the left, the full invocation tree of agent calls is shown, each node represents a sub-agent or tool. You can view the agent structure (claim analyzer, PDF tool, photo analyzer, and final report writer) and track the call flow end-to-end. On the right, Weave displays the full request and model response payload for each call, which helps debug specific input/output decisions made by the agent.

To complement OTEL’s tracing, we also explicitly call weave.init() and use the @weave.op decorator on custom tools in our code. This approach ensures that any Python function or tool marked with the @weave.op is included in the visual trace, allowing us to clearly log and inspect granular steps such as analyzing PDFs or processing images. By returning information such as the full PDF text or a call ID for image analysis from these functions, we make it easy to review these artifacts and results directly in the Weave UI.
Since we logged the call ID inside our image analyzer tool, we can easily search for the trace and visualize the performance of our image analyzer. Here’s the call for my demo:

This sort of logging may seem simple, but its utility is extremely valuable. In a production setting, being able to monitor and visualize the performance of your models is absolutely essential, and robust observability becomes the backbone of scalable, reliable systems. This is especially true for image analysis pipelines, where troubleshooting can be challenging due to the complexity of the data and the potential for subtle edge cases.

Conclusion

The combination of Google’s Agent Development Kit (ADK) and the Weave + OpenTelemetry integration offers a powerful foundation for building, deploying, and maintaining practical AI-powered agent pipelines. By embracing modular agent composition, robust session and state management, and flexible tool and artifact integration, ADK enables developers to solve real-world problems, from natural language research and critique to sophisticated document and image analysis, using tools and abstractions that scale.
Production-focused observability, made possible through Weave’s deep tracing and W&B dashboards, elevates your workflows from experimental code to reliable, operational systems. With every agent decision, tool call, and artifact automatically logged and visualized, you cannot only quickly pinpoint bottlenecks or errors but also gain invaluable insight into model and pipeline performance over time. This level of transparency is especially crucial in domains such as insurance claims processing, where both precision and trustworthiness are paramount.
Whether you’re building multi-step language pipelines, customer-facing assistants, or advanced image verification tools, the ADK’s agentic architecture, enhanced with the traceability and monitoring of Weave, empowers rapid iteration, reliable debugging, and confident scaling. By following these practices, you ensure your AI solutions remain not just intelligent and helpful, but also maintainable, auditable, and ready for the demands of modern production environments.




Iterate on AI agents and models faster. Try Weights & Biases today.