unlob Docs
Browse documentation

Google ADK

Plain Python functions become tools, and the dev UI shows you every call.

ADK builds a tool schema from a function’s signature and docstring, so a normal typed function is already a tool. Nothing below is Google-specific on our side — unlob is called over plain HTTPS, so the same functions work unchanged if you move off ADK later.

pip install google-adk httpx

The tools

# tools.py
import os

import httpx

API = "https://api.unlob.com"
HEADERS = {"x-api-key": os.environ["UNLOB_API_KEY"]}


def web_search(query: str, min_sources: int = 2) -> dict:
    """Search the web on unlob's own index.

    Returns passages — the snippet IS the text, so most questions need no
    follow-up fetch.

    Args:
        query: What to search for. Supports AND, OR, -exclude, "exact phrase".
        min_sources: Independent sources required. Use 3 for factual claims.

    Returns:
        A dict with a "results" list and an "incomplete" flag.
    """
    r = httpx.get(f"{API}/search", headers=HEADERS, timeout=15, params={
        "q": query,
        "min_independent_sources": min_sources,
        # One row per near-duplicate cluster, so the agent does not read the
        # same syndicated article five times.
        "collapse": "story",
        "limit": 8,
    })
    r.raise_for_status()
    body = r.json()
    return {
        # A 200 that means the answer is incomplete: part of the corpus was
        # unreachable. Left unsaid, a short list gets reported as "nothing found".
        "incomplete": bool(body.get("partial")),
        "results": body["results"],
    }


def assemble_context(question: str, budget: int = 3000) -> dict:
    """Build a corroborated, deduplicated context pack packed to a token budget.

    Prefer this over several searches when you need to READ source material
    rather than just find it.

    Args:
        question: The question the pack should answer.
        budget: Maximum tokens of context to return.

    Returns:
        A dict with "estimated_tokens" and an "items" list, each carrying the
        reason it was included.
    """
    r = httpx.get(f"{API}/assemble_context", headers=HEADERS, timeout=30, params={
        # The parameter is `q`, not `query` — this endpoint and /search differ.
        "q": question,
        "budget": budget,
    })
    r.raise_for_status()
    return r.json()

The Google-style docstring with the Args: block is the schema. That is not a style preference here: omit it and the parameters reach the model undescribed.

The agent

# agent.py
from google.adk.agents import Agent

from tools import assemble_context, web_search

root_agent = Agent(
    name="researcher",
    model="gemini-2.5-pro",
    description="Researches questions against the open web with citations.",
    instruction=(
        "Search before you answer. Prefer claims carried by several independent "
        "sources, and say so when a claim rests on one. If a result comes back "
        "with incomplete set, say the answer may be partial. Always cite URLs."
    ),
    tools=[web_search, assemble_context],
)

Run it, and watch what it did

# Interactive, with the trace UI at http://localhost:8000
adk web

# Or headless, for a one-shot run
adk run . --input "What changed in the EU AI Act GPAI rules in 2026?"

The dev UI is the strongest argument for ADK over a bare SDK: it shows every tool call, its arguments and its response beside the conversation. For working out why an agent chose one tool over another, that beats reading logs.

Four things that will cost you time

  • The variable must be named root_agent. Both adk web and adk run report finding no agents otherwise, with no hint as to why.
  • Return a dict, not a list or a bare string. ADK expects a structured tool response, and a list produces a schema error at call time rather than at definition time.
  • /assemble_context takes q, not query. /search and the graph calls differ here; see the API reference.
  • A 429 without retry-after is a hard-capped monthly quota, not a rate limit. Returning “429” to the model gets you an agent that sensibly retries something that cannot succeed. See Rate limits and quotas.

Using Claude models instead

ADK reaches other providers through LiteLLM, though Gemini is the native and smoothest path. If Claude is a hard requirement, Pydantic AI and LangGraph treat provider choice as first-class.

Next