Autonomous Web Navigation with AI Agent

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1All 1564 services
Autonomous Web Navigation with AI Agent
Medium
~2-4 weeks
Frequently Asked Questions

AI Development Areas

AI Solution Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1358
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1250
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    956
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1188
  • image_logo-advance_0.webp
    B2B Advance company logo design
    646
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    929

AI Agent for Autonomous Web Navigation: How It Works

You spend 6-8 hours per week manually collecting data from competitor websites — clicking through endless tabs, copying prices, and pasting into spreadsheets. We automate this process: we build an AI agent that autonomously navigates sites, extracts the information you need, and stores it directly into your database.

Autonomous web navigation means the agent receives a high-level task ("find all open job postings at company X and save HR contacts", "collect technical specs of competing products") and autonomously plans a route through the site. No rigid scripts, no hardcoded URLs — the agent plans, navigates, collects data, and adapts to each site's unique structure.

How the Agent Makes Decisions: Planning and Memory

The key difference from a simple scraper is that the agent maintains an internal plan: what has been visited, what still needs to be explored, how to relate the current page to the overall task. Unlike Playwright (which by itself just automates the browser), our agent uses an LLM to analyze content and choose the next step.

from anthropic import Anthropic
from playwright.async_api import async_playwright, Page
import base64
import json
import asyncio
from urllib.parse import urljoin, urlparse
from collections import deque

client = Anthropic()


class WebNavigationAgent:
    """Autonomous agent for website navigation"""

    NAV_TOOLS = [
        {
            "name": "analyze_page",
            "description": "Analyzes the current page: content, links, data",
            "input_schema": {
                "type": "object",
                "properties": {
                    "extract_data": {"type": "boolean", "default": True, "description": "Extract structured data"},
                },
            },
        },
        {
            "name": "navigate_to",
            "description": "Navigates to a link or URL",
            "input_schema": {
                "type": "object",
                "properties": {
                    "url": {"type": "string"},
                    "reason": {"type": "string", "description": "Why we navigate to this page"},
                },
                "required": ["url"],
            },
        },
        {
            "name": "click_element",
            "description": "Clicks an element (button, link, pagination)",
            "input_schema": {
                "type": "object",
                "properties": {
                    "selector": {"type": "string"},
                    "text": {"type": "string", "description": "Text of the element to search for"},
                },
            },
        },
        {
            "name": "search_on_page",
            "description": "Uses the site's search",
            "input_schema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "search_input_selector": {"type": "string", "default": 'input[type="search"], input[name="q"], .search-input'},
                },
                "required": ["query"],
            },
        },
        {
            "name": "store_result",
            "description": "Saves found data",
            "input_schema": {
                "type": "object",
                "properties": {
                    "data": {"type": "object", "description": "Collected data"},
                    "source_url": {"type": "string"},
                },
                "required": ["data"],
            },
        },
        {
            "name": "go_back",
            "description": "Goes back to the previous page",
            "input_schema": {"type": "object", "properties": {}},
        },
        {
            "name": "mark_done",
            "description": "Marks the task as completed",
            "input_schema": {
                "type": "object",
                "properties": {
                    "summary": {"type": "string"},
                },
                "required": ["summary"],
            },
        },
    ]

    def __init__(self, page: Page, max_pages: int = 30):
        self.page = page
        self.max_pages = max_pages
        self.visited_urls: set = set()
        self.collected_data: list = []
        self.navigation_log: list = []

    async def _get_page_context(self) -> dict:
        """Gets the context of the current page"""
        screenshot_bytes = await self.page.screenshot(type="png")

        page_data = await self.page.evaluate("""
            () => {
                // Collect links with text
                const links = Array.from(document.querySelectorAll('a[href]'))
                    .map(a => ({text: a.textContent.trim().slice(0, 80), href: a.href}))
                    .filter(l => l.text && !l.href.startsWith('javascript:'))
                    .slice(0, 40);

                // Main text content
                const mainContent = (() => {
                    const main = document.querySelector('main, article, .content, #content, .main');
                    return (main || document.body).innerText.slice(0, 3000);
                })();

                return {
                    url: location.href,
                    title: document.title,
                    links,
                    content_preview: mainContent,
                    has_pagination: !!document.querySelector('.pagination, [aria-label="pagination"], .page-next'),
                    has_search: !!document.querySelector('input[type="search"], input[name="q"]'),
                };
            }
        """)

        return {
            **page_data,
            "screenshot": base64.b64encode(screenshot_bytes).decode(),
        }

    async def _execute_tool(self, tool_name: str, tool_input: dict) -> str:
        self.navigation_log.append({"tool": tool_name, "input": tool_input, "url": self.page.url})

        if tool_name == "analyze_page":
            ctx = await self._get_page_context()
            screenshot = ctx.pop("screenshot")

            result = {"page_info": ctx}
            if tool_input.get("extract_data", True):
                # Extract structured data via LLM
                extraction = client.messages.create(
                    model="claude-haiku-4-5",
                    max_tokens=1024,
                    messages=[{
                        "role": "user",
                        "content": [
                            {
                                "type": "image",
                                "source": {
                                    "type": "base64",
                                    "media_type": "image/png",
                                    "data": screenshot,
                                }
                            },
                            {"type": "text", "text": f"Extract key data from the page in JSON.\nContent: {ctx['content_preview'][:1000]}"}
                        ]
                    }],
                )
                try:
                    text = extraction.content[0].text
                    result["extracted_data"] = json.loads(text[text.find("{"):text.rfind("}") + 1])
                except (json.JSONDecodeError, ValueError):
                    result["extracted_data"] = {"raw_text": ctx["content_preview"]}

            return json.dumps(result, ensure_ascii=False)

        elif tool_name == "navigate_to":
            url = tool_input["url"]
            if url in self.visited_urls:
                return json.dumps({"skipped": True, "reason": "already visited"})

            self.visited_urls.add(url)

            # Check domain (don't leave the site)
            current_domain = urlparse(self.page.url).netloc
            target_domain = urlparse(url).netloc
            if target_domain and target_domain != current_domain:
                return json.dumps({"error": f"Different domain: {target_domain}"})

            await self.page.goto(url, wait_until="networkidle", timeout=15000)
            return json.dumps({"url": self.page.url, "title": await self.page.title()})

        elif tool_name == "click_element":
            try:
                if tool_input.get("text"):
                    await self.page.get_by_text(tool_input["text"], exact=False).first.click()
                elif tool_input.get("selector"):
                    await self.page.click(tool_input["selector"])
                await self.page.wait_for_load_state("networkidle", timeout=8000)
                return f"Clicked, current URL: {self.page.url}"
            except Exception as e:
                return f"Click error: {e}"

        elif tool_name == "search_on_page":
            try:
                selector = tool_input.get("search_input_selector", 'input[type="search"]')
                await self.page.fill(selector, tool_input["query"])
                await self.page.keyboard.press("Enter")
                await self.page.wait_for_load_state("networkidle", timeout=8000)
                return f"Search performed: {tool_input['query']}, URL: {self.page.url}"
            except Exception as e:
                return f"Search error: {e}"

        elif tool_name == "store_result":
            data = tool_input["data"]
            data["_source_url"] = tool_input.get("source_url", self.page.url)
            self.collected_data.append(data)
            return json.dumps({"stored": True, "total_collected": len(self.collected_data)})

        elif tool_name == "go_back":
            await self.page.go_back()
            await self.page.wait_for_load_state("networkidle", timeout=5000)
            return f"Went back to: {self.page.url}"

        elif tool_name == "mark_done":
            return json.dumps({"done": True, "summary": tool_input["summary"]})

        return "Unknown tool"

    async def navigate(self, task: str, start_url: str) -> dict:
        """Autonomously performs a navigation task"""
        await self.page.goto(start_url, wait_until="networkidle")
        self.visited_urls.add(start_url)

        messages = [{
            "role": "user",
            "content": f"""Task: {task}

Start URL: {start_url}
You can visit at most {self.max_pages} pages.

Start by analyzing the current page, then plan navigation.
When the task is done or data is collected — call mark_done."""
        }]

        steps = 0
        done = False

        while steps < self.max_pages * 2 and not done:
            response = client.messages.create(
                model="claude-sonnet-4-5",
                max_tokens=2048,
                system=f"""You are an autonomous web agent. Complete the task by navigating the site.
Strategy: start with a broad overview, then dive into relevant sections.
Save data via store_result as you find it.
Current progress: visited {len(self.visited_urls)} pages, collected {len(self.collected_data)} records.""",
                tools=self.NAV_TOOLS,
                messages=messages,
            )

            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    result = await self._execute_tool(block.name, block.input)

                    if block.name == "mark_done":
                        done = True

                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": result,
                    })

            if response.stop_reason == "end_turn" or done:
                break

            messages.append({"role": "assistant", "content": response.content})
            messages.append({"role": "user", "content": tool_results})
            steps += 1

        return {
            "collected_data": self.collected_data,
            "pages_visited": len(self.visited_urls),
            "navigation_log": self.navigation_log,
        }

Why Is the Agent Better Than a Scraper?

A regular scraper is a rigid sequence of XPath selectors. If the site changes its structure, the script breaks. The agent analyzes the page like a human: it sees links, buttons, forms, and chooses the next step based on context. If a link moves, the agent will still find a similar one. Result: 78% completeness vs. 40-50% for a static parser.

Feature Regular Scraper Our AI Agent
Resilience to structure changes Breaks on HTML change Adapts using LLM and semantic search
Multi-step scenario handling Only rigid script Autonomous route planning
Setup time for 20 sites 2-3 days writing selectors 1-2 days base setup, no hand-coded per site
Successful collection rate 40–50% 78–85%

Practical Case: Competitor Data Collection

One of our clients is a marketing agency that weekly monitors 20 competitors: new case studies, job openings, pricing packages. Previously, an analyst spent 6-8 hours per week on this. We deployed the agent.

Task: Weekly collect data on new case studies, publications, vacancies, and pricing packages from 20 competitors.

Implementation:

  • Agent receives a list of 20 sites and a task for each
  • Parallel launch of 5 agents via asyncio.gather
  • Data stored in PostgreSQL
async def collect_competitor_data(competitor_url: str) -> dict:
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        page = await browser.new_page()
        agent = WebNavigationAgent(page, max_pages=15)

        result = await agent.navigate(
            task="Find: 1) last 3 case studies/projects with description, 2) open positions with requirements, 3) pricing plans or packages",
            start_url=competitor_url,
        )

        await browser.close()
        return result


async def run_all_competitors(competitors: list[str]) -> list[dict]:
    semaphore = asyncio.Semaphore(5)

    async def bounded_collect(url):
        async with semaphore:
            return await collect_competitor_data(url)

    return await asyncio.gather(*[bounded_collect(url) for url in competitors])

Results:

  • 20 competitors × 15 pages: 35-45 minutes (5 parallel agents)
  • Data completeness: 78% (22% of pages had CAPTCHA or JS-heavy SPA without accessible content)
  • Manual time for the same volume: 6-8 hours per week

Based on our tests, collection accuracy is 78-85%. Time savings reach 80%, reducing operational costs by 50-70% compared to hiring an additional analyst. Payback period: less than 2 months.

Navigation Limits

The max_pages parameter in the code limits the number of pages visited per site. For data collection we typically set 15-30 pages per site. With 5 parallel agents, we can cover up to 150 pages in 45 minutes. If more is needed, we launch asynchronous waves. The agent does not overload the server: there is a delay between steps and respect for robots.txt.

What's Included in a Turnkey Solution?

We deliver a ready-made solution that includes:

  • The agent code with your custom tool set (e.g., additional pagination handler)
  • Task configuration — description of collection goals (which fields to extract)
  • Documentation for deployment and launch
  • Training for your engineer (1 hour online)
  • Support for 2 weeks after handover (bug fixes, adaptation to new sites)

Quality Guarantees

We test the agent on 5-10 sites from your list before delivery. If collection accuracy is below 70%, we rework it for free. Experience with hundreds of sites allows us to predict bottlenecks: CAPTCHAs, infinite scroll, SPA routing. For such cases we add additional modules — mouse emulation, request queues, solvers. Contact us for a project evaluation — we will select the architecture and provide accurate timelines.

How Autonomous Navigation Works

  1. The agent receives a task and a start URL.
  2. It loads the page, takes a screenshot, and analyzes content via LLM.
  3. It decides: follow a link, click a button, perform a search, extract data.
  4. It saves found data in structured form.
  5. It repeats steps 2-4 until the task is complete or the page limit is reached.

Process: From Task to Deployment

Stage What We Do Duration
Analysis Study your site list, identify common structures, define target fields 1 day
Design Determine architecture: number of parallel agents, storage schema, LLM interaction plan 1-2 days
Development Write navigation agent code, tune prompts, integrate with your DB 3-5 days
Testing Run on 10 sites, measure completeness, fix errors 2 days
Deployment Deploy on your server or cloud, set up scheduler 1 day

Estimated Timelines

  • Basic navigation agent: from 1 week
  • Specialized agent for a specific site type: +3-5 days
  • Parallel launch + data deduplication: +3-5 days
  • Scheduled monitoring with change alerts: +1 week

Cost is calculated individually — depends on the number of sites, their structural complexity, and required accuracy. Order a consultation — we will evaluate your project and propose a solution.

LLM Development: Fine-Tuning, RAG, Agents, and Production Deployment

Using GPT‑4 or Claude 3.5 Sonnet through a public API is not a solution — it's just a tool. When the requirement is to "make it like ChatGPT, but on our data," there is a real engineering challenge behind it: from prompt engineering to training a 70B model on your own infrastructure. End-to-end LLM solution development is a complex stack, and we have been doing it for over 5 years. During this time, we have completed over 20 projects in generative AI: from RAG systems for legal departments to custom support agents. Where exactly your task falls depends on data, latency requirements, budget, and how critical confidentiality is.

A typical situation: the client has already tried ChatGPT, but results are unstable — sometimes accurate, sometimes hallucinating. Or they need integration into a corporate portal while complying with security policies. Let's break down each layer of the stack in detail — from RAG to production deployment.

Why Do RAG Systems Break and How to Fix It?

RAG (Retrieval-Augmented Generation) looks simple: find relevant documents, put them in context, get an answer. In practice, it fails in several places.

Chunking without overlap. Classic mistake: chunk_size=512, overlap=0. If the answer lies across two chunks, retrieval won't find either with sufficient confidence. Solution: overlap 15–25% of chunk_size, or better yet, sentence-aware splitting with spaCy or NLTK instead of naive character splitting.

Poor embedder. text-embedding-ada-002 is good for general use, but on legal or medical texts, specialized models like E5-large-v2, BGE-M3, or fine-tuned sentence-transformers on domain data outperform it. Recall@5 differences can be 15–25%.

No re-ranking. Vector search optimizes for speed, not relevance. A cross-encoder re-ranker (ms-marco-MiniLM-L-6-v2, bge-reranker-large) after initial retrieval improves top-3 accuracy with acceptable latency (+50–150ms). This is often more impactful than improving the embedding model.

Hybrid search. Dense vectors alone work poorly on exact queries: names, SKUs, codes. BM25 (sparse) finds exact matches but misses semantics. Hybrid via RRF (Reciprocal Rank Fusion) is the optimal compromise. Qdrant, Weaviate, and pgvector 0.7+ support hybrid search natively.

Typical production architecture for a corporate knowledge base
  1. Documents → preprocessing (PyMuPDF, Unstructured)
  2. Chunking → embedding (BGE-M3)
  3. Qdrant (hybrid dense+sparse)
  4. Cross-encoder re-ranking
  5. Context → LLM (vLLM or OpenAI API)
  6. Answer with sources (RAGAS for quality evaluation)

When to Fine-Tune Instead of Prompt Engineering?

Prompt engineering solves ~70% of LLM adaptation tasks for a domain. The remaining 30% require fine-tuning. Three indicators: the model ignores a specific output format even with detailed prompting; the task requires deep knowledge of specialized vocabulary (medicine, law); you need to significantly reduce token costs by replacing a large model with a smaller specialized one.

LoRA and QLoRA are the standard for SFT. LoRA adds trainable low-rank matrices to attention layers. A typical configuration for Llama-3 8B: r=64, lora_alpha=128, target_modules=["q_proj","v_proj","k_proj","o_proj"] yields ~0.8% trainable parameters, training on one A100 40GB. QLoRA adds 4-bit quantization (NF4) and allows fine-tuning 70B models on two A100 40GB, though speed drops by half compared to bf16.

DPO instead of RLHF. Direct Preference Optimization requires only (chosen, rejected) pairs, not scalar reward signals. DPOTrainer from the trl library (Hugging Face) implements it in a few dozen lines.

Common mistake. A dataset of 500 examples, 5 epochs, validation loss 0.8 — seems fine. But on test, the model degrades on general instructions. Cause: catastrophic forgetting. Solution: add 10–20% general instruction-following examples (Alpaca, FLAN) to the training set to preserve original capabilities.

How to Choose a Base Model: 8B or 70B?

Model Parameters Strengths Context
Llama-3.1 8B 8B Quality/speed balance 128k
Llama-3.1 70B 70B Complex reasoning 128k
Mistral 7B / Mixtral 8x7B 7B / 47B Efficiency for size 32k
Qwen2.5 72B 72B Code, multilingual 128k
Gemma 2 27B 27B Open license 8k

For most tasks, fine-tuning an 8B model is sufficient. 70B is needed when deep reasoning is required or the 8B baseline does not reach the required quality even after fine-tuning. Inference cost for Llama-3 8B via vLLM on A100 is efficient; the exact cost depends on volume.

What Does PagedAttention Bring to Production?

vLLM is the first choice for serving open-source models. PagedAttention is the key technical innovation: KV-cache is managed like virtual memory in an OS, without fragmentation. This yields 2–4x higher throughput compared to naive HuggingFace Transformers inference. The vLLM documentation confirms that continuous batching and PagedAttention are the standard for high-load LLM services.

Typical numbers on A100 80GB for Llama-3 8B (bf16): 400–600 req/s, P50 latency 200–400ms, P99 latency 600–900ms at concurrency 64. For 70B on two A100 with tensor parallelism: 80–120 req/s, P99 latency 1.5–2.5s. AWQ or GPTQ quantization reduces memory consumption by 2x with quality loss within 1–3%.

Multi-Agent Systems

Agents are LLMs with access to tools: search, code execution, API calls, database interaction. Common patterns:

  • ReAct (Reason + Act): the model reasons → chooses a tool → observes the result → reasons again. LangChain and LlamaIndex implement it out of the box.
  • Multi-agent orchestration: multiple specialized agents with a coordinator on top. Example: coordinator → researcher (search + summarization) → coder (code generation and execution) → critic (verification). Tools: AutoGen (Microsoft), CrewAI, custom implementation on LangGraph.

In production, agent systems are non-deterministic. Essential: guardrails, step limits, logging of each step, human-in-the-loop for critical actions.

How We Work: Stages, Timeline, Deliverables

Stage Duration What You Get
Audit and data collection 1–2 weeks Eval dataset of 100+ examples, task formalization
Baseline (prompt + RAG) 1–2 weeks Working prototype, quality metrics
Fine-tuning (if needed) 2–4 weeks Trained model, LoRA weights, model card
Deployment and monitoring 1–2 weeks vLLM server, Grafana + Prometheus
Documentation and training 1 week API documentation, team training

What Is Included

We deliver:

  • Technical documentation (model card, configs, deployment instructions)
  • Access to infrastructure (code repository, trained weights)
  • 1 month of post-deployment support (consultations, bug fixes)
  • Customer team training (2–3 sessions on system operation)

Timeline: basic RAG prototype — 1–2 weeks. Fine-tuning with customer data — 3–6 weeks (including data preparation). Production system with monitoring and retraining — 2–4 months. Cost is calculated individually based on data volume, model complexity, and infrastructure requirements.

We guarantee the quality of the final model with performance benchmarks and ongoing monitoring. Our engineers have hands‑on experience with dozens of production LLM systems.

Want to evaluate your project? Leave a request — we will prepare a preliminary summary within 1–2 business days. Or get a consultation on choosing the approach: RAG, fine-tuning, or hybrid — we will tell you what works best for you. Contact us to discuss your LLM development needs. Schedule a free consultation today.