Hacking out an AI spider with Node
Tags: openai, javascript, gpt3, gpt4
Published: 2024-05-11 09:06:38

## Prologue

`Python` is great for AI software, but I prefer `NodeJS` for crawling with a headless browser, IMO `python` has some good libraries for parsing the content but `puppeter` is slightly more powerful when looking at headless browsers.
That said I wanted to do an experiment and try to pull some data from a page without going through the tedious process of checking every element of interest and discarding the fluff.
The nice thing about `puppeteer` is also that you don't need to reverse engineer the requests going into the page, figure out cookies and whatnot.

## Why?

Web/data scraping is pretty cool, it gives you the power to combine different data sources and devise interesting ways to draw conclusions about it. It is even more interesting these days with ChatGPT, you can simply dump some data and ask it what it can do with it, or if it can extract usable info from it. That is in fact quite nice.

## What?

We're going to be looking at a [listing site](https://www.immobiliare.it/vendita-case/verona/?criterio=rilevanza&prezzoMassimo=120000) for rentals in Italy, picked something at random for no real reason, mostly to see if it works.

Let's plan this out:
- grab the content
- identify the listings wrapper and grab the innerHTML of it
- pass in the HTML to `gpt-4-turbo` via the API and construct a dialogue with it to extract the data we are looking for

## How?

We're going to install a few dependencies:

```bash
# we like speed so while npm is nice, pnpm is faster
npm i -g pnpm
pnpm i openai puppeteer-extra dotenv
```

Then figure out the main listing element, that at the time of writing was something easy like `.in-realEstateResults`.

Some quick [docs fumbling](https://pptr.dev/) and you get a working script for loading the page and grabbing the innerHTML of the listing element like so:

```js
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setUserAgent(
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
  );
  const element = await page.$(".in-realEstateResults");
  const innerHtml = await page.evaluate(
    (element) => element.innerHTML,
      element,
  );
```

The `openai` library api is quite straightforward and they offer [quite a few examples](https://platform.openai.com/docs/quickstart?context=node) too. The library provides some responses and also some metrics about the usage you had for your specific query.

```js
  const response = await openai.chat.completions.create({
    model: "gpt-4-turbo",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: `Can you extract the property prices out of this
            html and send me the results? The text is in italian so
            you should translate that. Can you send me the output as JSON?`,
          },
          {
            type: "text",
            text: propertyInfoHtml,
          },
        ],
      },
    ],
  });
  console.log(response.choices[0]);
  console.log(response.usage);
```

Another extremely cool thing is that you can simply tell the model to extract data as `json` and what you will have done is convert `html` to a `json` api. Basically you have a magicbox that you can tell what you want to extract from some  structure. You simply have a piece of flexible code that can extract data from various `html` pages with different selectors, so no more hassle figuring that stuff out. And the output is quite well structured and would require minimal after-processing.

```bash
[\n' +
      '    {\n' +
      '        "price": "€ 115.000",\n' +
      '        "description": "Bilocale via Felice Casorati 33, Borgo Venezia, Verona",\n' +
      '        "rooms": "2 locali",\n' +
      '        "size": "60 m²",\n' +
      '        "bathroom": "1 bagno",\n' +
      '        "floor": "Piano 1",\n' +
      '        "elevator": "Ascensore",\n' +
      '        "balcony": "Balcone"\n' +
      '    },\n' +
      '    {\n' +
      '        "price": "€ 120.000",\n' +
      '        "description": "Trilocale via Arnaldo Da Brescia, 27, Porto San Pancrazio, Verona",\n' +
      '        "rooms": "3 locali",\n' +
      '        "size": "77 m²",\n' +
      '        "bathroom": "1 bagno",\n' +
      '        "floor": "Piano R",\n' +
      '        "elevator": "No Ascensore",\n' +
      '        "cellar": "Cantina"\n' +
      '    },\n
...
```

## How much though?

This processing though can quickly get out of hand even with the low pricing, so you want to cache results. You also need to pay attention to the context window of the model you are using. The last line with `response.usage` tells you how many tokens you have used in the query.

```bash
 { prompt_tokens: 30337, completion_tokens: 972, total_tokens: 31309 }
```
This works no problem with `gpt-4`, but smaller models that you can host locally for example might have an issue with this. There are pages out there that can be quite large also.

## Trimming the fat

If you think about it a big part of `html` is non structure defininig. What that means is that it is either display information(css) or interaction capabilities(events and such). The interesting part is that it does not have any true value for the data which is what we are after.
What if we removed all of that? Crazy idea right?

```bash
pnpm i jsdom
```

So let's add an intermediate step before sending the `html` out to OpenAI:

```js
    const dom = new JSDOM(innerHtml);
    spinner.text = "Cleaning up HTML";
    const document = dom.window.document;
    const elements = document.querySelectorAll("*");
    elements.forEach((element) => {
      Array.from(element.attributes).forEach((attribute) => {
        element.removeAttribute(attribute.name);
      });
    });
    const cleanedHtml = dom.serialize();
```
And presto, the metrics of the trimmed down version:

```bash
{ prompt_tokens: 7790, completion_tokens: 586, total_tokens: 8376 }
```

Pretty handy...less than one third.

## Conclusions
- the `gpt-4-turbo` apis are definitely worth exploring, 15 minutes exploration is not doing it justice
- you can tweak the content and let go of everything that is not important for your query
- caching might work in some cases depending on what data you want to pull
- slimming down the context actually seems to make quite a difference