supereasyai 1.0.6
A super easy AI LLM library.
Requires Python: >=3.8

# supereasyai

[![PyPI](https://img.shields.io/pypi/v/supereasyai.svg)](https://pypi.org/project/supereasyai/)
[![Tests](https://github.com/DominicDJC/supereasyai/actions/workflows/test.yml/badge.svg)](https://github.com/DominicDJC/supereasyai/actions/workflows/test.yml)
[![Changelog](https://img.shields.io/github/v/release/DominicDJC/supereasyai?include_prereleases&label=changelog)](https://github.com/DominicDJC/supereasyai/releases)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/DominicDJC/supereasyai/blob/main/LICENSE)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/DominicDJC/supereasyai)

A super easy AI LLM library.

## Installation

Install this library using `pip`:
```bash
pip install supereasyai
```
## Usage

Get an AI up and running fast with super easy tool support via Python functions.

## Initializing an AI

#### Supported Providers (So far)
- OpenAI
- Groq
- Ollama

#### OpenAI
```python
ai = create_openai(
    api_key=OPENAI_API_KEY,
    model="gpt-4.1-mini"
)
```
#### Groq
```python
ai = create_groq(
    api_key=GROQ_API_KEY,
    model="llama-3.3-70b-versatile"
)
```
#### Ollama
```python
ai = create_ollama(
    model="llama-3.1"
)
```

### Using Environment Variables

You can store your API keys in an environment if prefered. The default key is ```AI_API_KEY```.
This can be changed by setting the ```api_environment_key``` variable when initializing an AI.
```python
ai = create_openai(
    model="gpt-4.1-mini",
    api_environment_key="OPENAI_API_KEY"
)
```

### Setting Models

You do not have to have a preset model. The model used can be changed on the AI object or overridden when calling the AI at any time.
```python
# Creates a GROQ AI with no model set
ai = create_groq(
    api_key=GROQ_API_KEY
)

ai.model = "llama-3.3-70b-versatile" # Changes the set model

ai.query(
    model="llama-3.3-70b-versatile" # Ignores whatever set model is, if any, and uses the input one instead
)
```

## Messages

When querying an AI, you need to pass in an array of messages. There are multiple types of messages, but they are all built upon the base ```Message``` class.

A message can have one of five roles. Each one has its own unique class to make creating them easier.

- ```system``` - used to define the main prompt for the AI
```python
SystemMessage("You are an AI chatbot. Assist the user to the best of your ability.")
```

- ```developer``` - essentially the same as the ```system``` role
```python
DeveloperMessage("You are an AI chatbot. Assist the user to the best of your ability.")
```

- ```assistant``` - a message that the AI has sent
```python
AssistantMessage("Hi there, how can I help you!")
```

- ```tool``` - a response from a tool
```python
# Tool messages are a little more complex as they are used in response to tool calls.
# So they require the tool_call_id and tool name
ToolMessage("abc123", "get_weather", "It's a nice day outside")
```

- ```user``` - a message that the user has sent
```python
UserMessage("How long is a humpback whale?")
```

If you prefer to use the ```Message``` class, you can manual set the role:
```python
Message("system", "You are an AI chatbot. Assist the user to the best of your ability.")
```

### Advanced Messages

Some types of messages have support for more than just text.

Assistant messages can have tool calls. They are used by the AI to declare that it's calling a function. I contains a unique ```id```, the tool's ```name```, and the ```arguments``` to pass to the tool. Typically, you wouldn't create tool calls or assistant messages, as they are generated by the AI.

In response to tool calls, there are tool messages. A ```ToolMessage``` is just like any other message, but it also contains a ```tool_call_id``` to link it back to the ```ToolCall``` that called it.

### Converting to and from JSON

In some cases, you may need to convert messages to JSON or parse message from JSON, such as storing and retreiving chat history in a file. This can be done via the ```pack_messages``` and ```unpack_messages``` functions.

```pack_messages``` will pack a list of messages into a list of dicts.

```python
messages: list[Message] = [AssistantMessage("Hello user!"), UserMessage("Hello AI!")]
print(pack_messages(messages))

# Would convert to:
# [{"role": "assistant", "content": "Hello user!"}, {"role": "user", "content": "Hello AI!"}]
```

```unpack_messages``` will unpack a list of dicts into a list of messages.

```python
messages: list[dict] = [{"role": "assistant", "content": "Hello user!"}, {"role": "user", "content": "Hello AI!"}]
print(unpack_messages(messages))

# Would convert to:
# [AssistantMessage("Hello user!"), UserMessage("Hello AI!")]
```

## Querying an AI

There are *three* different ways to query an AI with supereasyai:
- A standard query via ```query```
- A format query that converts the response to a python object via ```query``` with the ```format``` parameter
- An automated query that automatically runs any called tools with variable autonomy via ```query_and_run_tools```

### Query

The ```query``` method returns either a ```AssistantMessage``` or an ```AssistantMessageStream``` depending on whether or not you want to stream the response.
```python
response: AssistantMessage = ai.query(
    messages=[UserMessage("What is 2 + 2?")]
)
# Access the message's actual content
print(response.content)

# The printed output would likely be something along the lines of:
# "2 + 2 = 4."
```

If you are streaming the response, it needs to be iterated:
```python
response: AssistantMessageStream = ai.query(
    messages=[UserMessage("What is 4 + 4?")],
    stream=True
)
# Iterates through the response as it's being generated
for chunk in response:
    # Print each together
    print(chunk, end="", flush=True)

# The printed output would likely be something along the lines of:
# "4 + 4 = 8."
```

Once an ```AssistantMessageStream``` has been fully iterated, you can access the full completed result and properties just like a regular ```AssistantMessage```:
```python
print(response.content)
# 4 + 4 = 8.
```

### Query Format

In some scenarios, you may want an AI's response to be restricted to a specific format. This can be easily achieved via the ```format``` parameter.

The ```format``` parameter should be an object type, which will be automatically converted into a JSON schema for the AI to interpret.

Responses when using ```format``` will come back as a ```FormattedAssistantMessage```. It extends ```AssistantMessage``` but also holds the formatted response in the object type provided as the ```format```

```python
class MathReasoning:
    def __init__(self, steps: list[str], answer: str) -> None:
        self.steps: list[str] = steps
        self.answer: str = answer

response: FormattedAssistantMessage = ai.query(
    messages=[UserMessage("What is 2 + 2?")],
    format=MathReasoning
)
formatted: MathReasoning = response.formatted

print(formatted.steps)
print(formatted.answer)

# The printed output would probably look something like this:
# ["Start with the number 2.", "Add another 2 to it.", "2 + 2 equals 4."]
# 4
```

You can provide descriptions for the AI via docstrings in the **reStructured (reST)** format. This can help the AI understand what to put where if the format is more complex.
```python
class MathReasoning:
    def __init__(self, steps: list[str], answer: str) -> None:
        """
        :param steps: The steps to solve the problem
        :param answer: The answer to the problem
        """
        self.steps: list[str] = steps
        self.answer: str = answer
```

### Using Tools

With supereasyai, regular Python functions can be used as tools.
```python
def add(a: int, b: int) -> str:
    return f"{a} + {b} = {a + b}"

response: AssistantMessage = ai.query(
    messages=[UserMessage("What is 2 + 2?")],
    tools=[add]
)
```

In this scenario, the AI will likely create a tool call to run the ```add``` tool with the arguments: ```{"a": 2, "b": 2}```.

The ```query``` function does *not* automatically run its tool calls. You can either handle it manually, running the functions and creating the ```ToolMessage``` responses:
```python
# Maybe something like this
tool_messages: list[ToolMessage] = []
for tool_call in response.tool_calls:
    # Check which tool was called
    if tool_call.name == "add":
        tool_messages.append(ToolMessage(
            tool_call_id=tool_call.id,
            name=tool_call.name,
            content=add(tool_call.arguments["a"], tool_call.arguments["b"])
            ))
    # This would easily pile up with more and more functions
```
Or you can use the ```run_tool_calls``` method to automatically run the functions and get a ```ToolMessage``` responses for each one.
```python
# Pass in any of the tools that the AI could have called
tool_messages: list[ToolMessage] = response.run_tool_calls([add])
```

### Query and Run Tools

The ```query_and_run_tools``` method makes it even easier. It will query the AI and run the tools for you automatically, returning both the ```AssistantMessage```(s) and ```ToolMessage```(s).
```python
response: list[Message] = ai.query_and_run_tools(
    messages=[UserMessage("What is 2 + 2?")],
    tools=[add]
)

# This would automatically call and run the "add" function with a=2 and b=2
```

You can set the level of autonomy that the function runs at:
- ```"none"``` - The default value. Only queries the AI once. The AI will either provide a direct response or call tools that will be automatically run.
- ```"follow_up"``` - Runs any tools and ensures the AI provides a worded response afterwords. This queries the AI a maximum of 2 times.
- ```"full"``` - Queries the AI until it decides to provide a worded response. Useful when the AI needs to complete multiple steps.

With the previous example above, if you were to use ```follow_up```, the response would end with an ```AssistantMessage``` probably saying something along the lines of "The answer to 2 + 2 is 4." instead of just running the function.

Now consider this example:
```python
def get_password() -> str:
    return "spaghetti"

def get_secret(password: str) -> str:
    if password != "spaghetti":
        return "The password is incorrect"
    return "The secret is a good sauce"

response: list[Message] = ai.query_and_run_tools(
    messages=[UserMessage("What's the secret?")],
    tools=[get_password, get_secret],
    autonomy="full"
)
```

The AI can't get the secret until it knows the password, so it must first call the get_password tool. Afterwards, it can use the password it just learned to get the secret, then it will finally respond to the user with what the secret is. The response list would probably be something like this:
- ```AssistantMessage``` - A single tool call for the ```get_password``` tool
- ```ToolMessage``` - A response for the AI that would say ```"spaghetti"```
- ```AssistantMessage``` - A single tool call for the ```get_secret``` tool with the arguments: ```{"password": "spaghetti"}```
- ```ToolMessage``` - A response for the AI that would say ```"The secret is a good sauce"```
- ```AssistantMessage``` - The AI would finally tell the user what the secret is.

### Schemas

The AIs understand tools and formats via JSON Schemas.

supereasyai automatically creates these schemas with the [**doms-json**](https://github.com/DominicDJC/doms-json) Python package. To better understand the process, consider taking a look.

## Examples

### Simple Streamed Chatbot

```python
from supereasyai import AI, create_ollama, Message, SystemMessage, UserMessage, AssistantMessageStream

PROMPT = """
You are a thoughtful and talkative AI chatbot.
"""

ai: AI = create_ollama("llama3.1")
history: list[Message] = [SystemMessage(PROMPT)]
while True:
    user_input: str = input("You: ")
    history.append(UserMessage(user_input))
    response: AssistantMessageStream = ai.query(
        messages=history,
        stream=True
    )
    history.append(response)
    print("AI: ", end="")
    for chunk in response:
        print(chunk, end="", flush=True)
    print("")
```

### Simple Calculator AI

```python
from supereasyai import AI, create_openai, Message, UserMessage

def add(a: float, b: float) -> float:
    return a + b

def subtract(a: float, b: float) -> float:
    return a - b

def multiply(a: float, b: float) -> float:
    return a * b

def divide(a: float, b: float) -> float:
    return a / b

ai: AI = create_openai(model="gpt-4.1-mini")

while True:
    user_input: str = input("Your simple math question: ")
    response: list[Message] = ai.query_and_run_tools(
        messages=[UserMessage(user_input)],
        tools=[add, subtract, multiply, divide],
        autonomy="full"
    )
    print("AI: " + response[-1].content)
```

### Story Scene Generator

```python
from supereasyai import AI, create_groq, SystemMessage, UserMessage

GROQ_API_KEY = "groq-api-key-here"

PROMPT = """
Your job is to create a detailed scene for a story based on the user's prompt.
"""

class Character:
    def __init__(self, name: str, description: str, traits: list[str]) -> None:
        self.name: str = name
        self.description: str = description
        self.traits: list[str] = traits

class Scene:
    def __init__(self, name: str, description: str, characters: list[Character]) -> None:
        self.name: str = name
        self.description: str = description
        self.characters: list[Character] = characters

ai: AI = create_groq(
    api_key=GROQ_API_KEY
)

user_input: str = input("Your scene prompt: ")
response: FormattedAssistantMessage = ai.query(
    messages=[
        SystemMessage(PROMPT),
        UserMessage(user_input)
    ],
    format=Scene,
    model="llama-3.3-70b-versatile"
)
scene: Scene = response.formatted
```

## Development

To contribute to this library, first checkout the code. Then create a new virtual environment:
```bash
cd supereasyai
python -m venv venv
source venv/bin/activate
```
Now install the dependencies and test dependencies:
```bash
python -m pip install -e '.[test]'
```
To run the tests:
```bash
python -m pytest
```