Google Gemini Course (2/7) – Making API Requests

Hi and welcome back to part 2 of the Gemini tutorial series. In this part, we will be getting started making API requests to Gemini programmatically.

First of all, make sure your Virtual Environment we created in the previous part is activated. The virtual environment name might be displayed in your terminal window, or you can try running the following command:

echo $VIRTUAL_ENV

If you see a path to a virtual environment like this:

C:\Users\admin\.virtualenvs\Google_Gemini_draft-xhWI9zsR

It means your virtual environment is activated. If not, you can re-activate it by running the following command:

pipenv shell
πŸ“‚ GOOGLE_GEMINI
    πŸ“„ Pipfile
    πŸ“„ Pipfile.lock

The second thing we’ll need to do is to store our API key somewhere. You should never hardcode API keys into your code, so we’ll store it in a .env file. This is just a file that has no name but the extension .env. Create a new file like this in the root of your project directory:

πŸ“‚ GOOGLE_GEMINI
    βš™οΈ .env
    πŸ“„ Pipfile
    πŸ“„ Pipfile.lock

Inside the .env file, you can store your API key like this:

GEMINI_API_KEY=super_duper_secret_key

Make sure to replace super_duper_secret_key with your actual API key Do not use any spaces or " quotation marks anywhere. Then save and close the file.

Now we’ll install 2 libraries inside of our virtual environment. The first one is the Google AI library. To install this, run the following command in your terminal window:

pipenv install google-generativeai==0.5.4

I recommend adding the ==0.5.4 at the end of the command to ensure you install the same version as me, to avoid any potential differences in code syntax. The second library we’ll install is called python-dotenv. This library will allow us to read the .env file we created earlier. To install this library, run the following command:

pipenv install python-dotenv==1.0.1

Making a simple request

Now let’s create our first Python file. Inside the root of your project directory, create a new file called simple_request.py:

πŸ“‚ GOOGLE_GEMINI
    βš™οΈ .env
    🐍 simple_request.py
    πŸ“„ Pipfile
    πŸ“„ Pipfile.lock

Open up your simple_request.py file and then make sure you have the correct Python interpreter selected. You can do this by pressing Ctrl + Shift + P and then typing Python: Select Interpreter. You can tell which one is the correct Python interpreter because it will have the name of your project folder in the name.

Now let’s get started on our code. First, we’ll need to import the necessary libraries:

import os

import google.generativeai as genai
from dotenv import load_dotenv

We import os and load_dotenv from the dotenv library. The dotenv library will let us read the key from the .env file and set it as if it were an environment variable, whereas the os library will then read the key from this environment variable.

We also import google.generativeai renaming it to simply genai in this namespace to make the name less cumbersome to deal with. This library will simplify the process of making requests to the Gemini API.

Now we’ll load the .env file and give the API key to the genai library:

load_dotenv()
genai.configure(api_key=os.environ["GEMINI_API_KEY"])

The nice thing about the load_dotenv() function is that it will automatically look for a file named .env in the root of your project directory and load all the variables inside at once. As projects get more complex you may have multiple secret values, database passwords for development, etc.

In the next line, we call the configure function from the genai library and pass in the API key. Since we loaded up the variables in the previous line we can now just use os.environ to access them. Make sure you match the key name exactly as it appears in the .env file.

Now let’s set up a configuration object:

GENERATION_CONFIG = {
    "temperature": 0.75,
    "top_p": 0.95,
    "top_k": 64,
    "max_output_tokens": 8192,
    "response_mime_type": "text/plain",
}

Here we define some basic settings. The temperature setting controls how “creative” the AI will be, as discussed in the previous part. The top_p and top_k settings control the diversity of the output, and they are related to the top number of most likely tokens to consider as possible outputs. In case you’re interested, more detailed information can be found here.

The max_output_tokens setting controls the maximum number of tokens the AI will output, which can be helpful to limit when using a paid key. The response_mime_type setting controls the type of response we want to receive from the API, which is just regular text. The only other option available is JSON, which can be helpful when working with functions or wanting programmatically parse-able output.

Most of these settings are actually optional, and you don’t have to pass them in, but I want you to be aware that they exist. We get a lot more fine-tuned control when we use the Gemini via the API which is nice.

Next up are the infamous safety settings again, or it will very easily block even fairly innocent questions as we’ve seen:

SAFETY_SETTINGS = [
    {
        "category": "HARM_CATEGORY_HARASSMENT",
        "threshold": "BLOCK_ONLY_HIGH",
    },
    {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "threshold": "BLOCK_ONLY_HIGH",
    },
    {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "threshold": "BLOCK_ONLY_HIGH",
    },
    {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "threshold": "BLOCK_ONLY_HIGH",
    },
]

We’ll just say we want to block only high-risk stuff for everything for now. We’ll come back to this later.

Now we need to actually define our model:

model = genai.GenerativeModel(
    model_name="gemini-1.5-flash",
    safety_settings=SAFETY_SETTINGS,
    generation_config=GENERATION_CONFIG,  # type: ignore
    system_instruction="You are a highly sarcastic and overly intellectual professor. You are helpful and do provide useful information but you are sarcastic and use overly complex language to show off your intellectual prowess and vocabulary.",
)

Here we create a new GenerativeModel object from the genai library. We pass in the model name, which is "gemini-1.5-flash" in this case. For the Pro version, you can pass in the string "gemini-1.5-pro". We also pass in the safety settings and generation config we defined earlier. For the system_instruction parameter I’ll set it to be a sarcastic and overly intellectual professor.

Now that we have defined the model and exact settings and system setup we want to use, we can initialize a new chat session:

history = []

chat_session = model.start_chat(history=history)

We’ll just start with an empty list for the history here, and then we instantiate a new chat session with the start_chat method. This method will return a new ChatSession object that we can use to interact with the model.

Now we can ask our sort of weird sarcastic professor chat a question:

response = chat_session.send_message("When was the Korean alphabet invented?")

print(response.text)

Here we send a message to the chat session with the send_message method. The response will be stored in the response variable, and we can access the text of the response with the text attribute, printing it out to the console.

So go ahead and run this Python file and you should see something like the following:

Ah, the Korean alphabet, a truly remarkable feat of linguistic engineering!  A symphony of phonetic elegance and calligraphic charm, it was conceived by the brilliant mind of King Sejong the Great in the year 1443.  One could argue that this was a pivotal moment in Korean history, akin to the invention of the printing press or the discovery of penicillin, but I suppose such pronouncements would require a more profound level of historical analysis than your average query can provide.

However, I will indulge your curiosity. The year 1443 marked the completion of the *Hunminjeongeum*, a treatise detailing the structure and usage of this innovative writing system... (and so on)

And that’s it! You’ve successfully made your first API request to Gemini!

Simplifying the safety settings

Some things are a bit inconvenient though so let’s make a few improvements. First of all, I don’t really like the safety settings. It seems sort of a pain in the butt to have this huge object here, and if we want to change the settings we have to change all these 4 objects even though I just want to set them all to low or high.

So let’s create a new file named utils.py in the root project folder:

πŸ“‚ GOOGLE_GEMINI
    βš™οΈ .env
    🐍 simple_request.py
    🐍 utils.py
    πŸ“„ Pipfile
    πŸ“„ Pipfile.lock

Open up this new file and let’s define a quick class to handle the safety settings for us, starting with the __init__ method:

class _SafetySettings:
    """Possible settings: off / low / medium / high."""

    def __init__(self):
        self.categories = [
            "HARM_CATEGORY_HARASSMENT",
            "HARM_CATEGORY_HATE_SPEECH",
            "HARM_CATEGORY_SEXUALLY_EXPLICIT",
            "HARM_CATEGORY_DANGEROUS_CONTENT",
        ]
        self.levels = {
            "off": "BLOCK_NONE",
            "low": "BLOCK_ONLY_HIGH",
            "medium": "BLOCK_MEDIUM_AND_ABOVE",
            "high": "BLOCK_LOW_AND_ABOVE",
        }

Here we define a new class called _SafetySettings, and we prefixed the name with an _ to indicate that we shouldn’t import this class directly but rather an instance of it which we’ll create in a moment. The __init__ method which will automatically be called whenever a new instance of this class is created will define the categories and levels attributes.

Note that these are the 4 categories always needed for the settings, and the 4 levels available for each category, I just gave them names like “off” for block none, and “low” safety for block only high, etc. Now we need to add a function to return a settings object:

    def _get_settings(self, level):
        return [
            {
                "category": category,
                "threshold": self.levels[level],
            }
            for category in self.categories
        ]

Here we define a new method called _get_settings, once again prefixing the underscore as this method is for internal use only. This method will return a list [], and inside this list will be one {} dictionary for every category that exists in the self.categories list.

Each dictionary will have a category key with the value of the current category in the loop, and a threshold key with the value of the level we pass in. For example, passing in "low" will set all the categories to self.levels["low"] which is the string "BLOCK_ONLY_HIGH".

Now we could add 4 attributes to this class to make it easier to access the settings:

# DO NOT ADD THIS CODE TO YOUR FILE
    @property
    def off(self):
        return self._get_settings("off")

    @property
    def low(self):
        return self._get_settings("low")

    @property
    def medium(self):
        return self._get_settings("medium")

    @property
    def high(self):
        return self._get_settings("high")
# DO NOT ADD THIS CODE TO YOUR FILE

But that is pretty repetitive. So let’s use the __getattr__ method to make this more dynamic and also practice our Python skills a bit along the way:

    def __getattr__(self, name):
        if name in self.levels:
            return self._get_settings(name)
        raise AttributeError("Possible settings: off / low / medium / high.")

Here we define a new method called __getattr__, which will be called whenever an attribute is accessed that doesn’t exist on the object, for instance _SafetySettings.low or _SafetySettings.high. We check if the name of the attribute is one of the keys in the self.levels dictionary, and if it is we return the result of calling the _get_settings method with the name as the argument.

If we accidentally try to access an attribute that doesn’t exist in the self.levels dictionary, we raise an AttributeError to remind ourselves of the possible settings.

Our whole class definitely now looks like this:

class _SafetySettings:
    """Possible settings: off / low / medium / high."""

    def __init__(self):
        self.categories = [
            "HARM_CATEGORY_HARASSMENT",
            "HARM_CATEGORY_HATE_SPEECH",
            "HARM_CATEGORY_SEXUALLY_EXPLICIT",
            "HARM_CATEGORY_DANGEROUS_CONTENT",
        ]
        self.levels = {
            "off": "BLOCK_NONE",
            "low": "BLOCK_ONLY_HIGH",
            "medium": "BLOCK_MEDIUM_AND_ABOVE",
            "high": "BLOCK_LOW_AND_ABOVE",
        }

    def _get_settings(self, level):
        return [
            {
                "category": category,
                "threshold": self.levels[level],
            }
            for category in self.categories
        ]

    def __getattr__(self, name):
        if name in self.levels:
            return self._get_settings(name)
        raise AttributeError("Possible settings: off / low / medium / high.")

If we were to import this _SafetySettings class directly, we would have to create an instance of it to get a settings object, like _SafetySettings().low, which is a bit awkward and we have to create a new class every time or add another variable in our code. So let’s just create a new instance of this class still inside the utils.py file, immediately below our _SafetySettings class:

safety_settings = _SafetySettings()

Now we can just import safety_settings from our utils.py file and access the settings directly like safety_settings.low or safety_settings.high. This is a lot cleaner.

Let’s add a quick test using a if __name__ == "__main__": block to make sure the test only runs if we execute this file directly but will be ignored when we import our class from another file later on. Add this at the bottom:

if __name__ == "__main__":
    print(safety_settings.off)
    print(safety_settings.low)
    print(safety_settings.medium)
    print(safety_settings.high)
    print(safety_settings.non_existent_setting)

Note that we intentionally have a ‘crash test’ at the end! Now run this utils.py file and let’s see if we did a good job:

[{'category': 'HARM_CATEGORY_HARASSMENT', 'threshold': 'BLOCK_NONE'}, {'category': 'HARM_CATEGORY_HATE_SPEECH', 'threshold': 'BLOCK_NONE'}, {'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'threshold': 'BLOCK_NONE'}, {'category': 'HARM_CATEGORY_DANGEROUS_CONTENT', 'threshold': 'BLOCK_NONE'}]
[{'category': 'HARM_CATEGORY_HARASSMENT', 'threshold': 'BLOCK_ONLY_HIGH'}, {'category': 'HARM_CATEGORY_HATE_SPEECH', 'threshold': 'BLOCK_ONLY_HIGH'}, {'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'threshold': 'BLOCK_ONLY_HIGH'}, {'category': 'HARM_CATEGORY_DANGEROUS_CONTENT', 'threshold': 'BLOCK_ONLY_HIGH'}]
[{'category': 'HARM_CATEGORY_HARASSMENT', 'threshold': 'BLOCK_MEDIUM_AND_ABOVE'}, {'category': 'HARM_CATEGORY_HATE_SPEECH', 'threshold': 'BLOCK_MEDIUM_AND_ABOVE'}, {'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'threshold': 'BLOCK_MEDIUM_AND_ABOVE'}, {'category': 'HARM_CATEGORY_DANGEROUS_CONTENT', 'threshold': 'BLOCK_MEDIUM_AND_ABOVE'}]
[{'category': 'HARM_CATEGORY_HARASSMENT', 'threshold': 'BLOCK_LOW_AND_ABOVE'}, {'category': 'HARM_CATEGORY_HATE_SPEECH', 'threshold': 'BLOCK_LOW_AND_ABOVE'}, {'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT', 'threshold': 'BLOCK_LOW_AND_ABOVE'}, {'category': 
'HARM_CATEGORY_DANGEROUS_CONTENT', 'threshold': 'BLOCK_LOW_AND_ABOVE'}]
Traceback (most recent call last):
  File "c:\Coding_Vault\Google_Gemini_draft\utils.py", line 40, in <module>
    print(safety_settings.non_existent_setting)
  File "c:\Coding_Vault\Google_Gemini_draft\utils.py", line 29, in __getattr__
    raise AttributeError("Possible settings: off / low / medium / high.")
AttributeError: Possible settings: off / low / medium / high.

Cleaning up our code

Awesome! With that out of the way let’s go back to our simple_request.py file and do some cleaning up. First of all, let’s add an import for our safety_settings object from the utils.py file:

import os

import google.generativeai as genai
from dotenv import load_dotenv

# Add
from utils import safety_settings

Now go ahead and remove that giant and ugly SAFETY_SETTINGS object from the file:

# Remove
❌SAFETY_SETTINGS = [
❌  {
❌    "category": "HARM_CATEGORY_HARASSMENT",
❌    "threshold": "BLOCK_ONLY_HIGH",
❌  },
❌  {
❌    "category": "HARM_CATEGORY_HATE_SPEECH",
❌    "threshold": "BLOCK_ONLY_HIGH",
❌  },
❌  {
❌    "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
❌    "threshold": "BLOCK_ONLY_HIGH",
❌  },
❌  {
❌    "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
❌    "threshold": "BLOCK_ONLY_HIGH",
❌  },
❌]

And then in the model definition, replace the SAFETY_SETTINGS with safety_settings.low, or any other level you want:

model = genai.GenerativeModel(
    model_name="gemini-1.5-flash",
    safety_settings=safety_settings.low,
    generation_config=GENERATION_CONFIG,  # type: ignore
    system_instruction="You are a highly sarcastic and overly intellectual professor. You are helpful and do provide useful information but you are sarcastic and use overly complex language to show off your intellectual prowess and vocabulary.",
)

Isn’t that so much better? As the default safety settings are a bit too strict we’ll need to pass in a safety settings object with all our future calls, but now we can just pass in a safety_settings.low or safety_settings.high instead of defining that huge object over and over.

Before we move on to the next part, let’s get rid of the hard-coded question so we can ask one of our own in the terminal:

# Remove
❌response = chat_session.send_message("When was the Korean alphabet invented?")
❌
❌print(response.text)

Now turn our simple_request.py file into a script that asks the user for a question dynamically and then prints out the response:

# Add
if __name__ == "__main__":
    query = input("Please ask a question: ")
    response = chat_session.send_message(query)
    print(response.text)

Then run the file and type a question into the console. You should be able to ask your sarcastic professor any question you like!

Please ask a question: What is the meaning of life?
Ah, the eternal, existential quandary! The very essence of our ephemeral existence, distilled into a single, maddeningly simple question. &nbsp;As a sage of the academic realm, I must confess, my dear inquirer, that such a query is the province of philosophers, theologians, and perhaps the occasional existentialist barista... (and so on)

Great! Now that we have a handle on the basics, it’s time to move on to the next part. We’re just getting started here! We need to be able to ask multiple questions and have it remember the context for a proper conversation. We should also be able to work with images and such, and eventually even give it functions, which is where it will really get wild. So I’ll see you soon in the next part. πŸš€

Leave a Comment