> ## Documentation Index
> Fetch the complete documentation index at: https://hub.hcompany.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> H Platform has four products: the Agents API (managed computer-use agents, base URL https://agp.eu.hcompany.ai/api/v2 or https://agp.hcompany.ai/api/v2 for the US), the Models API (OpenAI-compatible Holo vision-language models at https://api.hcompany.ai/v1), HoloDesktop CLI (Holo on the user's own desktop), and HoloTab (a free no-code Chrome extension that runs Holo in the user's browser, with recordable routines and schedules).
> Authenticate with a bearer API key from the HAI_API_KEY environment variable. SDKs: `pip install hai-agents` (Python, `from hai_agents import Client`) and `npm install hai-agents` (TypeScript, `import { HaiAgentsClient } from "hai-agents"`). CLI: `hai`.
> Agents do work in a browser or on a desktop; describe the task as an imperative instruction. To run a task quickly, prefer the pre-built agent `h/web-surfer-flash`. Read results from the session's `latest_answer` after it reaches a terminal status.
> Sessions are the unit of work; wait for a terminal status (completed, failed, timed_out, interrupted) before reading the answer. Use webhooks or the `changes` long-poll endpoint to follow progress.

# Browser agent

> A complete Holo browser agent in one Python file: Playwright takes screenshots, Holo returns clicks and keystrokes, the loop runs until the answer tool is called.

Playwright drives headless Chromium, each screenshot goes to Holo, and Holo returns the next click, keystroke, scroll, or URL. It applies [Core concepts](/models-api/build-an-agent/core-concepts) in the [function calling](/models-api/build-an-agent/function-calling) format. The other cookbooks build on this file.

<Steps titleSize="h3">
  <Step title="Install">
    ```bash theme={"system"}
    pip install openai playwright
    playwright install chromium
    export HAI_API_KEY="your-api-key-here"
    ```
  </Step>

  <Step title="Save the agent">
    Save the [image budget helper](/models-api/build-an-agent/core-concepts#image-budget) as `trim.py`, then save this next to it:

    ```python browser_agent.py expandable theme={"system"}
    import base64
    import json
    import os

    from openai import OpenAI
    from playwright.sync_api import sync_playwright

    from trim import trim_to_last_n_images

    client = OpenAI(base_url="https://api.hcompany.ai/v1/", api_key=os.environ["HAI_API_KEY"])
    MODEL = "holo4-35b-a3b"
    WIDTH, HEIGHT = 1280, 800


    def fn(name: str, description: str, **properties) -> dict:
        return {
            "type": "function",
            "function": {
                "name": name,
                "description": description,
                "parameters": {"type": "object", "properties": properties, "required": list(properties)},
            },
        }


    INT = {"type": "integer", "description": "Coordinate as integer in [0, 1000]"}
    tools = [
        fn("click", "Click at (x, y) coordinates", element={"type": "string", "description": "Detailed description of the target UI element"}, x=INT, y=INT),
        fn("type", "Type text into the focused element, optionally pressing Enter", text={"type": "string"}, press_enter={"type": "boolean"}),
        fn("scroll", "Scroll the page", direction={"type": "string", "enum": ["up", "down"]}),
        fn("goto", "Navigate to a URL", url={"type": "string"}),
        fn("answer", "Provide a final answer", content={"type": "string", "description": "The answer content"}),
    ]


    def execute(page, name: str, args: dict) -> str:
        if name == "click":
            page.mouse.click(args["x"] / 1000 * WIDTH, args["y"] / 1000 * HEIGHT)
        elif name == "type":
            page.keyboard.type(args["text"])
            if args.get("press_enter"):
                page.keyboard.press("Enter")
        elif name == "scroll":
            page.mouse.wheel(0, HEIGHT * 0.8 * (1 if args["direction"] == "down" else -1))
        elif name == "goto":
            page.goto(args["url"])
        page.wait_for_timeout(1000)
        page.wait_for_load_state()
        return f"Done. Current URL: {page.url}"


    def observation(page) -> dict:
        b64 = base64.b64encode(page.screenshot()).decode()
        return {"role": "user", "content": [
            {"type": "text", "text": "<observation>\n"},
            {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}},
            {"type": "text", "text": "\n</observation>"},
        ]}


    def run(task: str, start_url: str, max_steps: int = 20) -> str:
        with sync_playwright() as p:
            page = p.chromium.launch().new_page(viewport={"width": WIDTH, "height": HEIGHT})
            page.goto(start_url)
            messages = [
                {"role": "system", "content": "You are a web agent. You see the browser through screenshots and act with your tools."},
                {"role": "user", "content": task},
            ]
            for _ in range(max_steps):
                messages.append(observation(page))
                trim_to_last_n_images(messages)
                resp = client.chat.completions.create(
                    model=MODEL,
                    messages=messages,
                    tools=tools,
                    tool_choice="required",
                    temperature=0.8,
                    extra_body={"chat_template_kwargs": {"enable_thinking": True}},
                )
                msg = resp.choices[0].message
                messages.append({"role": "assistant", "content": msg.content, "tool_calls": msg.tool_calls})
                if not msg.tool_calls:
                    continue
                call = msg.tool_calls[0]
                args = json.loads(call.function.arguments)
                if call.function.name == "answer":
                    return args["content"]
                print(f"--- {call.function.name}({args})")
                messages.append({"role": "tool", "tool_call_id": call.id, "content": execute(page, call.function.name, args)})
        return "Step budget exhausted"


    if __name__ == "__main__":
        print(run("Search Wikipedia for Ada Lovelace and tell me her date of birth.", "https://en.wikipedia.org"))
    ```
  </Step>

  <Step title="Run it">
    ```bash theme={"system"}
    python browser_agent.py
    ```

    ```text Output theme={"system"}
    --- click({'element': 'Search Wikipedia input field at the top of the page', 'x': 364, 'y': 43})
    --- type({'text': 'Ada Lovelace', 'press_enter': True})
    Ada Lovelace was born on December 10, 1815.
    ```
  </Step>
</Steps>

## How it works

* **Screen size.** The viewport and screenshots are fixed at 1280 by 800, so scaling `[0, 1000]` back is one multiplication.
* **Settling.** After each action the loop waits a second, then for the page to load. Otherwise a screenshot can catch a stale page mid-navigation.
* **Memory.** Only the last 3 screenshots stay in context. Older ones become a text placeholder.
* **Stopping.** The run ends only when Holo calls `answer`. A step without a tool call is skipped.

For a desktop or Android device, swap Playwright for your OS or emulator driver. `observation()` and `execute()` change, but the loop does not.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.