Agno + Anchor: Production Browser Agents in Python

Hands On
Jun 17
by Idan Raman
Agno + Anchor: Production Browser Agents in Python

Agno (formerly PhiData) has quietly become one of the fastest-growing Python agent frameworks. Its design is refreshingly direct: agents are first-class objects with tools, memory, and structured output — and they compose into multi-agent teams without orchestration ceremony. It crossed 20k GitHub stars without a lot of fanfare, because developers who try it tend to keep using it.

Pair Agno with Anchor's managed cloud browsers — session isolation, residential proxies, CAPTCHA handling — and you get a browser agent stack that's ergonomic to build with and reliable to run in production.

Why Agno?

  • Native multi-agent teams. Agno's Team class coordinates multiple agents on a shared task. No custom orchestrator to write — just declare roles and let the framework handle handoffs.
  • Structured output by default. Pass any Pydantic model as response_model and the agent returns validated Python objects, not strings to parse.
  • Built-in storage. Sessions, run logs, and agent memories persist to SQLite or PostgreSQL with no extra configuration.
  • Model-agnostic. OpenAI, Anthropic, Gemini, and local models all use the same Agent interface — swap in one line.

Setup

pip install agno anchorbrowser playwright
playwright install chromium
export ANCHOR_API_KEY="your_anchor_key"
export OPENAI_API_KEY="your_openai_key"

Defining Browser Tools

Agno tools are plain Python functions decorated with @tool. The framework inspects the docstring and type hints to build the LLM schema automatically:

import os
from anchorbrowser import AnchorClient
from playwright.sync_api import sync_playwright
from agno.tools import tool

anchor = AnchorClient(api_key=os.environ["ANCHOR_API_KEY"])

session = anchor.sessions.create()
pw = sync_playwright().start()
browser = pw.chromium.connect_over_cdp(session.data.cdpUrl)
page = browser.contexts[0].pages[0]

@tool
def navigate(url: str) -> str:
    'Navigate to a URL and return the page title.'
    page.goto(url, wait_until="domcontentloaded")
    return f"At {url!r} — title: {page.title()}"

@tool
def get_text(selector: str = "body") -> str:
    'Extract visible text from a CSS selector on the current page.'
    return page.inner_text(selector)[:5000]

@tool
def click(selector: str) -> str:
    'Click an element matching the CSS selector.'
    page.click(selector)
    page.wait_for_load_state("networkidle", timeout=5000)
    return f"Clicked {selector!r}. Now at: {page.url}"

@tool
def type_text(selector: str, text: str) -> str:
    'Fill an input field matching the CSS selector with text.'
    page.fill(selector, text)
    return f"Typed into {selector!r}"

Your First Agent

from agno.agent import Agent
from agno.models.openai import OpenAIChat

agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[navigate, get_text, click, type_text],
    instructions=[
        "You are a browser automation agent.",
        "Use navigate first, then extract or interact with the page.",
    ],
    show_tool_calls=True,
    markdown=True,
)

agent.print_response(
    "Go to news.ycombinator.com and list the top 5 story titles and scores.",
    stream=True,
)

Structured Output

For production pipelines you want typed data. Define a Pydantic model and pass it as response_model — the agent validates its own output before returning:

from pydantic import BaseModel
from typing import List

class Story(BaseModel):
    title: str
    score: int
    url: str

class HNResult(BaseModel):
    stories: List[Story]

structured_agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[navigate, get_text],
    response_model=HNResult,
)

result: HNResult = structured_agent.run(
    "Go to news.ycombinator.com and extract the top 10 stories."
)

for story in result.stories:
    print(f"[{story.score:>4}] {story.title}")

Multi-Agent Teams

Agno's Team class assigns specialized roles and coordinates handoffs. Here a navigator explores the page while an analyst extracts structured insights:

from agno.team import Team

navigator = Agent(
    name="Navigator",
    model=OpenAIChat(id="gpt-4o-mini"),
    tools=[navigate, click],
    instructions=["Navigate to the requested URL and describe what you see."],
)

analyst = Agent(
    name="Analyst",
    model=OpenAIChat(id="gpt-4o"),
    tools=[get_text],
    instructions=["Read the page content and return structured insights."],
    response_model=HNResult,
)

team = Team(
    members=[navigator, analyst],
    model=OpenAIChat(id="gpt-4o"),
    instructions=["Navigator goes first, then Analyst extracts data."],
    show_tool_calls=True,
)

team.print_response("Analyze the top stories on news.ycombinator.com")

Persistent Storage

Agno persists session history to SQLite with no extra configuration. Pass a fixed session_id and the agent resumes where it left off:

from agno.storage.sqlite import SqliteStorage

storage = SqliteStorage(
    table_name="browser_agent_runs",
    db_file="runs.db",
)

persistent_agent = Agent(
    model=OpenAIChat(id="gpt-4o"),
    tools=[navigate, get_text],
    storage=storage,
    add_history_to_messages=True,
    session_id="hn-monitor",
)

# First run seeds the session
persistent_agent.run("What are the top HN stories today?")

# Second run picks up context from the first
persistent_agent.run("Which of those have the most comments?")

Switching to Claude

The model swap is one line — tools and response models are unchanged:

from agno.models.anthropic import Claude

claude_agent = Agent(
    model=Claude(id="claude-sonnet-4-6"),
    tools=[navigate, get_text, click, type_text],
    instructions=["You are a precise browser automation agent."],
    response_model=HNResult,
)

Production Tips

  • Scope sessions to runs. Create a fresh Anchor session per agent run and close it in a finally block. This avoids session bleed across concurrent jobs.
  • Set max_retries. Pass max_retries=3 to Agent() for automatic retry on tool failures — handles transient page load issues without custom logic.
  • Use debug_mode=True during development. Agno logs every tool call and LLM response. Flip it off in production.
  • Parallel sessions. Agno has a native async API — await agent.arun() — so you can fan out across multiple Anchor sessions with asyncio.gather(). See our parallel agents guide for patterns.

What's Next

Try Anchor free and run your first Agno browser agent in minutes →

Stay ahead in browser automation

We respect your inbox. Privacy policy

Welcome aboard! Thanks for signing up
Oops! Something went wrong while submitting the form.