IA locale avec Ollama et Nginx : un guide pour la validation automatisée

Traduit de l'original en espagnol. Lire en espagnol

iStock AI Generator

Objectif du projet

L’objectif de ce projet est d’exécuter un processus d’IA qui analyse des cartons de bingo et détermine si les numéros fournis donnent un bingo. Pour cela, nous utilisons des LLM locaux avec Ollama pour l’inférence et Nginx comme couche de sécurité pour gérer le SSL et l’authentification. Même si ce tutoriel porte sur l’analyse de cartons de bingo, le même principe s’applique à bien d’autres tâches nécessitant une validation par l’IA.

Ce tutoriel couvre la configuration d’Ollama sur un système Linux, y compris la configuration de Nginx comme reverse proxy avec SSL.

Il s’agit uniquement d’un exemple à des fins de test et il n’est pas destiné aux environnements de production.

Pour plus de détails sur Ollama, consultez le dépôt officiel : Ollama GitHub.

Installer Ollama

Pour installer Ollama, exécutez les commandes suivantes :

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

Télécharger les modèles

Récupérez les modèles nécessaires :

ollama pull llama3.2-vision
ollama pull phi4

Installer Nginx

Installez Nginx sur votre serveur :

sudo apt update
sudo apt install nginx -y

Obtenir des certificats SSL

Vous devez obtenir un certificat SSL pour sécuriser la communication. Vous pouvez utiliser Let’s Encrypt ou une autre autorité de certification, mais la procédure n’est pas couverte dans ce tutoriel.

Configurer Nginx comme reverse proxy avec SSL et authentification

Modifiez la configuration de Nginx :

sudo vim /etc/nginx/nginx.conf

Ajoutez ou modifiez le contenu suivant :

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;
        }
    }
}

Pour plus de détails sur la configuration de l’authentification dans Nginx, consultez la documentation officielle : Configurer l’authentification HTTP Basic.

Redémarrez Nginx pour appliquer les changements :

sudo systemctl restart nginx

Le code

D’abord, nous devons importer les bibliothèques nécessaires et définir quelques constantes.

import base64
import requests
import json

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

Fonction pour lire et encoder l’image

Cette fonction lit l’image du carton de bingo et l’encode en base64 pour pouvoir l’envoyer dans la requête POST.

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

Fonction pour envoyer une requête POST

Cette fonction envoie une requête POST au serveur avec l’image encodée et le modèle d’IA.

Seed : garantit la cohérence des réponses en fixant le caractère aléatoire du modèle.

Température : contrôle la créativité des réponses ; des valeurs basses donnent des sorties prévisibles, tandis que des valeurs élevées permettent plus de variabilité.

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

Définition du format de réponse attendu

Nous définissons ici le format que nous attendons en réponse de l’IA. Cela nous aide à structurer correctement les données. L’utilisation du structured output avec les modèles de langage (LLM) améliore la précision, facilite l’intégration avec des systèmes externes, réduit les ambiguïtés et optimise l’analyse des données. On gagne ainsi en fiabilité, en interopérabilité et en scalabilité dans les flux automatisés.

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"]
}

Préparation de l’image et requête initiale

Nous lisons et encodons l’image du carton de bingo, puis nous préparons le prompt pour identifier les numéros présents sur l’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)

Analyse des résultats et validation

Nous analysons les résultats renvoyés par l’IA pour obtenir la liste des numéros trouvés, puis nous vérifions si l’on peut annoncer un bingo avec une liste de numéros tirés.

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', [])

Code complet

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  :( !!!")

Ce processus vous permet d’exécuter un serveur Ollama sécurisé sous Linux, avec SSL et authentification, tout en effectuant la reconnaissance des numéros et la validation à l’aide de modèles d’IA.

Remarque : cet exemple est destiné uniquement aux tests et au développement. Il n’est pas recommandé pour les environnements de production.

Maximiliano Díaz Doglia

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

Publié dans : IA