Local AI with Ollama and Nginx: A Guide to Automated Validation

Translated from the Spanish original. Read in Spanish

iStock AI Generator

Project Purpose

The goal of this project is to run an AI process that analyses bingo cards and determines whether the numbers provided make a bingo. We do this using local LLMs with Ollama for AI inference and Nginx as a security layer to handle SSL and authentication. Although this tutorial focuses on analysing bingo cards, the same idea can be applied to many other tasks that need AI-driven validation.

This tutorial covers setting up Ollama on a Linux system, including configuring Nginx as a reverse proxy with SSL.

This is only an example for testing purposes and is not intended for production environments.

For more details about Ollama, visit the official repository: Ollama GitHub.

Install Ollama

To install Ollama, run the following commands:

curl -fsSL https://ollama.com/install.sh | sh

Download Models

Get the models you need:

ollama pull llama3.2-vision
ollama pull phi4

Install Nginx

Install Nginx on your server:

sudo apt update
sudo apt install nginx -y

Get SSL Certificates

You need an SSL certificate for secure communication. You can use Let’s Encrypt or another certificate authority, but the process isn’t covered in this tutorial.

Configure Nginx as a Reverse Proxy with SSL and Authentication

Edit the Nginx configuration:

sudo vim /etc/nginx/nginx.conf

Add or modify the following content:

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  3600;
    client_max_body_size  5G;
    client_body_timeout  3600;
    proxy_read_timeout  3600;
    proxy_send_timeout  3600;

    server {
        listen       11435 ssl;
        server_name  yourdomain.com;

        ssl_certificate      /path/to/your/cert.pem;
        ssl_certificate_key  /path/to/your/key.pem;

        ssl_session_cache    shared:SSL:1m;
        ssl_session_timeout  60m;

        ssl_ciphers  HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers  on;

        location / {
            proxy_pass http://localhost:11434;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

For details on setting up authentication in Nginx, see the official documentation: Configuring HTTP Basic Authentication.

Restart Nginx to apply the changes:

sudo systemctl restart nginx

The Code

First, we need to import the required libraries and define some constants.

import base64
import requests
import json

OLLAMA_SERVER_URL = "https://ollama.zerogap.com:11435"
MODEL_NAME = "llama3.2-vision"
IMAGE_PATH = "bingo.jpg"

Function to Read and Encode the Image

This function reads the image of the bingo card and encodes it in base64 so it can be sent in the POST request.

def encode_image(image_path):
    with open(image_path, 'rb') as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

Function to Send a POST Request

This function sends a POST request to the server with the encoded image and the AI model.

Seed: keeps responses consistent by fixing the model’s randomness.

Temperature: controls how creative the responses are; low values give predictable outputs, while high values allow more variability.

def send_post_request(model, prompt, images=None, stream=False, response_format=None, system_prompt=None):
    body = {
        "model": model,
        "prompt": prompt,
        "system": system_prompt,
        "images": images if images else [],
        "stream": stream,
        "format": response_format if response_format else {},
        "options": {
            "seed": 46,
            "temperature": 0.1
        }
    }
    headers = {
        "Content-Type": "application/json",
    }

    try:
        response = requests.post(f"{OLLAMA_SERVER_URL}/api/generate", json=body, headers=headers)
        response.raise_for_status()  # Raise an HTTPError on bad response
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"An error occurred: {e}")
        return None

Defining the Expected Response Format

Here we define the format we expect the AI to respond with. This helps us structure the data properly. Using structured output with language models (LLMs) improves accuracy, makes integration with external systems easier, reduces ambiguity and streamlines data analysis. The result is greater reliability, interoperability and scalability in automated workflows.

response_format = {
    "type": "object",
    "properties": {
        "found_numbers": {
            "type": "array",
            "items": {"type": "number"}
        }
    },
    "required": ["found_numbers"]
}

validation_response_format = {
    "type": "object",
    "properties": {
        "is_valid": {"type": "boolean"}
    },
    "required": ["is_valid"]
}

Preparing the Image and the Initial Request

We read and encode the image of the bingo card, then prepare the prompt to identify the numbers in the image.

encoded_image = encode_image(IMAGE_PATH)

vision_identifier_prompt =  "Identify and return all numbers present in the image. The output should be a structured JSON object with a key 'found_numbers' containing a list of all detected numbers."
system_prompt="Your task is to analyze the image provided, following specific instructions from the user."

vision_results = send_post_request(MODEL_NAME, vision_identifier_prompt, images=[encoded_image],response_format=response_format, system_prompt=system_prompt)

Analysing the Results and Validating

We analyse the results from the AI to get the list of numbers found, and then check whether a bingo can be called with a list of selected numbers.

if vision_results:
    message_content = vision_results.get('response')
    vision_data = json.loads(message_content)
    found_numbers = vision_data.get('found_numbers', [])

    validator_prompt = (
        f"""CARD: {found_numbers}, SELECTED: [32,29,17,75]"""
    )

    system_prompt = "Your task is to determine if all numbers in the 'SELECTED' list are present in the 'CARD' list. Return only a JSON object with the key 'is_valid' and the boolean value true if all numbers are found, or false otherwise. Do not provide any explanation or code. Example: Input: { \"CARD\": [3, 9, 10, 14, 17, 20, 24, 25, 29, 32, 34, 37, 42, 47, 48, 50, 55, 57, 62, 68, 69, 72, 70, 75], \"SELECTED\": [302, 29, 17, 9] } Output: { \"is_valid\": false }"

    MODEL_NAME = "phi4"
    validation_results = send_post_request(MODEL_NAME, validator_prompt, response_format=validation_response_format, system_prompt=system_prompt)

    if validation_results:
        validation_message_content = validation_results.get('response')
        validation_data = json.loads(validation_message_content)
        validated_numbers = validation_data.get('is_valid', [])

Complete code

import base64
import requests
import json

# Define constants for server URL and model
OLLAMA_SERVER_URL = "https://ollama.zerogap.com:11435"
MODEL_NAME = "llama3.2-vision"
IMAGE_PATH = "bingo.jpg"

# Function to read and encode the image
def encode_image(image_path):
    with open(image_path, 'rb') as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

# Function to send a POST request
def send_post_request(model, prompt, images=None, stream=False, response_format=None, system_prompt=None):
    body = {
        "model": model,
        "prompt": prompt,
        "system": system_prompt,
        "images": images if images else [],
        "stream": stream,
        "format": response_format if response_format else {},
        "options": {
            "seed": 46,
            "temperature": 0.1
        }
    }
    headers = {
        "Content-Type": "application/json",
    }

    try:
        response = requests.post(f"{OLLAMA_SERVER_URL}/api/generate", json=body, headers=headers)
        response.raise_for_status()  # Raise an HTTPError on bad response
        return response.json()
    except requests.exceptions.RequestException as e:
        print(f"An error occurred: {e}")
        return None

# Define the expected response format for a list of numbers
response_format = {
    "type": "object",
    "properties": {
        "found_numbers": {
            "type": "array",
            "items": {"type": "number"}
        }
    },
    "required": ["found_numbers"]
}

# Define the expected response format for validations (boolean)
validation_response_format = {
    "type": "object",
    "properties": {
        "is_valid": {"type": "boolean"}
    },
    "required": ["is_valid"]
}

# Prepare the image
encoded_image = encode_image(IMAGE_PATH)

# Improved vision identifier prompt
vision_identifier_prompt =  "Identify and return all numbers present in the image. The output should be a structured JSON object with a key 'found_numbers' containing a list of all detected numbers."
system_prompt="Your task is to analyze the image provided."

vision_results = send_post_request(MODEL_NAME, vision_identifier_prompt, images=[encoded_image],response_format=response_format, system_prompt=system_prompt)

if vision_results:
    # Parse the structured output from the vision results
    message_content = vision_results.get('response')
    vision_data = json.loads(message_content)
    found_numbers = vision_data.get('found_numbers', [])

    # Validate the results
    validator_prompt = (
        f"""CARD: {found_numbers}, SELECTED: [32,29,17,75]"""
    )

    system_prompt = "Your task is to determine if all numbers in the 'SELECTED' list are present in the 'CARD' list. Return only a JSON object with the key 'is_valid' and the boolean value true if all numbers are found, or false otherwise. Do not provide any explanation or code. Example: Input: { \"CARD\": [3, 9, 10, 14, 17, 20, 24, 25, 29, 32, 34, 37, 42, 47, 48, 50, 55, 57, 62, 68, 69, 72, 70, 75], \"SELECTED\": [302, 29, 17, 9] } Output: { \"is_valid\": false }"

    MODEL_NAME = "phi4"
    validation_results = send_post_request(MODEL_NAME, validator_prompt, response_format=validation_response_format, system_prompt=system_prompt)

    if validation_results:
        # Parse the structured output from the validation results
        validation_message_content = validation_results.get('response')
        validation_data = json.loads(validation_message_content)
        validated_numbers = validation_data.get('is_valid', [])

# Print the final analysis
print("--- Final Analysis ---")
if validated_numbers:
    print("BINGO!!!")
else:
    print("No BINGO  :( !!!")

This process lets you run a secure Ollama server on Linux with SSL and authentication while performing number recognition and validation with AI models.

Note: This example is intended only for testing and development. It is not recommended for production environments.

Maximiliano Díaz Doglia

AI Platform Engineer & Full-Stack Developer
Building Enterprise Integrations & Automations

Published in: AI