Skip to content

amyaby/first-agent

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

14 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ADK + LiteLLM — Complete Guide

A practical guide to building AI agents with Google ADK, LiteLLM, and Gemini via AI Studio. Written while learning — covers setup, tools, workflows, memory, and control patterns.


Table of Contents

  1. LiteLLM vs OpenRouter
  2. Concepts
  3. Prerequisites
  4. Setup
  5. Writing Your Agent
  6. Running Your Agent
  7. Support Agent — Real Project
  8. Workflow Agents
  9. Session — Remembering the Conversation
  10. Callbacks
  11. Artifacts
  12. Events
  13. Summary
  14. Further Reading

1. LiteLLM vs OpenRouter

alt text


alt text

LiteLLM is a local Python library. It runs in your code, on your machine. You bring your own API keys for each provider, and LiteLLM just translates the call format so you don't have to rewrite your code per provider. Nothing goes through a third-party server — it's purely a code abstraction layer.

OpenRouter is a cloud API gateway. Instead of managing 5 different API keys for 5 different providers, you get one OpenRouter API key and one endpoint (https://openrouter.ai/api/v1). OpenRouter handles the routing to the actual provider on their servers. You pay OpenRouter, and they pay the providers.

alt text


2. Concepts

What is ADK?

Agent Development Kit (ADK) is Google's open-source framework for building, testing, and deploying AI agents. It provides high-level abstractions for agent orchestration, tool use, memory, and multi-agent workflows — letting you focus on logic rather than infrastructure.

What is LiteLLM?

LiteLLM is a lightweight Python library that provides a unified interface for calling different LLM providers (OpenAI, Anthropic, Google, etc.) using the same code. ADK uses LiteLLM internally to support models beyond native Gemini.

Without LiteLLM, every time you want to switch models, you'd have to rewrite your API call from scratch — because each provider has its own SDK, its own request format, and its own response structure. With LiteLLM, you write the call once and just swap the model string. One interface. Any model. No rewrite.

Key concept: The model string prefix tells LiteLLM which provider to route to.

  • gemini/gemini-2.5-flash → Google AI Studio
  • anthropic/claude-sonnet-4-6 → Anthropic
  • openai/gpt-4o → OpenAI

3. Prerequisites


4. Setup

1. Create a virtual environment

python -m venv venv
source venv/bin/activate       # macOS/Linux
# venv\Scripts\activate        # Windows

2. Install dependencies

pip install google-adk litellm

3. Create your agent project

adk create my_agent

This generates the following structure:

my_agent/
├── agent.py       # Main agent definition
├── .env           # API keys (never commit this)
└── __init__.py

Important: Add .env and venv/ to your .gitignore:

echo ".env" >> .gitignore
echo "venv/" >> .gitignore
echo "__pycache__/" >> .gitignore

4. Configure your API key

In my_agent/.env:

GEMINI_API_KEY=your_api_key_here

5. Writing Your Agent

Step 1 — Basic agent (Q&A)

A simple agent that answers user questions. No tools, no special logic — just a model and an instruction.

Edit my_agent/agent.py:

from google.adk.agents.llm_agent import Agent
from google.adk.models.lite_llm import LiteLlm

root_agent = Agent(
    name="root_agent",  # do not change this name
    model=LiteLlm(model="gemini/gemini-2.5-flash"),
    description="A helpful assistant for user questions.",
    instruction="Answer user questions to the best of your knowledge.",
)

Common mistake: Using aistudio/gemini-2.5-flash will throw a BadRequestError. The correct LiteLLM prefix for Google AI Studio is gemini/.

GUI output

alt text alt text

CLI output

alt text


Step 2 — Agent with a tool (current time)

Now we upgrade the agent by giving it a tool — a Python function the agent can call when it needs to. In this case, a get_current_time function that returns the current time for a given city.

from google.adk.agents.llm_agent import Agent
from google.adk.models.lite_llm import LiteLlm

def get_current_time(city: str) -> dict:
    """Returns the current time in a specified city."""
    return {"status": "success", "city": city, "time": "10:30 AM"}

root_agent = Agent(
    name="root_agent",  # do not change this name
    model=LiteLlm(model="gemini/gemini-2.5-flash"),
    description="Tells the current time in a specified city.",
    instruction="You are a helpful assistant that tells the current time in cities. Use the 'get_current_time' tool for this purpose.",
    tools=[get_current_time],
)

What changed from Step 1:

  • A get_current_time function is defined and passed to tools=[]
  • The description and instruction are updated to reflect the agent's new purpose
  • The agent now knows when to call the tool based on the user's question

Note: The tool here uses a hardcoded time ("10:30 AM"). In a real app you would replace the function body with actual logic using Python's datetime module or a time zone API.

To test the Step 2 agent separately, create a new project folder:

adk create my_agent_v2

Then paste the Step 2 code into my_agent_v2/agent.py and run:

adk run my_agent_v2

6. Running Your Agent

CLI (terminal chat)

adk run my_agent

alt text

Web UI

adk web --port 8000

alt text

Then open http://localhost:8000 in your browser.


7. Support Agent — Real Project

A Moroccan technical support agent named Karim that speaks Darija/Arabizi, authenticates users via national ID, and searches a knowledge base to solve technical issues.

The agent is instructed to always search it first using search_knowledge_base before answering.

alt text alt text

After running:

adk web --port 8000

alt text alt text


8. Workflow Agents

Beyond a single agent, ADK supports Workflow Agents — a special layer that controls how multiple agents run together. Think of it as a coordinator that decides whether its team works in a line, all at once, or keeps retrying until the job is done.

There are 3 modes:


Sequential

Agents run one after the other, in order. The output of the first becomes the input of the next.

Agent A → Agent B → Agent C

Example: Validate user → Search knowledge base → Format response.

alt text


Parallel

Agents run at the same time, simultaneously. Use this when tasks are independent from each other and you want speed.

Agent A ↘
Agent B  → results combined
Agent C ↗

Example: Search 3 different databases at the same time, then merge results.

alt text


Loop

An agent keeps repeating until a condition is met — like a while loop but for agents.

Agent A → check condition → not done → run again
                          → done     → stop

Example: Keep asking the user for their national ID until it's valid.


How they fit together

Your App
   └── Workflow Agent (the coordinator)
         ├── Sequential → for ordered steps
         ├── Parallel   → for independent tasks
         └── Loop       → for retry / repeat logic
               └── sub-agents doing the actual work

9. Session — Remembering the Conversation

By default, an agent has no memory. Every message you send is treated as if it's the first one. Sessions solve this — they give the agent a memory of the current conversation so it can remember what was said earlier.

Think of a session like a notebook the agent keeps open during your conversation. Everything you say gets written in it, and the agent can refer back to it at any time.

How it works

Every conversation gets a unique session ID. ADK stores the full message history under that ID, and passes it to the agent on every new message so it always has context.

User sends message
       ↓
ADK loads session (conversation history)
       ↓
Agent reads history + new message
       ↓
Agent replies
       ↓
ADK saves updated history to session

The 3 types of SessionService

ADK gives you 3 options for where to store the session data, depending on your needs:

InMemorySessionService

Stores the conversation in RAM. Fast and simple — no setup needed. But the history is lost as soon as the program stops.

from google.adk.sessions import InMemorySessionService

session_service = InMemorySessionService()

Use when: local development, testing, or short-lived demos.

DatabaseSessionService

Stores the conversation in a persistent database. History survives restarts — users can come back later and continue where they left off.

from google.adk.sessions import DatabaseSessionService

session_service = DatabaseSessionService(db_url="sqlite:///sessions.db")

Use when: production apps where users need persistent history.

VertexAiSessionService

Stores the conversation in Google Cloud (Vertex AI). Fully managed, scalable, no database to maintain yourself.

from google.adk.sessions import VertexAiSessionService

session_service = VertexAiSessionService(
    project="your-gcp-project",
    location="us-central1"
)

Use when: deploying on Google Cloud at scale.

Quick comparison

InMemory Database VertexAI
Setup None Minimal Google Cloud account
Persists after restart
Good for Testing Production Cloud deployments
Cost Free Free (local) Paid (GCP)

Session State — The Agent's Scratchpad

Within each session, session.state is the agent's dedicated scratchpad for that specific conversation. While the session holds the full message history, session.state is where the agent stores and updates dynamic details during the conversation.

Think of it this way:

  • session.events = the full transcript of everything said
  • session.state = a sticky note the agent keeps updating as the conversation evolves

What is session.state used for?

It's a dictionary of key-value pairs designed for information the agent needs to recall or track mid-conversation:

{"user_preference_theme": "dark"}        # personalize interaction
{"booking_step": "confirm_payment"}      # track task progress
{"shopping_cart_items": ["book", "pen"]} # accumulate information
{"user_is_authenticated": True}          # make informed decisions

How agents read and write state

Agents write to state automatically via output_key:

agent = LlmAgent(
    name="auth_agent",
    instruction="Validate the user and confirm authentication.",
    output_key="user_is_authenticated"
)

Agents read from state via {} placeholders in their instruction:

agent = LlmAgent(
    name="support_agent",
    instruction="""
    The user authentication status is: {user_is_authenticated}
    If authenticated, help them. Otherwise, ask them to log in first.
    """
)

You can also set state manually before the conversation starts:

session = session_service.create_session(
    app_name="support_app",
    user_id="user_123",
    state={
        "user_name": "Youssef",
        "language": "Darija",
        "user_is_authenticated": False
    }
)

Session vs Session State — full picture

session.events session.state
What it is Full conversation transcript Key-value scratchpad
What it stores All messages back and forth Dynamic data updated during conversation
Who writes to it ADK automatically You or agents via output_key
How agents read it Automatically (always has context) Via {} placeholders in instructions
Value types Messages Strings, numbers, booleans, lists, dicts
Example "User said X, agent replied Y" {"user_is_authenticated": True}

10. Callbacks — Observe, Customize, and Control Agent Behavior

Imagine you could tap into your agent's shoulder at any moment during its work and say "before you do that, let me check something" or "after you did that, let me log it". That's exactly what callbacks are.

A callback is a Python function that ADK calls automatically at specific moments during the agent's lifecycle. You don't call them yourself — you register them and ADK triggers them at the right time.

Why use callbacks?

Without callbacks your agent is a black box. With callbacks you can:

  • Observe — log every tool call, every model request, every agent response
  • Customize — modify the input before it reaches the model
  • Control — block a tool call, cancel a model request, or short-circuit the agent entirely

The 6 callback moments

User sends message
       ↓
[ before_agent ]         ← agent is about to start
       ↓
[ before_model ]         ← model is about to be called
       ↓
[ after_model ]          ← model just responded
       ↓
[ before_tool ]          ← tool is about to run
       ↓
[ after_tool ]           ← tool just finished
       ↓
[ after_agent ]          ← agent just finished

How to write a callback

from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse

def before_model_callback(callback_context: CallbackContext, llm_request: LlmRequest):
    print(f"About to call model with: {llm_request}")
    return None  # None = proceed normally

def after_model_callback(callback_context: CallbackContext, llm_response: LlmResponse):
    print(f"Model responded: {llm_response}")
    return None

def before_tool_callback(tool, args, tool_context):
    print(f"Tool '{tool.name}' called with args: {args}")
    return None

def after_tool_callback(tool, args, tool_context, result):
    print(f"Tool '{tool.name}' returned: {result}")
    return None

How to attach callbacks to your agent

root_agent = Agent(
    name="root_agent",
    model=LiteLlm(model="gemini/gemini-2.0-flash"),
    instruction="You are a helpful assistant.",
    before_model_callback=before_model_callback,
    after_model_callback=after_model_callback,
    before_tool_callback=before_tool_callback,
    after_tool_callback=after_tool_callback,
)

Real use cases

# 1. Log every model call
def before_model_callback(callback_context, llm_request):
    print(f"[LOG] Sending to model: {llm_request.messages[-1]}")
    return None

# 2. Block unsafe tool calls
def before_tool_callback(tool, args, tool_context):
    if tool.name == "delete_database":
        return {"error": "This action is not allowed."}
    return None

# 3. Inject data before model call
def before_model_callback(callback_context, llm_request):
    llm_request.system_instruction += "\nAlways respond in Darija."
    return None

# 4. Track token usage
def after_model_callback(callback_context, llm_response):
    tokens = llm_response.usage_metadata.total_token_count
    print(f"[TOKENS USED] {tokens}")
    return None

The return value is the control mechanism

Callback Return None Return a value
before_model_callback Model call proceeds Skips model, uses your value
after_model_callback Uses model response Replaces model response
before_tool_callback Tool runs normally Skips tool, uses your value
after_tool_callback Uses tool result Replaces tool result

Callback summary

Callback When it fires Common use
before_agent Agent about to start Setup, auth checks
after_agent Agent finished Cleanup, final logging
before_model Just before LLM call Modify prompt, log request
after_model Just after LLM response Log response, modify output
before_tool Just before tool runs Validate args, block calls
after_tool Just after tool finishes Log result, modify output

11. Artifacts — Storing Files and Large Data

While session state is great for storing small pieces of data like names or results, Artifacts are designed for larger content — files, images, audio, PDFs, or any binary data that doesn't belong in a text-based conversation.

Think of artifacts as a file system attached to the session. An agent can save a file to it, and another agent (or the user) can retrieve it later.

# save an artifact
await tool_context.save_artifact(
    filename="report.pdf",
    artifact=types.Part.from_bytes(data=pdf_bytes, mime_type="application/pdf")
)

# load an artifact
artifact = await tool_context.load_artifact(filename="report.pdf")

Use when: your agent generates or processes files — reports, images, audio transcriptions, exported data, etc.


12. Events — Everything That Happens Inside the Agent

An Event is ADK's way of representing every single thing that happens during an agent's run — a message received, a tool called, a model response, an error. Everything is an event.

Think of events as a live stream of the agent's internal activity. ADK emits them one by one as the agent works, and you can listen to them to know exactly what's happening at any moment.

User sends "search for EVs"
       ↓
Event: user message received
       ↓
Event: model called
       ↓
Event: tool "google_search" invoked
       ↓
Event: tool result returned
       ↓
Event: final response generated

You already use events every time you run an agent:

async for event in runner.run_async(user_id="user", session_id="123", new_message=msg):
    if event.is_final_response():
        print(event.content)

Use when: you want to stream responses, build a custom UI, log every step, or react to specific moments in the agent's execution.


13. Summary

Component Role
ADK Agent framework — orchestration, tools, memory
LiteLLM Universal LLM router — translates calls to any provider
gemini/ prefix Tells LiteLLM to route to Google AI Studio
GEMINI_API_KEY Auth credential read by LiteLLM at runtime
Tools Python functions the agent can call
Workflow Agents Coordinate multiple agents (sequential, parallel, loop)
Session Stores conversation history
Session State Shared key-value scratchpad between agents
Callbacks Hook into agent lifecycle to observe and control
Artifacts Store files and binary data attached to a session
Events Live stream of everything happening inside the agent

14. Further Reading

About

Discovering ADK (Agent Development Kit)

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors