---
title: "An introduction to Marvin and Ibis"
author: "Cody Peterson"
date: "2023-10-12"
execute: 
  warning: false
categories:
    - "LLMs and data"
---

## Introduction

In this "LLMs and data" series, we'll explore how to apply large-language models
(LLMs) to data analytics. We'll walk through the steps to build Ibis Birdbrain.

Throughout the series, we'll be using
[Marvin](https://www.askmarvin.ai/welcome/overview/) and
[Ibis](https://ibis-project.org). A brief introduction to each is provided
below.

## Marvin

[Marvin](https://www.askmarvin.ai/welcome/overview/) is an AI engineering
framework that makes it easy to build up to an interactive conversational
application.

Marvin makes calls to an AI platform. You typically use an API key set as an
environment variable -- in this case, we'll load a `.env` file that contians
secrets for the AI platform that Marvin will use. We also set the large language
model model.

```{python}
import marvin  # <1>

from rich import print  # <1>
from time import sleep  # <1>
from dotenv import load_dotenv  # <1>

load_dotenv()  # <2>

# increase accuracy
marvin.settings.llm_model = "openai/gpt-4"  # <3>
# decrease cost
# marvin.settings.llm_model = "openai/gpt-3.5-turbo"  # <3>

test_str = "working with data and LLMs on 18+ data platforms is easy!"  # <4>
test_str
```

1. Import the libraries we need.
2. Load the environment variable to setup Marvin to call our OpenAI account.
3. Configure the LLM model to use.
4. Some text to test on

### Functions

AI functions are one of the building blocks in Marvin and allow yout to specify
a typed python function with no code -- only a docstring -- to achieve a wide
variety of tasks.

We'll demonstrate this with an AI function that trnaslates text:

```{python}
@marvin.ai_fn
def translate(text: str, from_: str = "English", to: str = "Spanish") -> str:
    """translates the text"""

translate(test_str)
```

```{python}
# | code-fold: true
sleep(1) # <1>
```

1. Avoid rate-limiting by waiting.

```{python}
translate(translate(test_str), from_="Spanish", to="English")
```

```{python}
# | code-fold: true
sleep(3) # <1>
```

1. Avoid rate-limiting by waiting.

### Models

AI models are another building block for generating python classes from input
text. It's a great way to build structured data from unstructured data that can
be customized for your needs. 

We'll demosntrate this with an AI model that translates text:

```{python}
from pydantic import BaseModel, Field

# decrease cost
marvin.settings.llm_model = "openai/gpt-3.5-turbo"

@marvin.ai_model
class ExtractParts(BaseModel):
    """Extracts parts of a sentence"""
    subject: str = Field(..., description="The subject of the sentence.")
    objects: list[str] = Field(..., description="The objects of the sentence.")
    predicate: str = Field(..., description="The predicate of the sentence.")
    modifiers: list[str] = Field(..., description="The modifiers of the sentence.")

ExtractParts(test_str)
```

```{python}
# | code-fold: true
sleep(1) # <1>
```

1. Avoid rate-limiting by waiting.

### Classifiers

AI classifiers are another building block for generating python classes from
input text. It's the most efficient (time and cost) method for applying LLMs as
it only results in a single output token, selecting an output in a specified
Enum.

We'll demonstrate this by classifying the language of some text:

```{python}
from enum import Enum

# increase accuracy
marvin.settings.llm_model = "openai/gpt-4"

@marvin.ai_classifier
class IdentifyLanguage(Enum):
    """Identifies the language of the text"""

    english = "English"
    spanish = "Spanish"


IdentifyLanguage(test_str).value
```

```{python}
# | code-fold: true
sleep(1) # <1>
```

1. Avoid rate-limiting by waiting.

```{python}
IdentifyLanguage(translate(test_str)).value
```

```{python}
# | code-fold: true
sleep(3) # <1>
```

1. Avoid rate-limiting by waiting.

## Ibis

[Ibis](https://ibis-project.org) is the portable Python dataframe library that
enables Ibis Birdbrain to work on many data platforms at native scale.

Ibis makes calls to a data platform, providing an API but pushing the compute to
(local or remote) query engines and storage. DuckDB is the default and we'll
typically use it for demo puroses. You can work with an in-memory instance, but
we'll often create a database file from example data:

```{python}
import ibis  # <1>

con = ibis.connect("duckdb://penguins.ddb")  # <2>
t = ibis.examples.penguins.fetch()  # <2>
t = con.create_table("penguins", t.to_pyarrow(), overwrite=True)  # <2>
```

1. Import the libraries we need.
2. Setup the demo datain an Ibis backend.

You will typically connect to an existing data platform via your corresponding
Ibis backend and have access to a number of tables:

```{python}
import ibis  # <1>

ibis.options.interactive = True  # <2>

con = ibis.connect("duckdb://penguins.ddb")  # <3>
t = con.table("penguins")  # <3>
```

1. Import Ibis.
2. Configure Ibis (interactive).
3. Connect to the data and load a table into a variable.

### Backend

A backend provides the connection and basic management of the data platform.
Above, we created the `con` variable that is an instance of a DuckDB backend:

```{python}
con
```

It usually contains some tables:

```{python}
con.list_tables()
```

We can access some internals of Ibis to see what backends are available:

::: {.callout-tip}
Don't rely on accessing internals of Ibis in production.
:::

```{python}
backends = [entrypoint.name for entrypoint in ibis.util.backend_entry_points()]
backends
```

### Table

You typically work with a table, conventionally named `t` for demo or
exploratory purposes:

```{python}
t
```

When working with many tables, you should name them descriptively.

### Schema

A table has a schema that Ibis maps to the data platform's data types:

```{python}
t.schema()
```

## LLMs and data: Marvin and Ibis

You can use Marvin and Ibis together to easily apply LLMs to data.

```{python}
from ibis.expr.schema import Schema
from ibis.expr.types.relations import Table

@marvin.ai_fn
def sql_select(
    text: str, table_name: str = t.get_name(), schema: Schema = t.schema()
) -> str:
    """writes the SQL SELECT statement to query the table according to the text"""


query = "the unique combination of species and islands"
sql = sql_select(query).strip(";")
sql
```

```{python}
t.sql(sql)
```

```{python}
# | code-fold: true
sleep(3) # <1>
```

1. Avoid rate-limiting by waiting.

```{python}
t.sql(sql_select(query + " and include their counts in from highest to lowest").strip(";"))
```

## Next steps

You can get involved with [Ibis
Birdbrain](https://github.com/ibis-project/ibis-birdbrain), our open-source data
& AI project for building next-generation natural language interfaces to data.

[Read the next post in this series](../llms-and-data-pt1).
