Mettre en place un serveur MCP en Python 3.13

Qu’est ce qu’un serveur MCP (Model Context Protocole?)

Un serveur MCP est un webservice qui fournit de la data, cette data va servir de contexte pour l’IA générative. A la différence du RAG (Retrieval Augmented Generation), il n’est pas besoin de base vectorielle pour stocker des informations.

Le workflow est le suivant, le client IA (Claude Desktop) va requêter le serveur MCP pour retirer les informations dont il a besoin pour envoyer au LLM.(chatGPT par exemple).

Cette technologie inventée par Anthropic (à l’origine de Claude) est devenue très populaire. Je pense que c’est dû au fait qu’il n’est besoin que de connaissances existantes (Webservice) par les développeurs pour pouvoir taquiner les IA afin d’éviter les hallucination.

Mais cela va au delà des hallucination, un serveur MCP est un webservice à destination non du programmeur mais de l’IA générative. Je pense que c’est une bonne comparaison, l’IA (ou plutôt l’agent IA) va pouvoir décider en autonomie de requêter un serveur MCP pour chercher les information dont il a besoin?

Donc un serveur MCP est un webservice pour agent IA, au même titre qu’un webservice est un …. webservice pour un programmeur. Dans notre cas on va utiliser Claude Desktop comme « agent IA » ou plutôt comme Client IA.

Mise en place du serveur MCP dans Pycharm

Création de mon environnement virtuel

On crée le répertoire MCP puis on fait la commande :

vous avez un répertoire appelé MCP rentrez dedans

$ python -m venv .venv

# activez l'environnement virtuel (sous powershell)
$ .\.venv\Scripts\Activate.ps1

#c'est activé (normalement) vous avez accès à la commande pip
$ pip --version

Installation de uv

uv est un gestionnaire de paquet nouvelle génération écrite en RUST.

pip install uv

puis avec uv on installe mcp 

uv add "mcp[cli]"

A partir de maintenant on va utiliser uv pour installer notre projet

uv init 

# la commande tree -L 2 nous donne la structure suivante
.
|-- README.md
|-- main.py
|-- .venv
|   |-- Include
|   |-- Lib
|   |-- Scripts
|   `-- pyvenv.cfg
|-- pyproject.toml
`-- uv.lock

Pour install tree dans Windows Gitbash, il faut télécharger l’EXE et le mettre dans C:\Program Files\Git\usr\bin, un tuto est accessible sur ce lien.

Le soucis c’est que mcp s’attends à ce que le dossier environnement virtuel soit nommé .venv, si vous faites la commande:

$ uv run mcp
warning: `VIRTUAL_ENV=mcpvenv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead


Désativez l'environnement avec 
$ deactivate
si un .venv est présent, exécutez le venv  de ce répertoire.

Exemple rapide de serveur MCP:

#server.py

from mcp.server.fastmcp import FastMCP

# Create an MCP server
mcp = FastMCP("Demo")


# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b


# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
    """Get a personalized greeting"""
    return f"Hello, {name}!"


# Add a prompt
@mcp.prompt()
def greet_user(name: str, style: str = "friendly") -> str:
    """Generate a greeting prompt"""
    styles = {
        "friendly": "Please write a warm, friendly greeting",
        "formal": "Please write a formal, professional greeting",
        "casual": "Please write a casual, relaxed greeting",
    }

    return f"{styles.get(style, styles['friendly'])} for someone named {name}."

Pour le faire tourner

mcp dev server.py

#il vous sera demandé d'installer le paquet @modelcontextprotocol/inspector@0.17.0

Source: https://composio.dev/blog/mcp-server-step-by-step-guide-to-building-from-scrtch

Connaitre l’objet global Javascript Object et ses propriétés

L’objet de tous les objets

En Javascript vous avez dû sans doute croiser Object, c’est un peu l’origine de tous les objets e, Javascript.

const obj = new Object()
console.log(obj)
{}
[[Prototype]]:Object

Si on creuse un peu plus l’objet :

Vous voyez toutes les propriétés nombreuse de cet object même primitif. Tous les objet en Javascript héritent de cet objet (Date, String etc).

Méthodes statique d Object

Les méthodes static peuvent peuvent être invoquées depuis Object sans instanciation

Object.keys({a:1,b:2})  // ['a','b']

Object.assign(target,…sources)

Copie des prpriétés d’un objet à un autre

const user = { name: "John" };
const details = { age: 30, country: "France" };

const merged = Object.assign({}, user, details);
console.log(merged);
// { name: 'John', age: 30, country: 'France' }

Object.create(proto,[propertiesObject])

Crée un nouvel objet avec en paramètre un prototype

const person = { greet() { console.log("Hello"); } };
const user = Object.create(person);
user.greet(); // Hello

Object.keys(obj)

Retourne un tableau de clé de l’objet

Object.keys({a:1, b:2}); // ['a', 'b']

Object.values(obj)

Retourne un tableau de valeurs de l’objet

Object.values({a:1, b:2}); // [1, 2]

Object.entries(obj)

Retourne un tableau de clé / valeur

Object.entries({a:1, b:2});
// [['a',1], ['b',2]]
// avec destructuring
for (const [key, val] of Object.entries({a:1, b:2})) {
  console.log(key, val);
}

Object.freeze(obj)

Après un freeze, on ne peut plus ajouter,effacer ou modifier une propriété

const config = { debug: true };
Object.freeze(config);
config.debug = false; // inopérant

Intéressant pour un objet configuration que vous voulez rendre immuable

Object.seal(obj)

Empêche l’ajout ou suppression de propriété, mais autorise la modification

const user = { name: "John" };
Object.seal(user);
user.name = "Jane";  // Ok
user.age = 30;       //  ignoré

Object.getOwnPropertyNames(obj)

Retourne tous les noms des propriétés (énumérable et non énumérables)

const obj = Object.create({}, { hidden: { value: 42, enumerable: false } });
console.log(Object.getOwnPropertyNames(obj)); // ['hidden']

Ne donne que les nom de niveau 1

Object.getOwnPropertyDescriptors(obj)

Retourne les informations de chaque propriété

const obj = { a: 1 };
console.log(Object.getOwnPropertyDescriptors(obj));

Utile pour le clonage incluant getter et setters

Object.getPrototypeOf(obj)

Retourne le prototype (en gros le parent, ou parent du parent etc) d’un objet donné

const arr = [];
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true

Object.setPrototypeOf(obj,prototype)

Sette le prototype à un objet

const animal = { speak() { console.log("hi"); } };
const dog = {};
Object.setPrototypeOf(dog, animal);
dog.speak(); // hi

C’est comme ça qu’on fait de l’orienté objet en Javascript et pas avec des classes, qui sont plus familières à la plupart des programmeurs.

Object.hasOwn(obj,prop)

(Ecmascript 2022 ) alternative plus safe pour hasOwnProperty

const user = { name: "Alice" };
console.log(Object.hasOwn(user, "name")); // true

Object.fromEntries(entries)

Convertit un tableau de [key,value] en un objet

const entries = [["name", "Bob"], ["age", 25]];
const user = Object.fromEntries(entries);
console.log(user); // { name: 'Bob', age: 25 }

Object.is(value1,value2)

Dit si deux objets sont les même (comme ===, mais dans le cas de NaN est plus safe

Object.is(NaN, NaN); // true
Object.is(0, -0);    // false

Object.defineProperty(obj,prop,descriptor)

Définit ou modifie une propriété avec descripteur.

const user = {};
Object.defineProperty(user, "name", {
  value: "Alice",
  writable: false,
  enumerable: true
});

Object.defineProperties(obj,props)

Définit de multiple propriétés

Méthodes d’instance

Les méthodes d’instance ne sont invocable que sur un objet instantié.

hasOwnProperty(prop)

Vérifie si la propriété existe directement dans l’objet.

const user = { name: "Alice" };
console.log(user.hasOwnProperty("name")); // true

isPrototypeOf(obj)

Vérifie si l’objet existe dans la chaine d eprototype d’un autre objet

function Animal() {}
function Dog() {}

Dog.prototype = Object.create(Animal.prototype);

const d = new Dog();
console.log(Animal.prototype.isPrototypeOf(d)); // true

propertyIsEnumerable(prop)

regarde si la propriété est énumérable

const obj = { x: 1 };
console.log(obj.propertyIsEnumerable("x")); // true

toString()

Retourne une représentation string de l’objet

console.log({}.toString()); // [object Object]

Utiliser Object.prototype.toString.call(value) pour détecter le type:

Object.prototype.toString.call([]);      // [object Array]
Object.prototype.toString.call(123);     // [object Number]

valueOf()

retourne la valeur primitive d’un objet (dans un contexte arithmétique)

const obj = { valueOf: () => 42 };
console.log(obj + 8); // 50

Applications pratiques de quelques méthodes

Clone an Object (shallow)

const copy = Object.assign({}, original);

Deep Clone (simple version)

const deepCopy = JSON.parse(JSON.stringify(original));

Merge Configurations

const defaultConfig = { debug: false, port: 8080 };
const userConfig = { debug: true };
const config = Object.assign({}, defaultConfig, userConfig);

Create Object from Key/Value Pairs

const pairs = [["id", 1], ["name", "John"]];
const obj = Object.fromEntries(pairs);

Immutable Constant Object

const roles = Object.freeze({ ADMIN: "admin", USER: "user" });

Installer et utiliser l’ORM Prisma avec NodeJS en Typescript

La base de données est MySQL

Installation de Prisma

npm init -y

npm install express @prisma/client

npx tsc --init   // va créer le fichier tsconfig.json

npx prisma --init  //


Fetching latest updates for this subcommand...

✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Run prisma dev to start a local Prisma Postgres server.
2. Define models in the schema.prisma file.
3. Run prisma migrate dev to migrate your local Prisma Postgres database.
4. Tip: Explore how you can extend the ORM with scalable connection pooling, global caching, and a managed serverless Postgres database. Read: https://pris.ly/cli/beyond-orm

More information in our documentation:
https://pris.ly/d/getting-started

A ce stade un fichier .env et un répertoire prisma a été créé. Le .env contient la chaine de connexion, par défaut c’est pour du postgres, mais on va changer en mysql. Il faut créer la base prisma_demo_ts avant de faire les opérations de migration.

DATABASE_URL="mysql://root:password@localhost:3306/prisma_demo_ts"

Maintenant on va constituer un fichier de modèle pour créer les tables dans

Création des fichiers de migration et migration dans la même commande !

npx prisma migrate dev --name add-user-contact-table 


Environment variables loaded from .env
Prisma schema loaded from prisma\schema.prisma
Datasource "db": MySQL database "prisma_demo_ts" at "localhost:3306"

Applying migration `20251018092422_add_user_contact_table`

The following migration(s) have been created and applied from new schema changes:

prisma\migrations/
  └─ 20251018092422_add_user_contact_table/
    └─ migration.sql

Your database is now in sync with your schema.

Le script index.ts

import express from 'express'
import { PrismaClient } from '@prisma/client'

const app = express()
const prisma = new PrismaClient()

app.use(express.json())

// GET all users
app.get('/users', async (req, res) => {
    const users = await prisma.user.findMany()
    res.json(users)
})

// POST new user
app.post('/users', async (req, res) => {
    const { name, email } = req.body
    const user = await prisma.user.create({ data: { name, email } })
    res.json(user)
})

app.listen(3000, () => console.log('🚀 Serveur démarré sur http://localhost:3000'))

Si vous avez une erreur de type commonJS blabla, c’est que la syntaxe import n’est pas commonJs. Pour voir la différence entre commonJS et ESM voir cet article.

Si vous avez un message d’avertissement relatif à verbatimModuleSyntax, commentez la ligne dans tsconfig.json.

Pour lancer la compilation du fichier index.ts, désignez le répertoire de sortie des fichier JS, c’est dans tsconfig.json, la clé outDir doit être décommentée :  « outDir »: « ./dist »,

npx tsc

ensuite faites
node node dist/index.js

Installer et utiliser l’ORM Prisma avec NodeJS

Installations de Prisma

// Créez un répertoire pour votre projet Prisma et entrez dedans

npm init -y
npm i prisma --save-dev

Créez la base de données

CREATE DATABASE prisma_demo;

// Dans le fichier .env mettre cette chaine de connexion

DATABASE_URL="mysql://root:password@localhost:3306/prisma_demo"

npm install  @prisma/client   

npx prisma init


✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.
Next steps:
1. Run prisma dev to start a local Prisma Postgres server.
2. Define models in the schema.prisma file.
3. Run prisma migrate dev to migrate your local Prisma Postgres database.
4. Tip: Explore how you can extend the ORM with scalable connection pooling, global caching, and a managed serverless Postgres database. Read: https://pris.ly/cli/beyond-orm

Un répertoire prisma a été créé dans le projet. Dans le fichier schema.prisma mettez le code ci-dessous :

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
}

Ce modèle crée une table User avec :

  • id auto-incrémenté
  • email unique
  • name optionnel
  • createdAt avec la date de création

Faire la migration

npx prisma migrate dev --name init

Cela crée :

  • table User
  • Génère le client Prisma dans node_modules/.prisma/client
  • un fichier de migration

Execution du script d’exploitation de la base de donnée :

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
    // Créer un utilisateur
    const newUser = await prisma.user.create({
        data: {
            email: 'alice@example.com',
            name: 'Alice',
        },
    })
    console.log('Utilisateur créé :', newUser)

    // Lire tous les utilisateurs
    const users = await prisma.user.findMany()
    console.log('Liste des utilisateurs :', users)
}

main()
    .catch((e) => console.error(e))
    .finally(async () => {
        await prisma.$disconnect()
    })

Ce script une fois exécuté va créer un utilisateur dans la base de données.

Visualiser la base de données avec Studio

npx prisma studio
Vue de Prisma Studio sur http://localhost:5555/

Syntaxe de l’ORM Prisma

// Trouver un user par email
const user = await prisma.user.findUnique({
  where: { email: 'alice@example.com' },
})

    // Mettre à jour un user
    const updatedRecord = await prisma.user.update({
        where: { id: 1 },
        data: { email: 'yvon.huynh@hotmail.com' },
    })
    console.log('Updated utilisateur :', updatedRecord )

// Supprimer un user
    // const deletedRecord = await prisma.user.delete({
    //     where: { email: 'yvon.huynh@gmail.com' },
    // })

    // console.log('Effacement utilisateur :', deletedRecord)

Rafraichir le navigateur avec nodeJS

Récemment pour les besoin de développement d’une extension Chrome, j’ai cherché un moyen de rafraichir l’extension car le process de rafraichissement était un peu fastidieux. Il fallait aller dans le menu des extensions (chrome://extensions) et rafraichir en cliquant sur un bouton. Je voulais automatiser cette procédure comme le fait le plugin Liveserver.

Installation du projet NodeJS

npm init -y
npm install --save-dev nodemon chokidar

#pour setup avancé (optionnel)
npm install --save-dev chrome-extension-cli

Création du script de reload

import chokidar from "chokidar";
import { exec } from "child_process";

const EXTENSION_PATH = "./dist"; // or your extension folder
const CHROME_PROFILE = "--profile-directory=Default"; // optional

// watch all files in your extension folder
chokidar.watch(EXTENSION_PATH, { ignoreInitial: true }).on("all", (event, path) => {
  console.log(`File ${path} changed — reloading Chrome extension...`);

  // command to reload the extension via Chrome debugger API
  exec(`osascript -e 'tell application "Google Chrome" to reload active tab'`);

  // alternative: use chrome-cli on macOS (brew install chrome-cli)
  // exec(`chrome-cli reload`);
});

Mise en place de nodemon

Dans package.json ajouter le script :

"scripts": {
  "dev": "nodemon --watch src --exec \"node reload-extension.js\""
}

et faites npm run dev

Dorénavant, il vous suffit de fermer et rouvrir pour réactiver l’extension si c’est une extension pour les devtools.

Installer votre propre IA avec Ollama sur votre ordinateur (Windows)

Installation de Ollama

Pour commencer il faut aller sur le site Ollama.com (à ne pas confondre avec Llama.com). Le lien Github, pour consulter les sources et la documentation sur les modèles

Téléchargez et installez. Une fois installé, vous aurez une opup comme ci-dessous :

Vous allez vous perdre dans la jungle des modèles. Vous connaissez sans doute aussi de nom Hugging Face, c’est un repository de modèle LLM open source. Une fois que vous avez installé Ollama, ouvrez un terminal et tapez la commande $ ollama, une liste de commande sera affichée. Allez sur le Github, vous verrz un tableau de modèles LLM, différentes capacité et taille, ça va de 1 Go à 404 Go ! En fonction de votre machine téléchargez celui que vous pouvez faire fonctionner !

La colonne parameters indique combien de paramètres est considéré par le modèle, comptez environs 1 giga de RAM pour 1 Giga (Billion milliards) de paramètres. Mon ordinateur fait 32 Giga de RAM, je peux aller avec le modèle à 30B paramètres.

Les commandes pour installer les LLM

ollama run llama2


Pour connaitres les différentes commande au sein du chat
/?
>>> /?
Available Commands:
  /set            Set session variables
  /show           Show model information
  /load <model>   Load a session or model
  /save <model>   Save your current session
  /clear          Clear session context
  /bye            Exit
  /?, /help       Help for a command
  /? shortcuts    Help for keyboard shortcuts

Use """ to begin a multi-line message.


Pour quitter :
/bye

Pour lister les modèles :
ollama list

.

Ecran du téléchargement du modèle, à la vue de la sortie terminal, ça ressemble à une image Docker

Test de llama2

La première chose que je constate, est que le modèle est vraiment limité, surtout quand vous avez l’habitude de vous amuser avec chatGPT ou Claude sur le web, donc sur les modèles les plus sophistiqués.

Par exemple je lui demande de me donner la recette du roesti, il me répond qu’il ne sait pas

Quand je lui demande la recette de la tarte tatin, il me liste en anglais (pas vraiment multilingue, ok pour la taille du modèle cependant !). Par contre comme il est hébergé en local il est très rapide. Je lui demande pourquoi il me répond qu’il n’a pas réussi à détecter (alors que je lui ai demandé en français), mais il me propose de donner la version française ce qu’il fit très bien.

Nous allons télécharger un autre modèle

Cette fois ci nous allons télécharger le modèle Mistral, équivalent en taille. Sur le repository, vous trouverez la commande dans le tableau

ollama run mistral

Après le téléchargement, il démarre le prompt automatiquement. Quand vous ferez bye, vous pourrez démarrer entre les deux modèles (Mistral ou llma2 en ligne de commande)
Pour effacer un modèle, vous avez la comamnde remove, mais pensez à faire ollama pour afficher les différentes commandes.

Et maintenant explorons l’API !

Eh oui c’est là que cela devient très intéressant car nous allons pouvoir exploiter un LLM depuis un programme pour automatiser nos tâche (notre GRAAL)

Ollama expose une API sur le localhost. Dans la barre des tâches, vous pouvez voir une image de Llama, cela veut dire que l’application Desktop est lancée, et donc que l’API est active. Mais si le desktop n’est pas llancé, vous pouvez toujours lancer le serveur en ligne de commande

ollama serve

En parlant de l’icone, vous pouvez accéder aux paramètres

Vous remarquez (j’espère) le slider pour définir la « mémoire » du LLM (la taille du contexte), ceci est utile pour se souvenir d’une conversation !

Vous pouvez exposer le LLM au réseau local (très intéressant !)

Installer le package Python ollama

Très simplement on va faire un script pour requêter Ollama par API avec pip install ollama. Si vous installez le paquet après avoir démarré l’environnement virtuel, il sera en local, si vous le faites hors environnement virtuels, il sera dans le système. Pour connaitre où est installé un package

#affiche les emplacements pouvant héberger les paquets
python -m site

# cherche un paquet en particuliers
python -m pip show <paquet>

#lister les paquets de l'environnement virtuel en cours
pip list
import ollama
client = ollama.Client()

model = "llama2"
prompt = "How to set up a venv?"

response = client.generate(model, prompt)
print(f"Response from {model}:")
print(response.response)

Si vous préférez que la réponse arrive morceau par morceau voici le script (il suffit de passer stream= True)

import ollama

client = ollama.Client()

model = "llama2"
prompt = "How to set up a venv?"

# Use the stream=True option
stream = client.generate(model=model, prompt=prompt, stream=True)

print(f"Response from {model}:\n")

for chunk in stream:
    # Each chunk is a small piece of the model's output
    print(chunk['response'], end='', flush=True)

print()  # final newline

Ok là c’est mieux mais on peut wrapper les phrases (retour à la ligne quand ça déborde)

Avec retour à la ligne sans streaming

import ollama
import textwrap
import shutil

client = ollama.Client()
model = "llama2"
prompt = "How to set up a venv?"

response = client.generate(model=model, prompt=prompt)
terminal_width = shutil.get_terminal_size().columns

print(f"Response from {model}:\n")
print(textwrap.fill(response['response'], width=terminal_width))

Avec retour à la ligne avec streaming

import ollama
import shutil
import sys

client = ollama.Client()
model = "llama2"
prompt = "How to set up a venv?"

stream = client.generate(model=model, prompt=prompt, stream=True)

print(f"Response from {model}:\n")

terminal_width = shutil.get_terminal_size().columns
current_line = ""

for chunk in stream:
    text = chunk["response"]
    for char in text:
        current_line += char
        if len(current_line) >= terminal_width - 1:
            print(current_line)
            current_line = ""
# print remaining text
if current_line:
    print(current_line)

Personnalisation du modèle

Avec un fichier de configuration, on peut agir sur la façon dont les réponse vont être apportées, il y a de nombreux paramètres qui font qu’un LLM va se comporer d’une certianes façon. Vous avez peut être remarqué que ChatGPT était plus jovial que Claude qui est plus sérieux. Perso j’aim bien cette touche.

Mettez Le fichier de personnalisation Modefile dans un répertoire et invoquez une commande sur ce fichier dans ce répertoire.

$ ollama create formabot -f ./Modelfile

# ici j'ai invoqué un modèle non présent ça met du temps à télécharger le modèle

On va lancer avec la commande run

$ ollama run formabot

>>> qui es tu?



# pour effacer le modèle
$ ollama rm formabot

Conclusion :

Dans sa forme, l’outil ressemble beaucoup à Docker (les commandes, les modèles). Voilà pour une première introduction de Ollama. Pourquoi est ce important d’avoir un LLM en local? Si vous voulez rester privé, avoir des temps de réponse rapide, et ne pas vous ruiner en crédit token, alors n’hésitez pas à utiliser un LLM open source, il y en a d’autres allez voir du côté de Deepseek et de HuggingFace.

Mettre en place une authentification JWT avec Node/express Typescript

Mettons nous dans le contexte d’une application avec front et back découplé. NodeJS/Express est le back avec une base de données, et le front par exemple est une application ReactJS. Toute l’application n’est que webservice, et l’on pourra tester l’application rien qu’avec Postman.

Ce dernier prend en charge l’authentification, grâce à la possibilité de stocker le token JWT et de le renvoyer à chaque requête.

Installation des packages NPM

Je suppose que vous avez déjà bootstrappé une application node/Express/Typescript, sinon allez visiter la page du lien. LEs paquet à installer sont jsonwebtoken, bcrypt et comme on travaille en typescript il faudra les fichier type pour l’autocomplétion. Je mets aussi mysql2 au cas où vous ne l’auriez pas installé.

npm install mysql2 bcrypt jsonwebtoken
npm install --save-dev @types/bcrypt @types/jsonwebtoken

Configurer le fichier d’environnement .env

JWT_SECRET=ta_cle_secrete_ici_change_la
JWT_EXPIRES_IN=1h
BCRYPT_SALT_ROUNDS=10

Voici le code de la route (!) qui s’occupe de l’authentification

app.post('/api/auth', async (req: Request, res: Response) => {

    const { email, password } = req.body

    if (!email || !password) {
        res.status(400).json({ message: "Email et mot de passe requis." });
    }
    try {
        const [rows] = await pool.execute(
            "SELECT id, email, password FROM utilisateurs WHERE email = ?", [email]
        )

        const users = rows as any[];

        if (users.length === 0) {
            res.status(401).json({ message: "Utilisateur inconnu." })
        }
        // vérification du mot de passe
        const user = users[0]
        const match = await bcrypt.compare(password, user.password);

        if (!match) {
            res.status(401).json({ message: "Mot de passe incorrect." });
        }

        // sinon on génère un JWT token
        const token = jwt.sign(
            { id: user.id, email: user.email },
            process.env.JWT_SECRET || "secretKey",
            { expiresIn: "24h" }
        );

        res.json({ token });

    } catch (err) {
        console.log(error);
        res.status(500).json({ message: 'Erreur serveur' })
    }

})

Je vous laisse mettre les imports qu’il faut. Aussi il vous faudra avoir la table utilisateurs pour

Requêter avec Postman.

Postman c’est comme un navigateur sans écran, en fait un navigateur est composé d’un écran et d’une antenne émettrice-réceptrice. Installez Postman, et démarrez votre serveur nodeJS, la route de l’authentification est http://localhost:3000/api/auth, faites une requête POST

Vous recevrez le JWT en réponse.

Stocker le JWT dans Postman

Copiez le token (sans les guillements et collez dans le champs avec à gauche la liste déroulante mis sur l’item Bearer Token.

Comprendre le fonctionnement du JWT

A chaque requêtes le serveur, en en-tête Authorization sera envoyé

Authorization: JhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MiwiZW1haWwiOiJ5dm9uLmh1eW5oQGdtYWlsLmNvbSIsImlhdCI6MTc1ODQ3NzgwOSwiZXhwIjoxNzU4NTY0MjA5fQ.JZ2Z5WRzCdSYiFcbIxknV5B9byJ_Fo9ueNDT9-iH40U

Côté serveur, on crée un middleware qui va :

  • Vérifier la présence du token.
  • Le valider avec jsonwebtoken.verify.
  • Attacher les infos décodées (userId, email, …) à req.user.
  • Laisser passer la requête si le token est valide.

Création du middleware pour authentifier la requête

Nous avons une route protégée, toute requête doit passer par la vérification de la validité du token, non expiré et valide.

app.get('/api/utilisateur/add', requireAuth, (req: AuthRequest, res: Response) => {
    res.json({ message: "accès  réussi" })
})

requireAuth est un middleware, il va filtrer toutes les requêtes qui viennent d’un client, l’avantage du middleware est qu’on n’a pas besoin de faire la validation manuellement dans la route, c’est automatique. Je le mets dans le répertoire src/middleware

fichier middleware/auth.ts

import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";

dotenv.config();

// solution JWT Stateless simple
const JWT_SECRET = process.env.JWT_SECRET || "dev_secret";
;

const verifyJwt = <T = object>(token: string): T => {
    return jwt.verify(token, JWT_SECRET) as T
}


export interface AuthRequest extends Request {
    user?: any;
}

export const requireAuth = (req: AuthRequest, res: Response, next: NextFunction) => {
    const authHeader = req.headers.authorization;
    if (!authHeader || !authHeader.startsWith("Bearer ")) {
        res.status(401).json({ message: "Missing or invalid authorization header" });
        return;//le simple fait de mettre return permet à authHeader de ne plus avoir de undefined possible (enlever le return pour montrer l'erreur)
    }

    const token = authHeader.split(" ")[1];

    if (!token) {
        return res.status(401).json({ message: "Token invalide" });
    }
    try {
        const payload = verifyJwt(token);
        req.user = payload;
        next();
    } catch (err) {
        res.status(403).json({ message: "Forbidden, vous êtes authentifié mais n'avez pas accès à la ressource, problème de permission" });
    }
}

Pour aller plus loin : https://www.youtube.com/watch?v=AU5WLAJkaC8

Créer une extension Google Chrome

Je démarre une nouvelle série passionnante sur les extensions Google Chrome !

Les extension Google Chrome (ou sur autre navigateur) sont des script HTML/CSS/ Javascript qui permettent d’interagir avec le DOM du navigateur.

Ce que j’adore c’est l’interaction avec les éléments du DOM, quelques soit le site web, ce qui permet une grande variété d’application, pour être plus productif !

Nous allons démarrer avec une extension très simple : une extension qui permet de mettre en local les prompts qu’on a fait avec chatGPT. Il servira à organiser les prompts, marque ceux qu’on aime, sauvegarder la liste en local, et pourquoi pas en ligne pour permettre de les partager. Il permettra peut être de créer une branche pour faire des variantes de prompt, ce qui est impossible au moment où j’écris ces lignes.

Je tiens à souligner que c’est grâce à un LLM que je progresse plus vite dans l’élaboration d’une extension Google Chrome.

J4ai fait des extension très simple par le passé, avec TamperMonkey, mais je ne me suis jamais aventuré à faire des extension pour de vrai. Mais avec l’arrivée de chatGPT, je peux maintenant m’attaquer à des projets ambitieux. Attention je ne dis pas que je vais demander à chatGPT de tout faire, ici l’utilisatoin de chatGPT que je fais est en mode « discovery ». J’utilise chatGPT pour aller beaucoup plus vite pour ingurgiter de l’information éparpillée sur le net, ce que mettrait des jours à faire, désormais je ne mettrait qu’une demi-journée.

Mais cet article n’est pas de parler de chatGPT, mais des extensions de navigateur Google Chrome.

Quels fichiers faut il pour faire une extension Google Chrome?

A minima il vous faudra le fichier manifest.json qui décrit l’extension, et un fichier javascript de point d’entrée.

manifest.json

{
    "manifest_version": 3,
    "name": "SEO Tools",
    "version": "1.0",
    "description": "Analyse les tags d'une page HTML",
    "permissions": [
        "storage"
    ],
    "content_scripts": [
        {
            "matches": [
                "*://*/*"
            ],
            "js": [
                "content.js"
            ]
        }
    ]
}

Lien : Automating terminals dans VsCode

Créer un utilisateur pour se connecter à Postgresql en bash

Pour les besoins de code PHP, vous devez vous connecter à la base de données avec PDO. Si vous n’avez jusqu’à présent fait une connexion qu’avec l’utilisateur postgres, vous ne pouvez faire ça avec ce dernier dans vos scripts PHP. Nous allons donc créer un utilisateur refschool.

Créer un utilisateur dans postgresql

Vous devez vous connecter avec l’utilisateur postgres, qui agit comme le superadministrateur, car vous ne pouvez pas vous connecter en root à postgresql, en fait c’est l’équivalent du root dans Mysql

sudo -u postgres psql  // attention c'est un u minuscule
// postgresql

CREATE USER refschool WITH LOGIN PASSWORD '123';

GRANT CONNECT ON DATABASE devdb TO refschool;
GRANT USAGE ON SCHEMA public TO refschool;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO refschool;

Notez que public est un SCHEMA et pas une table ! Si vous venez de MySQL comme moi, un SCHEMA est une table. Apparemment la logique de Postgresql est plus répandue du moins chez les grands systèmes de SGBDR comme SQLSERVER et ORACLE.

Création d’une table dans le SCHEMA public

CREATE TABLE public.users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) UNIQUE,
    age INTEGER,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

// mention du SCHEMA public, ce dernier étant par défaut dans Postgesql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) UNIQUE,
    age INTEGER,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Autres commandes de Postgresql

//

SELECT current_database();

//

Créer une paire de clés SSH pour s’authentifier sans mot de passe

A quoi ça sert?

Principalement pour vous connecter à un terminal distant sans mot de passe. Le principe étant d’avoir une paire de clé privée/publique. Vosu déposez la clé publique sur le terminal distant, ensuite pour vous y connecter il suffit de ssh à l’adresse IP

Il faut d’abord installer OpenSSH, qui va vous donner les outils

Génération de la paire de clé SSH

ssh-keygen -t rsa -C "your_email@example.com"

Vous serez amené à entrer un mot de passe (optionnel) appelée passphrase.

Uploader la clé publique sur le serveur distant

Il s’agit d’uploader la clé dans le répertoire /home/user. An l’absence de logiciel FTP, vous pouvez faire un SSH. LE plus simple est de se logger en root dans le serveur distant et de copier à la mains les texte des clés publiques. Le plus simple eset de copier coller le texte de la clé publique et le coller dans le fichier authorized_keys du serveur.

Connectez vous en root et éditez le fichier authorized_keys.

ssh-copy-id (source)

// syntaxe générale
ssh-copy-id [-f] [-n] [-i identity file] [-p port] [-o ssh_option] [user@]hostname

ssh-copy-id root@123.123.123.123

Créer un serveur FTP dans un Droplet Digital Ocean

Vous pouvez essayer de faire ce tuto dans un container docker sous Debian

On va d’abord créer un droplet, puis mettre à jour le package et installer Proftpd

apt update
apt install proftpd

On va créer un utilisateur FTP (pas un utiliser avec Shell), on est sous root

useradd yvon -d /home/yvon -m -s /bin/false yvon  // ce user ne possède pas de shell, et son répertoire home est /home/yvon
passwd yvon

//définir le mot de passe

Le but est que l’utilisateur lorsqu’il se connecte avec Filezilla, voit le répertoire /home/yvon, pour y mettre ses fichiers

Puis on va configurer le fichier /etc/proftpd/proftpd.conf

//Afin de contraindre le répertoire à /home/yvon on doit avoir

DefaultRoot ~

// comme yvon n'a pas accès au shell
RequireValidShell off

// spécifier les port passif (intervalle de ports)
PassivePorts 49152 65534

Il faut ouvrir les port 21 et la plage de port dans

ufw allow 21/tcp
ufw allow 49152:65534/tcp

// vérifier le status
ufw status

To                         Action      From
--                         ------      ----
21/tcp                     ALLOW       Anywhere                  
49152:65534/tcp            ALLOW       Anywhere                  
21/tcp (v6)                ALLOW       Anywhere (v6)             
49152:65534/tcp (v6)       ALLOW       Anywhere (v6)   

Faire un script bash pour envoyer des fichiers vers ce serveur FTP

Nous allons d’abord installer le client ftp si ce n’est déjà fait

apt install ftp

// upload d'un fichier (il faut créer le user et le login)
ftp -n 165.22.194.80 <<EOF
user yvon motdepasse
put fichier_local.txt
quit
EOF

On peut faire l’équivalent en script bash c’est plus sympa pour la simplicité

//script bash  syncftp.sh

// on définit les variables
HOST=165.22.194.80
USER=yvon
PASS=123
FILE=date.log

ftp -n $HOST << EOF
user $USER $PASS
put $FILE
quit
EOF

Mettez les lignes dans l’ordre, au début j’ai fait une bêtise j’ai mis le put avant le user !

Voici une version un peu améliorée, va setter le mode de transfert en binary malgré que ce soit du texte, ceci pour ménager les caractères spéciaux, les retour chariots CLRF <-> LF, le ls va lister le fichier pour vérifier que le fichier est bien uploadé !

HOST=165.22.194.80
USER=yvon
PASS=123
FILE=date.log

ftp -n $HOST << EOF
user $USER $PASS
binary
put $FILE
ls
quit
EOF

Cours administrateur de serveur Web Dans un Linux Debian Dans un Docker

Que contient ce cours Linux et administration de serveur web?

A partir d’une image Docker de Debian, on va installer différents éléments tels nginx, php-fpm, postgreSQL et faire tourner un serveur web. Nous allons voir la configuration d’un vhost, expliquer l’architecture client serveur, les DNS?

Puis on va installer un serveur FTP proftpD, nftable, fail2ban pour protéger le serveur web.

Puis on va faire un backup et un certificat autosigné, puis un certificat Let’s Encrypt

Nous allons aussi voir les commandes de base de Linux, les outils de base de Linux,

Sommaire et liens vers les cours

Article bonus sur Nginx

Introduction à nftables le remplaçant de iptables

Installation de nftables

Visualisation des règles (rules)

nft list ruleset


#Si vous avez le message même en étant root :

netlink: Error: cache initialization failed: Operation not permitted 

#il faut lancer le container avec le flag --privileged

docker run --privileged -it <image_docker>

Rien n’apparait, il n’y a pas de règles on va en créer une, d’abord on va créer une table

nft add table inet filter

puis nft list ruleset

table inet filter {
}

C’est vide pour l’instant, on va ajouter une règle pour autoriser les connexion sur le port 22

nft add rule inet filter input tcp dport 22 accept

mais vous risquez d'avoir l'erreur

Error: Could not process rule: No such file or directory
add rule inet filter input tcp dport 22 accept
                     ^^^^^                                                         

Comment ajouter une règle

nftable organise les règle en hiérarchie : table (conteneur de niveau supérieur) > chaine (groupe de règle dans une table) > règle (instruction de filtrage)

Types de tables:

  • ip : ipv4
  • ip6 : ipv6
  • inet : ipv4 + ipv6
  • arp : ARP
  • bridge : bridge
  • netdev : interface réseau

Création table puis chaine puis règle

# Créer la table inet filter (on vient de le faire)
nft add table inet filter

# Créer la chaîne input
nft add chain inet filter input { type filter hook input priority 0 \; policy accept \; }

# Maintenant ajouter votre règle SSH
nft add rule inet filter input tcp dport 22 accept

# lister les règles
nft list ruleset

table inet filter {
        chain input {
                type filter hook input priority filter; policy accept;
                tcp dport 22 accept
        }
}

Commandes de diagnostic

# voir une table spécifique
nft list table inet filter

# Voir le contenu d'une table spécifique
nft list table inet filter

# Voir les chaînes d'une table
nft list chains inet filter

# Voir une chaîne spécifique
nft list chain inet filter input

Générer un certificat autosigné dans Debian et mapper les ports docker et hôte

Cet article fait suite à l’article sur la mise en place d’un serveur web Nginx et php-fpm, suivi de Postgresql.

J’avais déjà fait par le passé un blog sur comment configurer pour Apache un https, il suffisait de pointer vers les certificats SSL dans le fichier vhost

Un certificat autosigné ne sert que lors de la phase de développement. EN effet si vous l’utilisez en production, il y a aura des avertissements. Les certificats permettent une marque de confiance, donc d’une autorité tierce de confiance. De ce fait un certificat autosigné n’aura aucue valeur, puisuqe c’est l’éditeur de site qui le génère. Il faut que ce soit un tiers indépendant qui le fasse, comme par exemple Lets Encrypt. Ce dernier ne génère que des certificat ordinaires, mais utilisable en production. Si vous voulez des certificats plus qualitatifs, comme les certificats EV, il faudra les acheter sur des sites spécialisés.

Nous allons générer un certificat SSL autosigné

mkdir -p /etc/ssl/private/mycert && cd /etc/ssl/private/mycert

ou vous pouvez aller dans le répertoire mycert pour avoir une commande plus courte
cd /etc/ssl/private

mkdir -p /mycert && cd mycert

Le flag -p permet de créer les dossier parents manquant dans la commande mkdir

Génération du certificat (ce sont deux fichiers)

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -subj "/C=FR/ST=France/L=Paris/O=MonEntreprise/CN=localhost"
  • -x509 : pour un certificat autosigné.
  • -nodes : ne pas chiffrer la clé privée (pas de mot de passe).
  • -days 365 : durée de validité (365 jours).
  • -newkey rsa:2048 : crée une nouvelle clé RSA 2048 bits.
  • -keyout : chemin vers la clé privée.
  • -out : chemin vers le fichier de certificat.
  • -subj : informations du certificat.

Maintenant dans le répertoire mycert vous avez les fichiers selfsigned.crt et selfsigned.key

Modification du fichier de configuration Nginx qu’on a vu dans cet article

# Redirection HTTP vers HTTPS (optionnel mais recommandé)
server {
    listen 80;
    server_name phpsite;
    return 301 https://$host$request_uri;
}

# Bloc HTTPS avec certificat autosigné
server {
    listen 443 ssl;
    server_name phpsite;

    ssl_certificate     /etc/ssl/private/mycert/selfsigned.crt;
    ssl_certificate_key /etc/ssl/private/mycert/selfsigned.key;

    root /var/www/phpsite;
    index index.php index.html;

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass 127.0.0.1:9000;
    }
}

Votre site non https va rediriger vers le https.

$ curl https://phpsite/info.php
// néanmoins vous aurez un erreur de type 
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

Ceci est dû au fait que ce soit un certificat autocertifié. Pour bypasser cette vérification, il faut rajouter le flag -k à la requête

curl -k https://phpsite/info.php

Redémarrer votre container docker et redémarrer les services

vous avez installé nginx, php-fpm, postgresql, et configuré votre vhost. Le soucis c’est la prochaine que vous faites un docker start <Id container> (après avoir stoppé votre container (ce ne sera pas le cas si vous fiates un exit de votre container), il faut redémarrer les services. Vous pouvez intégrer une commande

De même le vhost que vous avez ajouté dans /etc/hosts a disparu ! ceci est dû à la nature éphémère du container docker, mais heureusement il existe un flag pour ajouter le vhost quand vous faites un run

docker run -d \
  --name mon-site \
  -p 80:80 \
  --add-host=phpsite:127.0.0.1 \
  mon-image

// ce qui donne dans notre cas sauf qu'on est en mode interactif

docker run -it \
  --name phpsite \
  -p 80:80 \
  --add-host=phpsite:127.0.0.1 \
  debian-nginx

// il nous manque la commande bash pour démarrer php-fpm et nginx (remplacez le nom de l'image par votre image)
docker run -it \
  --name phpsite \
  -p 80:80 \
  --add-host=phpsite:127.0.0.1 \
  nginx_postgres_image \
  bash -c "/usr/sbin/php-fpm8.2 -F && nginx -g 'daemon off;'"


// ce qui donne en une seul ligne
docker run -it --name phpsite -p 80:80 --add-host=phpsite:127.0.0.1 nginx_postgres_image  bash -c "/usr/sbin/php-fpm8.2 -F && nginx -g 'daemon off;'"

// en non interactif
docker run -d --name phpsite -p 80:80 -p 443:443 --add-host=phpsite:127.0.0.1 nginx_postgres_image  bash -c "/usr/sbin/php-fpm8.2 -F && nginx -g 'daemon off;'"

Attention avec la syntaxe ci-dessus pour le bash bash -c « /usr/sbin/php-fpm8.2 -F && nginx -g ‘daemon off;' », ça ne va pas marcher car php-fpm étant lancé en foreground, il ne laissera pas la main à nginx pour démarrer, donc il faut remplacer le double && par le simple & , qui va lancer php-fpm en background

docker run -d --name phpsite -p 80:80 -p 443:443 --add-host=phpsite:127.0.0.1 nginx_postgres_image  bash -c "/usr/sbin/php-fpm8.2 -F & nginx -g 'daemon off;'"

On peut améliorer ceci en mettant dans un script bash. Nous pouvons aussi inclure le démarrage de postgresql afin d’avoir une stack complète pour développer.

Accéder au site phpsite depuis l’hôte

La partie intéressante est de se connecter à notre site dans docker depuis notre navigateur ou curl, car on a mappé les ports de docker et de l’hôte. Pour ce faire nous devons ajouter au fichier hosts de l’hôte : 127.0.0.1 phpsite.

//  depuis l'hôte

curl -k https://phpsite/info.php

// depuis votre navigateur vous aurez un avertissement de site non sécurisé, confirmez l'exception.

Prochaine étape nous allons plutôt explorer les volumes docker afin de pouvoir héberger les scripts php sur notre hôte.

Les volumes sous Docker c’est quoi?

C’est un répertoire sur l’hôte, qui sert à herberger des fichiers de code, mais accessible par le container docker, voyez ça comme un mount d’un drive sur un système de fichier Linux. Cela permet de travailler plus facilement et efficacement avec des container Docker. En effet notreIDE préféré est sur notre hôte et pas sur un container docker.

Dans la suite, mon répertoire se trouve dans le drive E: sur Windows dans le répertoire volume_nginx

docker run -d --name phpsite -p 80:80 -p 443:443 -v E:\volume_nginx:/var/www/phpsite --add-host=phpsite:127.0.0.1 nginx_postgres_image  bash -c "/usr/sbin/php-fpm8.2 -F & nginx -g 'daemon off;'"


// avec la ligne -v E:\volume_nginx:/var/www/phpsite on monte le répertoire hôte dans le container docker, du coup ce dernier va être masqué par le répertoire windows

// connextez vous en interactif dans le container en question

$ docker exec -it <idContainer> bash
// allez dans le répertoire /var/www/phpsite, il est vide pour l'instant mais créez un fichier index.php dedans et faites un ls vous verrez ce fichier. Félicitation vous avez réussi à connecter un volume ! 

Avouez que maintenant vous allez être beaucoup plus confortable pour faire un site web en php !

Générer un meilleur certificat SSL avec Let’s Encrypt

Avec une autorité externe les certificats sont de milleurs qualité. Let’s Encrypt délivre des certificats gratuitement et de grade production. MAIS il faut que votre site soit accessible depuis internet, car Let’s Encrypt va accéder à votre site pour installer des fichiers.

#installation de certbot
apt install certbot

# Génération du certificat
certbot certonly --webroot -w /var/www/html -d votre-domaine.com -d www.votre-domaine.com

Installation de Postgresql dans un container docker Debian

Installation de postgresSQL

apt update
apt install postgresql

Nous sommes dans le cas où il n’y a pas systemd ou supervisor, et cet article reprend la suite de la procédure d’installation de nginx et php-fpm

Normalement si vous avez installé avec apt, pas besoin de créer un répertoire ni d’initialiser une base de données.

Nous allons maintenant lancer postgresql, la commande est un peut longue et vous ne pouvez pas lancer en root

postgres /usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/15/main


"root" execution of the PostgreSQL server is not permitted.
The server must be started under an unprivileged user ID to prevent
possible system security compromise.  See the documentation for
more information on how to properly start the server.

De plus si on veut ne pas taper cette commande il vaut mieux ajouter au PATH

export PATH="$PATH:/usr/lib/postgresql/15/bin"

La commande à faire est via un utilisateur non root, il faut créer un utilisateur

// création d'utilisateur 
adduser refschool   // il vaut mieux utiliser adduser car useradd nécessite de setter le password avec la commande passwd

// on ajouter refschool dans la liste de sudoer
usermod -aG sudo refschool  // en fait on l'ajoute au groupe sudoer

On exécute en tant que utilisateur postgres

sudo -u postgres /usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/15/main

Si vous avez l’erreur suivante : postgres: could not access the server configuration file « /var/lib/postgresql/15/main/postgresql.conf »: No such file or directory

c’est que la base de donnée a été initialisée mais ne contient pas le fichier de configuration postgresql.conf.

Nous allons initialiser la base dans un autre répertoire

mkdir -p /opt/postgres_data
chown postgres:postgres /opt/postgres_data
sudo -u postgres /usr/lib/postgresql/15/bin/initdb -D /opt/postgres_data

Une base de données sera initialisée dans le répertoire /opt/postgres_data

Sortie de cette commande

The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "C".                                                               The default database encoding has accordingly been set to "SQL_ASCII".                                                  The default text search configuration will be set to "english".

Data page checksums are disabled.

fixing permissions on existing directory /opt/postgres_data ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default max_connections ... 100
selecting default shared_buffers ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    /usr/lib/postgresql/15/bin/pg_ctl -D /opt/postgres_data -l logfile start

Attention lorsque vous arrêtez Postgresql, la commande pour le redémarrer est différente, en effet, on fait ici (ci-dessus) un initdb, qu’on fait une seule fois, pour les fois suivantes:

// à exécuter lorsque vous êtes loggé en utilisateur refschool (mais pas root)


sudo -u postgres /usr/lib/postgresql/15/bin/postgres -D /opt/postgres_data

Pratique des commandes principales de Postgresql

Une fois que vous êtes dans le prompt de Postgresql, vous pouvez faire les commandes suivantes:

Je rappelle qu’il faut démarrer postgresql avant, vous ne pouvez le faire en root, donc switchez sur un user normal et lancez la commande suivante :

// vous êtes en user normal (non root)

sudo -u postgres /usr/lib/postgresql/15/bin/postgres -D /opt/postgres_data

//puis dans un autre shell connectez vous avec la commande psql en tant que user postgres

psql -U postgres

lister les bases de données

\l  ou \list

postgres=# \list
                                             List of databases
   Name    |  Owner   | Encoding  | Collate | Ctype | ICU Locale | Locale Provider |   Access privileges   
-----------+----------+-----------+---------+-------+------------+-----------------+-----------------------
 devdb     | devuser  | SQL_ASCII | C       | C     |            | libc            | =Tc/devuser          +
           |          |           |         |       |            |                 | devuser=CTc/devuser
 postgres  | postgres | SQL_ASCII | C       | C     |            | libc            | 
 template0 | postgres | SQL_ASCII | C       | C     |            | libc            | =c/postgres          +
           |          |           |         |       |            |                 | postgres=CTc/postgres
 template1 | postgres | SQL_ASCII | C       | C     |            | libc            | =c/postgres          +
           |          |           |         |       |            |                 | postgres=CTc/postgres
(4 rows)

Choisir une base de donnés

Nous devons comme dans tous les sytème sde base de données choisir une base pour faire des requêtes

postgres=# \c devdb
You are now connected to database "devdb" as user "postgres".

// à noter que le prompt a changé

Créer une table et lister les tables

Coller ce code

devdb=# CREATE TABLE produit (
    id SERIAL PRIMARY KEY,
    nom VARCHAR(100) NOT NULL,
    description TEXT,
    prix NUMERIC(10,2) NOT NULL,
    stock INTEGER DEFAULT 0,
    date_creation TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);


devdb=# \dt
          List of relations
 Schema |  Name   | Type  |  Owner   
--------+---------+-------+----------
 public | produit | table | postgres
(1 row)

Accessoirement pour lister les schémas dans toutes les bases de données
\dt *.*

pour sortir du mode "pager" appuyez sur la touche "q"

Exécuter une requête SQL pour insérer des données

On va insérer des données :

devdb=# INSERT INTO produit (nom, description, prix, stock)
VALUES 
('Iphone', 'Description du Iphone', 1999, 10),
('Samsung', 'Description du Samsung', 299, 5);

// afficher les données de la table avec un SELECT
// attention il faut respecter la casse

SELECT * FROM produit

Faire un dump de la base de données

On va utiliser l’utilitaire pg_dump

pg_dump -U postgres -d devdb -f sav.sql

Installer Nginx avec php-fpm

Vous qui avez l’habitude de fonctionner avec Apache, vous allez voir une alternative intéressante avec Nginx, qui peut à la différence de Apache jouer plusieurs rôles, reverse proxy, load balancer, mise en cache.

Nous allons partir d’une image docker debian de base officielle

docker pull debian
docker run -it debian

Nous aurions pu faire un dockerfile pour installer les packages nécessaire mais nous allons essayer de faire une configuration minimaliste, sans systemd ni supervisor.

Installation des packages

nous aurons besoin de curl nginx vim php-fpm etc (on découvrira au fur et à mesure)

apt update
apt install nginx php-fpm curl vim

configuration du fichier vhost de phpsite

Ce fichier est à placer dans le répertoire /etc/nginx/sites-available/

server {
    listen 80;
    server_name phpsite;

    root /var/www/phpsite;
    index index.php index.html;

location ~ \.php$ {
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass 127.0.0.1:9000;
}

la ligne include fastcgi_params est important, car permet à Nginx d’exécuter le PHP, sinon un curl vous renvoit le texte du code.

La ligne fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

Vous testez ce script avec la commande nginx -t qui vérifiera s’il n’y a pas d’erreur.

Pour rendre ce site actif il faut créer un lien symbolic dans le répertoire /etc/nginx/sites-enabled/ qui pointe vers ce fichiers de configuration de vhsot qu’on appelera phpsite.conf

sudo ln -s /etc/nginx/sites-available/phpsite.conf /etc/nginx/sites-enabled/phpsite.conf

Le fichier php info.php

cd /var/www/phpsite
touch info.php
nano info.php

<?php 
echo "Hello";
?>

Vous devez démarrer php-fpm pour prendre en charge l’exécution du php et démarrer nginx aussi

Démarrage des services pour accéder à un site en php

Démarrage de php-fpm

le fait qu’on n’utilise pas systemd ni supervisor nécessite qu’on démarre à la main php-fpm et utiliser le socket TCP et non le socket Unix

 /usr/sbin/php-fpm8.2 -F

le -F fait démarrer php-fpm en foreground.

Démarrage de Nginx

nginx -g "daemon off;"

Cette commande évite de démarrer nginx en tâche de fond, comme ça on a les logs si jamais il y a une erreur.

Erreurs et problèmes possibles

Si vous essayez d’atteindre une page php avec curl

curl http://phpsite/info.php

// n'oubliez pas d'ajouter dans le fichier /etc/hosts/ le vhost phpsite
vim /etc/hosts

// contenu du fichier

et que vous avez un erreur 502 Bad Gateway, c’est que php-fpm écoute sur un socket Unix et que Nginx est paramétré sur un socket TCP. Pour savoir quel type de configuration a php-fpm:

// commande pour savoir quel type de socket php-fpm utilise
grep -E "^listen" /etc/php/8.2/fpm/pool.d/www.conf

//Dans le fichier /etc/php/8.2/fpm/pool.d/www.conf, remplacez le socket Unix par le socket TCP.
listen = /run/php/php8.2-fpm.sock  # socket Unix
listen = 127.0.0.1:9000    # socket TCP

Dans notre cas (fonctionnement sans systemd et supervisor, on va choisir les socket TCP.)

On va donc changer la configuration de php-fpm

vim /etc/php/8.2/fpm/pool.d/www.conf 
on cherche la ligne où on a 
listen = /run/php/php8.2-fpm.sock 
pour remplacer par
listen = 127.0.0.1:9000

et on redémarre php-fpm et nginx 

PHP ne s’exécute pas mais affiche le code à la place, Nginx n’arrive pas à communiquer avec php-fpm. Vérifiez que ce dernier soit bien démarré.

cela veut dire que le fichier est traité comme un fichier texte, vérifiez que vous avez inclus

Note : Créer une nouvelle image avec ces modifications

Créer une image avec toutes ces modifications, afin de ne pas avoir à refaire les manipulations, en somme préparer une image pour un usage.

Quittez tous les process à l’intérieur du docker, quittez le docker et vérifiez que le process docker en question est stoppé avec docker ps. Ensuite on va commiter le docker avec son id selon la syntaxe suivante:

docker ps -a // pour voir l'id du container 

docker commit <nom_ou_id_du_conteneur> monimage:1.0

docker commit b425 monimage:1.0

Sauver l’image docker dans le repository distant (hub de docker)

Ensuite vous devez sauver cete image dans un repository, le plus commun est le hub de docker.com

// connexion avec le hub
docker login

// tagging de l'image avant de pouvoir l'envoyer dans le repository
docker tag debian-nginx refschool/debian-nginx:1.0
^ debian-nginx est l'image qu'on vient de créer
// push de l'image docker
docker push refschool/debian-nginx:1.0

Rappel de commandes Nginx sans Systemd et sans supervisor

  • démarrer en avant plan : nginx -g « daemon off »; »
  • test du fichier de configuration : nginx -t
  • arrêt : nginx -s stop
  • arrêt graceful : nginx -s quit (nginx arrête les connexions avant de s’arrêter
  • redémarrer : nginx -s reload
  • savoir si nginx tourne : ps aux | grep nginx

Tuto Réalisez un serveur MCP Model Context Protocol

Dans quelle situation un serveur MCP est pertinent?

Un modèle LLM comme ChatGPT est généraliste et a été entrainé sur un dataset limité bien que grand. Il ne dispose pas d’une connaissance universelle. Mais avez vous remarqué qu’une IA a toujours réponse à tous néanmoins? quitte à vous sortir des insanités? qu’on appelle des hallucinations.

Pour éviter ce phénomène d’hallucination, qui résulte d’un trou dans sa connaissance, il faut pouvoir compléter avec des données supplémentaires. Seulement voilà, le modèle de chatGPT a été entrainé déjà (et c’est ultra coûteux d’entrainer une IA, tout le monde ne dispose pas les moyens de le faire.

La solution du RAG (Retrieval Augmented Generation)

Le principe repose sur le stockage dans une base vectorielle des données, avec une base de données vectorielle comme FAISS ou ChromaDB, puis de prompter la base au lieu de prompter chatGPT, le résultat de ce prompt est envoyé à chatGPT. (prompt augmenté).

C’est une bonne solution, et vraiment adapté à certaines situation comme chatPDF. Cependant c’est un peu lourd. Nous allons voir qu’Anthropic propose une solution qui marche dans le sens inverse mais qui semble être plus légère et performante (quoique différent).

La solution MCP (Model Context Protocol)

Cette solution à base de données structurée (et c’est là la différence avec la RAG) requiert à Claude (et non chatGPT puisque qu’au moment d’écrire cet article chatGPT ne le prend pas encore en charge), de faire un sorte de requête AJAX avec un serveur MCP pour contextualiser la réponse à un prompt.

MCP ouvre tout un univers de champs d’application très intéressant, tout en préservant l’anonymat des données.

Exemple de code en Python d’un serveur MCP

Assistant RH intelligent

rh-assistant-mcp/
├── employee_db.json
├── mcp_server.py
├── mcp_manifest.json

Notre serveur MCP est simplement un serveur REST, le différence c’est ce n’est pas vous qui requêtez en AJAX mais c’est Claude qui va le requêter (on comprend mieux pourquoi les autres vendors d’IA ne l’ont pas encore implémenté)

employee_db.json est la base de données, mcp_manifest/json va décrire la façon dont les données seront formatées en input (venant de Claude) et en output (allant vers Claude).

{
  "employees": [
    {
      "id": "E001",
      "name": "Alice Dupont",
      "role": "Développeuse Backend",
      "hire_date": "2021-06-12",
      "skills": ["Python", "Django", "PostgreSQL"]
    },
    {
      "id": "E002",
      "name": "Bruno Martin",
      "role": "Data Analyst",
      "hire_date": "2022-01-20",
      "skills": ["SQL", "Power BI", "Python"]
    }
  ]
}

Le fichier manifest.json qui décrit les format d’entrée et sortie

{
  "name": "employee-context",
  "description": "Provides structured context about employees from the HR database.",
  "version": "1.0",
  "input_schema": {
    "type": "object",
    "properties": {
      "employee_name": {
        "type": "string",
        "description": "Name of the employee to look up"
      }
    },
    "required": ["employee_name"]
  },
  "output_schema": {
    "type": "object",
    "properties": {
      "employee_info": {
        "type": "object",
        "properties": {
          "role": { "type": "string" },
          "hire_date": { "type": "string" },
          "skills": { "type": "array", "items": { "type": "string" } }
        }
      }
    }
  }
}

Le fichier serveur avec FastAPI

from fastapi import FastAPI, Request
import json

app = FastAPI()

with open("employee_db.json", "r") as f:
    db = json.load(f)

@app.post("/mcp/context")
async def provide_context(request: Request):
    body = await request.json()
    name = body.get("employee_name", "").lower()

    for emp in db["employees"]:
        if emp["name"].lower() == name:
            return {
                "employee_info": {
                    "role": emp["role"],
                    "hire_date": emp["hire_date"],
                    "skills": emp["skills"]
                }
            }
    return {"employee_info": None}

Pour tester rapidement le serveur:

curl -X POST http://localhost:8000/mcp/context   -H "Content-Type: application/json"   -d '{"employee_name": "alice dupont"}'

Comment déployer ce système

Pour que Claude puisse lire le manifest.json, on peut le stocker sur une github page, ou un serveur accessible publiquement. On va notifier cette url à Claude via son interface graphique ou par API.

Le serveur REST doit être accessible publiquement, si vous le déployez en local il faut faire un tunnel vers votre serveur.

Comment utiliser ce système

Il faut que ce soit clair que dans ce cas de figure vous ne pouvez requêter Claude que par API. Quand est ce que Claude va savoir qu’il va avoir besoin de requêter le serveur MCP? C’est lui qui détermine quand il faut requêter le serveur MCP, quand il va s’apercevoir que le contexte lui manque et qu’il existe un serveur MCP décrit par le manifest.json existe.

Exemple de requête faite à Claude

vous pouvez faire en Javascript cette requête POST

POST /v1/messages
{
  "model": "claude-3-opus-20240229",
  "messages": [
    { "role": "user", "content": "Peux-tu me dire ce que fait Bruno Martin ?" }
  ],
  "context_providers": [
    { "url": "https://ton-domaine.com/mcp_manifest.json" }
  ]
}

Pour exposer le site sur un serveur il suffit de simplement déployer sur un serveur. Une alternative serait de créer un tunnel SSH pour exposer votre site local à Claude.

Quelques visuels pour évaluer le modèle

Je vous présente quelques graphique qui servent à avoir un insight dans le modèle ainsi que l’évaluation de la qualité du modèle, sa précision etc.

Confusion Matrix

Ceci graphique permet de voir el taux de faux positifs et de faux négatifs.

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix

import joblib
loaded_rf = joblib.load('random_forest_model.pkl')
#load the datas
import pickle
with open('X_test.pkl', 'rb') as f:
    X_test = pickle.load(f)
with open('y_test.pkl', 'rb') as f:
    y_test = pickle.load(f)

prediction = loaded_rf.predict(X_test)
# Create confusion matrix
cm = confusion_matrix(y_test, prediction)

# Plot confusion matrix
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', cbar=False)
plt.title('Confusion Matrix')
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.show()

Receiver Operating Characteristics

from sklearn.metrics import roc_curve, auc

# Get probability scores for the positive class
if hasattr(loaded_rf, "predict_proba"):
    proba = loaded_rf.predict_proba(X_test)[:, 1]
    fpr, tpr, _ = roc_curve(y_test, proba)
    roc_auc = auc(fpr, tpr)
    
    # Plot ROC curve
    plt.figure(figsize=(8, 6))
    plt.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (area = {roc_auc:.2f})')
    plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
    plt.xlim([0.0, 1.0])
    plt.ylim([0.0, 1.05])
    plt.xlabel('False Positive Rate')
    plt.ylabel('True Positive Rate')
    plt.title('Receiver Operating Characteristic (ROC) Curve')
    plt.legend(loc='lower right')
    plt.show()

Precision recall curve

from sklearn.metrics import precision_recall_curve, average_precision_score

# Get probabilities and calculate precision-recall
if hasattr(loaded_rf, "predict_proba"):
    proba = loaded_rf.predict_proba(X_test)[:, 1]
    precision, recall, _ = precision_recall_curve(y_test, proba)
    avg_precision = average_precision_score(y_test, proba)
    
    # Plot precision-recall curve
    plt.figure(figsize=(8, 6))
    plt.plot(recall, precision, color='blue', lw=2, label=f'Precision-Recall curve (AP = {avg_precision:.2f})')
    plt.xlabel('Recall')
    plt.ylabel('Precision')
    plt.title('Precision-Recall Curve')
    plt.legend(loc='lower left')
    plt.show()

Classification report

from sklearn.metrics import classification_report
import pandas as pd

# Get classification report as a dictionary
report = classification_report(y_test, prediction, output_dict=True)
report_df = pd.DataFrame(report).transpose()

# Plot classification report
plt.figure(figsize=(10, 6))
sns.heatmap(report_df.iloc[:-1, :].drop(['support'], axis=1), annot=True, cmap='Blues')
plt.title('Classification Report')
plt.tight_layout()
plt.show()

Feature importance

if hasattr(loaded_rf, "feature_importances_"):
    # Create a dataframe of feature importances
    feature_importance = pd.DataFrame({
        'feature': X_test.columns,
        'importance': loaded_rf.feature_importances_
    }).sort_values('importance', ascending=False)
    
    # Plot feature importances
    plt.figure(figsize=(10, 8))
    sns.barplot(x='importance', y='feature', data=feature_importance)
    plt.title('Feature Importance')
    plt.tight_layout()
    plt.show()

Overall Performance Metrics

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

accuracy = accuracy_score(y_test, prediction)
precision = precision_score(y_test, prediction, average='weighted')
recall = recall_score(y_test, prediction, average='weighted')
f1 = f1_score(y_test, prediction, average='weighted')

metrics = pd.DataFrame({
    'Metric': ['Accuracy', 'Precision', 'Recall', 'F1 Score'],
    'Value': [accuracy, precision, recall, f1]
})

# Plot metrics
plt.figure(figsize=(10, 6))
sns.barplot(x='Value', y='Metric', data=metrics, hue='Metric',palette='viridis')
plt.title('Model Performance Metrics')
plt.xlim(0, 1)
for i, v in enumerate(metrics['Value']):
    plt.text(v + 0.01, i, f'{v:.4f}', va='center')
plt.tight_layout()
plt.show()

Réutiliser un modèle Machine Learning généré

Afin de ne pas à avoir à calculer toujours le modèle pour déterminer si un client va partir ou non, on sauve le modèle entrainé et on on va le réutiliser

Charger le modèle

import joblib

#load the model
loaded_rf = joblib.load('random_forest_model.pkl')
print(loaded_rf)

Charger les données

Normalement nous chargeons de nouvelles données qu’on va donner à moudre au modèle, mais on n’en a pas, donc on va repasser les même données qu’on a sauvegardé.

#load the datas
import pickle

with open('X_test.pkl', 'rb') as f:
    X_test = pickle.load(f)
with open('y_test.pkl', 'rb') as f:
    y_test = pickle.load(f)

X_test.head()

Injection des données de test (les données de test n’ont pas servi à entrainer le modèle), on va obtenir une Series avec des 0 et 1

# Now you can use the loaded model to make predictions

from sklearn.metrics import accuracy_score
prediction = loaded_rf.predict(X_test)
#la série qui contient les outcome (churn si 1 , 0 si no churn)
print(prediction)

[0 1 1 1 0 0 0.... ]

Test sur une ligne (un client)

# test on row
# subset = X_test[X_test['TotalCharges']<100]
# extract a line from X_test
oneline = X_test.loc[[6125]]
#convert to dataframe
#oneline = oneline.to_frame()

import pandas as pd
isinstance(oneline,pd.DataFrame) # test if it's DataFrame
 
# affichage des informations de la ligne
print(oneline)
oneline.head()

#prediction sur la ligne
prediction = loaded_rf.predict(oneline)
print(prediction)

chatPDF clone sans base vectorielle

Cet exemple est plus facile à comprendre

Installer les module Python suivants

pip install PyPDF2
pip install openai
pip install tiktoken
#!/usr/bin/env python3
"""
ChatPDF Simple - Version corrigée pour OpenAI v1.0+
Permet d'extraire le texte d'un PDF et de poser des questions dessus
Sans base vectorielle
"""

import PyPDF2
from openai import OpenAI
import os
import argparse
import sys
from typing import List, Optional
import tiktoken
import re


class ChatPDF:
    def __init__(self, api_key: Optional[str] = None):
        """
        Initialise ChatPDF avec la clé API OpenAI
        """
        self.api_key = 'sk-proj-BBXFnLZzRMt8EZp4LStJXyfzc_6knvyK'
        if not self.api_key:
            raise ValueError("Clé API OpenAI requise. Définissez OPENAI_API_KEY ou passez api_key")

        self.client = OpenAI(api_key=self.api_key)
        self.pdf_content = ""
        self.pdf_path = ""
        self.encoding = tiktoken.get_encoding("cl100k_base")

    def extract_text_from_pdf(self, pdf_path: str) -> str:
        """
        Extrait tout le texte d'un fichier PDF
        """
        try:
            with open(pdf_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                text = ""

                print(f"Extraction du texte de {len(pdf_reader.pages)} pages...")

                for page_num, page in enumerate(pdf_reader.pages):
                    try:
                        page_text = page.extract_text()
                        text += f"\n--- Page {page_num + 1} ---\n{page_text}\n"
                    except Exception as e:
                        print(f"Erreur lors de l'extraction de la page {page_num + 1}: {e}")
                        continue

                self.pdf_content = text
                self.pdf_path = pdf_path
                print(f"Extraction terminée. {len(text)} caractères extraits.")
                return text

        except FileNotFoundError:
            raise FileNotFoundError(f"Fichier PDF non trouvé: {pdf_path}")
        except Exception as e:
            raise Exception(f"Erreur lors de la lecture du PDF: {e}")

    def chunk_text(self, text: str, max_tokens: int = 3000) -> List[str]:
        """
        Divise le texte en chunks pour respecter les limites de tokens
        """
        sentences = re.split(r'[.!?]+', text)
        chunks = []
        current_chunk = ""

        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue

            test_chunk = current_chunk + " " + sentence if current_chunk else sentence

            if len(self.encoding.encode(test_chunk)) <= max_tokens:
                current_chunk = test_chunk
            else:
                if current_chunk:
                    chunks.append(current_chunk)
                current_chunk = sentence

        if current_chunk:
            chunks.append(current_chunk)

        return chunks

    def ask_question(self, question: str, model: str = "gpt-3.5-turbo") -> str:
        """
        Pose une question sur le contenu du PDF
        """
        if not self.pdf_content:
            return "Aucun PDF chargé. Veuillez d'abord extraire le contenu d'un PDF."

        # Préparer le contexte
        chunks = self.chunk_text(self.pdf_content, max_tokens=3000)

        # Si le document est court, utiliser tout le contenu
        if len(chunks) == 1:
            context = chunks[0]
        else:
            # Pour des documents longs, prendre les premiers chunks
            context = " ".join(chunks[:2])  # Utiliser les 2 premiers chunks
        print(('======'))
        print(context)
        print(('======'))

        prompt = f"""
Vous êtes un assistant qui répond aux questions basées sur le contenu d'un document PDF.

Contenu du document:
{context}

Question: {question}

Instructions:
- Répondez uniquement basé sur le contenu fourni
- Si l'information n'est pas dans le document, dites-le clairement
- Citez des passages spécifiques quand c'est pertinent
- Soyez précis et concis

Réponse:"""

        try:
            response = self.client.chat.completions.create(
                model=model,
                messages=[
                    {"role": "system", "content": "Vous êtes un assistant spécialisé dans l'analyse de documents PDF."},
                    {"role": "user", "content": prompt}
                ],
                max_tokens=500,
                temperature=0.1
            )

            return response.choices[0].message.content.strip()

        except Exception as e:
            return f"Erreur lors de la génération de la réponse: {e}"

    def summarize_pdf(self) -> str:
        """
        Génère un résumé du PDF
        """
        if not self.pdf_content:
            return "Aucun PDF chargé."

        chunks = self.chunk_text(self.pdf_content, max_tokens=3000)
        context = " ".join(chunks[:3])  # Utiliser les 3 premiers chunks

        prompt = f"""
Veuillez créer un résumé concis du document suivant:

{context}

Le résumé doit:
- Capturer les points clés et idées principales
- Être structuré et facile à lire
- Faire environ 200-300 mots maximum

Résumé:"""

        try:
            response = self.client.chat.completions.create(
                model="gpt-3.5-turbo",
                messages=[
                    {"role": "system", "content": "Vous créez des résumés clairs et concis de documents."},
                    {"role": "user", "content": prompt}
                ],
                max_tokens=400,
                temperature=0.2
            )

            return response.choices[0].message.content.strip()

        except Exception as e:
            return f"Erreur lors de la génération du résumé: {e}"


def main():
    parser = argparse.ArgumentParser(description="ChatPDF Simple - Analyseur de PDF avec IA")
    parser.add_argument("pdf_path", help="Chemin vers le fichier PDF")
    parser.add_argument("--api-key", help="Clé API OpenAI (ou utilisez la variable d'environnement OPENAI_API_KEY)")
    parser.add_argument("--model", default="gpt-3.5-turbo", help="Modèle OpenAI à utiliser")

    args = parser.parse_args()

    try:
        # Initialiser ChatPDF
        chat_pdf = ChatPDF(api_key=args.api_key)

        # Extraire le texte du PDF
        print(f"Chargement du PDF: {args.pdf_path}")
        chat_pdf.extract_text_from_pdf(args.pdf_path)

        # Générer un résumé automatique
        print("\n=== RÉSUMÉ AUTOMATIQUE ===")
        summary = chat_pdf.summarize_pdf()
        print(summary)

        # Mode interactif pour les questions
        print("\n=== MODE QUESTIONS-RÉPONSES ===")
        print("Posez vos questions sur le document (tapez 'quit' pour quitter):")

        while True:
            try:
                question = input("\n❓ Votre question: ").strip()

                if question.lower() in ['quit', 'exit', 'q']:
                    break

                if not question:
                    continue

                print("🤔 Recherche de la réponse...")
                answer = chat_pdf.ask_question(question, model=args.model)
                print(f"\n💡 Réponse:\n{answer}")

            except KeyboardInterrupt:
                break
            except Exception as e:
                print(f"Erreur: {e}")

        print("\nAu revoir!")

    except Exception as e:
        print(f"Erreur: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()

# Exemple d'utilisation en tant que module
"""
from chatpdf_simple import ChatPDF

# Initialiser
chat = ChatPDF(api_key="votre_clé_api")

# Charger un PDF
chat.extract_text_from_pdf("document.pdf")

# Poser des questions
response = chat.ask_question("Quels sont les points principaux de ce document?")
print(response)

# Générer un résumé
summary = chat.summarize_pdf()
print(summary)
"""

Usage

Macos
python3 main.py  document.pdf
Windows
python main.py document.pdf

Créez un clone de chatPDF en Python et la base de données vectorielle FAISS

Si vous ne connaissez pas chatPDF, je vous recommande de le tester sur le site en uploadant un fichier pdf, et interrogez le document comme si vous interrogiez chatGPT. L’intérêt est que vous pouvez connaitre c’e qui vous intéresse du document sans le lire entièrement.

Par exemple vous souscrivez à une assurance, et il y a un long contrat en petits caractères à lire, plutôt que de perdre des heures à décortiquer le document, vous interrogez chatGPT sur le document.

Techniquement ce n’est pas comme ça. En fait vous injecter le texte du document dans la base vectorielle FAISS, vous interrogez FAISS, qui vous retournera un résultat (qui est un contexte) et vous le soumettez à chatGPT.

Vous contextualisez très précisément votre prompt.

Installation des composants

La base de données vectorielle FAISS

FAISS est un logiciel fait par Facebook, acronyme de Facebook AI Similarity Search. Elle tokenise le pdf et le stocke sous forme vectorielle.

Le parseur de PDF

PyPDF2 est un très bon parseur de pdf.

Un tokeniser

tiktoken va transformer le texte en token. Un token est une unité qui a une signification mathématique pour le LLM. tiktoken travaille de concert avec le modèle de OpenAI.

Openai

Permet d’appeler le webservice d’OpenAI facilement

Une clé API par exemple chatGPT

Il vous faudra acheter des crédits pour le webservice chatGPT, 10 euros est pas mal et vous permettra de faire beaucoup d’appels.

Le code Python

#!/usr/bin/env python3
"""
ChatPDF Clone avec Base de Données Vectorielle
Utilise des embeddings et une recherche vectorielle pour une meilleure précision
"""

import PyPDF2
from openai import OpenAI
import os
import argparse
import sys
import numpy as np
from typing import List, Optional, Dict, Tuple
import tiktoken
import re
import faiss
import pickle
from sentence_transformers import SentenceTransformer
from dataclasses import dataclass
import json


@dataclass
class TextChunk:
    text: str
    page_num: int
    chunk_id: int
    embedding: Optional[np.ndarray] = None


class VectorChatPDF:
    def __init__(self, api_key: Optional[str] = None, embedding_model: str = "all-MiniLM-L6-v2"):
        """
        Initialise ChatPDF avec base vectorielle
        """
        self.api_key = "sk-proj-BBFnLZzRMt8EZp4LStJXyfzc_6knvyKX2jOhYXIRh27MmGHIkxzcGFfcisYMS3uiRYBP0r2qT3BlbkFJObsGY8qW-VjRQNLsB9Fr-iPhSmCs0Aaj2Y8hSF34dBQQ4dEgiq7mQO30BCWDItJC775SUA"
        if not self.api_key:
            raise ValueError("Clé API OpenAI requise. Définissez OPENAI_API_KEY ou passez api_key")

        self.client = OpenAI(api_key=self.api_key)

        # Modèle d'embeddings local (gratuit)
        print("Chargement du modèle d'embeddings...")
        self.embedding_model = SentenceTransformer(embedding_model)

        # Base de données vectorielle FAISS
        self.vector_db = None
        self.chunks: List[TextChunk] = []
        self.pdf_path = ""
        self.encoding = tiktoken.get_encoding("cl100k_base")

    def extract_text_from_pdf(self, pdf_path: str) -> List[TextChunk]:
        """
        Extrait le texte du PDF et le divise en chunks
        """
        try:
            with open(pdf_path, 'rb') as file:
                pdf_reader = PyPDF2.PdfReader(file)
                chunks = []
                chunk_id = 0

                print(f"Extraction du texte de {len(pdf_reader.pages)} pages...")

                for page_num, page in enumerate(pdf_reader.pages):
                    try:
                        page_text = page.extract_text()
                        if page_text.strip():
                            # Diviser la page en chunks
                            page_chunks = self._split_text_into_chunks(page_text, max_tokens=500)
                            for chunk_text in page_chunks:
                                chunk = TextChunk(
                                    text=chunk_text,
                                    page_num=page_num + 1,
                                    chunk_id=chunk_id
                                )
                                chunks.append(chunk)
                                chunk_id += 1

                    except Exception as e:
                        print(f"Erreur lors de l'extraction de la page {page_num + 1}: {e}")
                        continue

                self.chunks = chunks
                self.pdf_path = pdf_path
                print(f"Extraction terminée. {len(chunks)} chunks créés.")
                return chunks

        except FileNotFoundError:
            raise FileNotFoundError(f"Fichier PDF non trouvé: {pdf_path}")
        except Exception as e:
            raise Exception(f"Erreur lors de la lecture du PDF: {e}")

    def _split_text_into_chunks(self, text: str, max_tokens: int = 500) -> List[str]:
        """
        Divise le texte en chunks optimaux pour les embeddings
        """
        # Nettoyer le texte
        text = re.sub(r'\s+', ' ', text).strip()

        # Diviser par phrases
        sentences = re.split(r'[.!?]+', text)
        chunks = []
        current_chunk = ""

        for sentence in sentences:
            sentence = sentence.strip()
            if not sentence:
                continue

            test_chunk = current_chunk + " " + sentence if current_chunk else sentence

            if len(self.encoding.encode(test_chunk)) <= max_tokens:
                current_chunk = test_chunk
            else:
                if current_chunk:
                    chunks.append(current_chunk)
                current_chunk = sentence

        if current_chunk:
            chunks.append(current_chunk)

        return [chunk for chunk in chunks if len(chunk.strip()) > 20]  # Filtrer les chunks trop courts

    def create_embeddings(self):
        """
        Crée les embeddings pour tous les chunks et construit l'index FAISS
        """
        if not self.chunks:
            raise ValueError("Aucun chunk disponible. Extrayez d'abord le texte du PDF.")

        print("Création des embeddings...")
        texts = [chunk.text for chunk in self.chunks]

        # Créer les embeddings par batch pour l'efficacité
        embeddings = self.embedding_model.encode(texts, show_progress_bar=True)

        # Associer les embeddings aux chunks
        for i, chunk in enumerate(self.chunks):
            chunk.embedding = embeddings[i]

        # Créer l'index FAISS
        dimension = embeddings.shape[1]
        self.vector_db = faiss.IndexFlatIP(dimension)  # Index par produit scalaire (cosine similarity)

        # Normaliser les vecteurs pour la similarité cosine
        normalized_embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
        self.vector_db.add(normalized_embeddings.astype('float32'))

        print(f"Index vectoriel créé avec {len(self.chunks)} embeddings.")

    def search_similar_chunks(self, query: str, top_k: int = 5) -> List[Tuple[TextChunk, float]]:
        """
        Recherche les chunks les plus similaires à la requête
        """
        if not self.vector_db:
            raise ValueError("Index vectoriel non créé. Appelez create_embeddings() d'abord.")

        # Créer l'embedding de la requête
        query_embedding = self.embedding_model.encode([query])
        query_embedding = query_embedding / np.linalg.norm(query_embedding)

        # Rechercher les chunks similaires
        scores, indices = self.vector_db.search(query_embedding.astype('float32'), top_k)

        results = []
        for score, idx in zip(scores[0], indices[0]):
            if idx < len(self.chunks):  # Vérification de sécurité
                results.append((self.chunks[idx], float(score)))

        return results

    def ask_question(self, question: str, model: str = "gpt-3.5-turbo", top_k: int = 5) -> Dict:
        """
        Pose une question en utilisant la recherche vectorielle
        """
        if not self.vector_db:
            return {"error": "Index vectoriel non créé. Appelez create_embeddings() d'abord."}

        # Rechercher les chunks pertinents
        similar_chunks = self.search_similar_chunks(question, top_k=top_k)

        if not similar_chunks:
            return {"error": "Aucun contenu pertinent trouvé."}

        # Construire le contexte à partir des chunks les plus pertinents
        context_parts = []
        source_info = []

        for chunk, score in similar_chunks:
            context_parts.append(f"[Page {chunk.page_num}] {chunk.text}")
            source_info.append({
                "page": chunk.page_num,
                "chunk_id": chunk.chunk_id,
                "similarity_score": score,
                "text_preview": chunk.text[:100] + "..." if len(chunk.text) > 100 else chunk.text
            })

        context = "\n\n".join(context_parts)

        prompt = f"""
Vous êtes un assistant expert qui répond aux questions basées sur le contenu d'un document PDF.

Contexte pertinent extrait du document:
{context}

Question: {question}

Instructions:
- Répondez uniquement basé sur le contexte fourni
- Si l'information n'est pas suffisante, dites-le clairement
- Citez les numéros de page quand c'est pertinent
- Soyez précis et structuré dans votre réponse

Réponse:"""

        try:
            response = self.client.chat.completions.create(
                model=model,
                messages=[
                    {"role": "system",
                     "content": "Vous êtes un assistant spécialisé dans l'analyse de documents PDF avec une expertise en recherche d'information."},
                    {"role": "user", "content": prompt}
                ],
                max_tokens=800,
                temperature=0.1
            )

            answer = response.choices[0].message.content.strip()

            return {
                "answer": answer,
                "sources": source_info,
                "context_used": len(similar_chunks)
            }

        except Exception as e:
            return {"error": f"Erreur lors de la génération de la réponse: {e}"}

    def save_index(self, filepath: str):
        """
        Sauvegarde l'index vectoriel et les métadonnées
        """
        if not self.vector_db:
            raise ValueError("Aucun index à sauvegarder.")

        # Sauvegarder l'index FAISS
        faiss.write_index(self.vector_db, f"{filepath}.faiss")

        # Sauvegarder les métadonnées
        metadata = {
            "chunks": [
                {
                    "text": chunk.text,
                    "page_num": chunk.page_num,
                    "chunk_id": chunk.chunk_id
                } for chunk in self.chunks
            ],
            "pdf_path": self.pdf_path
        }

        with open(f"{filepath}.json", 'w', encoding='utf-8') as f:
            json.dump(metadata, f, ensure_ascii=False, indent=2)

        print(f"Index sauvegardé: {filepath}.faiss et {filepath}.json")

    def load_index(self, filepath: str):
        """
        Charge un index vectoriel sauvegardé
        """
        try:
            # Charger l'index FAISS
            self.vector_db = faiss.read_index(f"{filepath}.faiss")

            # Charger les métadonnées
            with open(f"{filepath}.json", 'r', encoding='utf-8') as f:
                metadata = json.load(f)

            # Reconstruire les chunks
            self.chunks = []
            for chunk_data in metadata["chunks"]:
                chunk = TextChunk(
                    text=chunk_data["text"],
                    page_num=chunk_data["page_num"],
                    chunk_id=chunk_data["chunk_id"]
                )
                self.chunks.append(chunk)

            self.pdf_path = metadata["pdf_path"]
            print(f"Index chargé: {len(self.chunks)} chunks disponibles.")

        except FileNotFoundError:
            raise FileNotFoundError(f"Fichiers d'index non trouvés: {filepath}.faiss ou {filepath}.json")


def main():
    parser = argparse.ArgumentParser(description="ChatPDF avec Base Vectorielle")
    parser.add_argument("pdf_path", help="Chemin vers le fichier PDF")
    parser.add_argument("--api-key", help="Clé API OpenAI")
    parser.add_argument("--model", default="gpt-3.5-turbo", help="Modèle OpenAI à utiliser")
    parser.add_argument("--save-index", help="Sauvegarder l'index vectoriel")
    parser.add_argument("--load-index", help="Charger un index vectoriel existant")
    parser.add_argument("--top-k", type=int, default=5, help="Nombre de chunks à récupérer")

    args = parser.parse_args()

    try:
        # Initialiser ChatPDF vectoriel
        chat_pdf = VectorChatPDF(api_key=args.api_key)

        if args.load_index:
            # Charger un index existant
            print(f"Chargement de l'index: {args.load_index}")
            chat_pdf.load_index(args.load_index)
        else:
            # Traiter un nouveau PDF
            print(f"Chargement du PDF: {args.pdf_path}")
            chat_pdf.extract_text_from_pdf(args.pdf_path)
            chat_pdf.create_embeddings()

            if args.save_index:
                chat_pdf.save_index(args.save_index)

        # Mode interactif
        print("\n=== MODE QUESTIONS-RÉPONSES VECTORIEL ===")
        print("Posez vos questions sur le document (tapez 'quit' pour quitter):")
        print("Tapez 'stats' pour voir les statistiques de l'index")

        while True:
            try:
                question = input("\n❓ Votre question: ").strip()

                if question.lower() in ['quit', 'exit', 'q']:
                    break

                if question.lower() == 'stats':
                    print(f"📊 Statistiques:")
                    print(f"   - Nombre de chunks: {len(chat_pdf.chunks)}")
                    print(f"   - Pages: {max(c.page_num for c in chat_pdf.chunks) if chat_pdf.chunks else 0}")
                    print(f"   - Document: {os.path.basename(chat_pdf.pdf_path)}")
                    continue

                if not question:
                    continue

                print("🔍 Recherche vectorielle en cours...")
                result = chat_pdf.ask_question(question, model=args.model, top_k=args.top_k)

                if "error" in result:
                    print(f"❌ {result['error']}")
                else:
                    print(f"\n💡 Réponse:\n{result['answer']}")
                    print(f"\n📚 Sources utilisées ({result['context_used']} chunks):")
                    for i, source in enumerate(result['sources'][:3], 1):
                        print(f"   {i}. Page {source['page']} (score: {source['similarity_score']:.3f})")
                        print(f"      {source['text_preview']}")

            except KeyboardInterrupt:
                break
            except Exception as e:
                print(f"Erreur: {e}")

        print("\nAu revoir!")

    except Exception as e:
        print(f"Erreur: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()

Lancer le programme en ligne de commande

lancement du script en mode normal, cela va créer un fichier binaire mon_index.faiss qui est la base vectorielle

python3 avecBaseVectorielle.py document.pdf --save-index mon_index

La même chose en plus léger

python3 avecBaseVectorielle.py document.pdf --fast --save-index mon_index

La même chose avec un LLM personnalisé

python3 avecBaseVectorielle.py document.pdf --embedding-model "paraphrase-MiniLM-L3-v2"

La même chose en chargeant une base vectorielle déjà calculée

python3 avecBaseVectorielle.py document.pdf --load-index mon_index

Lors du lancement du script en mode normal, une base vectorielle va être créée. Vous allez poser une question qui va requêter sur cette base de données. Par exemple, « Combien de jours de formation y a t il dans cette formation? », la requête ne va pas partir vers chatGPT directement mais va extraire un contexte de la base vectorielle, qui va être mis dans le prompt vers chatGPT.

Sans le vouloir vraiment, vous venez de faire du RAG, Retrieval Augmented Generation, autrement dit intelligence générative augmentée par une extraction de données.

Modèle d’embedding c’est quoi?

Un modèle d’embedding est un système qui transforme du texte (mots, phrases, paragraphes) en vecteurs numériques de dimension fixe. Ces vecteurs capturent le sens sémantique du texte de manière à ce que des textes similaires aient des représentations vectorielles proches dans l’espace mathématique.

Comment ça fonctionne

Les modèles d’embedding utilisent généralement des réseaux de neurones entraînés sur de grandes quantités de texte pour apprendre les relations sémantiques. Par exemple, les mots « chien » et « animal » auront des vecteurs plus proches que « chien » et « voiture ».

Utilité pratique

Ces représentations vectorielles permettent de :

  • Mesurer la similarité sémantique entre textes
  • Effectuer des recherches par similarité
  • Regrouper des documents par thème
  • Alimenter des systèmes de question-réponse

Le modèle « paraphrase-MiniLM-L3-v2 » est un modèle d’embedding compact et efficace développé par Sentence Transformers. Il est particulièrement adapté pour :

  • Identifier des paraphrases (textes exprimant la même idée différemment)
  • Créer des bases de données vectorielles pour la recherche sémantique

Le mode –fast

C’est u mode d’optimisation qui accélère le traitement en faisant certains compromis.

Le mode fast améliore généralement la vitesse au détriment de :

  • La précision de la recherche sémantique
  • La qualité des réponses
  • La finesse de l’analyse du document

Supervisor pour Linux, un système de controle de process

Supervisor est un programme qui manage l’exécution des autres programmes. Il démarre, arrête ou redémarre un programme en cas de crash, et monitore leur status.

Ce que fait Supervisor:

Process management :

Au boot de Linux, il peut démarrer plusieurs programmes, et les garde en exécution continuellement.

Redémarrage automatique

Supervisor peut redémarrer un process qui a crashé

Logging :

Enregistre dans un fichier de logs tous les événements

Monitoring :

Vous pouvez checker le status de chaque programme managé via une interface web, ou en ligne de commande.

Dans ce tutoriel sur le load balancing, on a un container Docker qui fait tourner plusieurs process, un process Nginx, et 3 process python. Il n’est peut être pas un système très stable, et un process peut s’arrêter, donc il faudrait le redémarrer manuellement. D’où l’utilisation de Supervisor qui nous garantit que les process vont redémarrer automatique.

Le Supervisor démarre en premier, et ensuite démarre les autres process en lisant le fichier ci-dessous

[supervisord]
nodaemon=true
user=root

[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/nginx_err.log
stdout_logfile=/var/log/supervisor/nginx_out.log

[program:python-server-1]
command=python3 /app/app.py 8001
directory=/app
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/python1_err.log
stdout_logfile=/var/log/supervisor/python1_out.log

[program:python-server-2]
command=python3 /app/app.py 8002
directory=/app
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/python2_err.log
stdout_logfile=/var/log/supervisor/python2_out.log

[program:python-server-3]
command=python3 /app/app.py 8003
directory=/app
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/python3_err.log
stdout_logfile=/var/log/supervisor/python3_out.log

Tous les process sont monitorés par Supervisor.

Que risque t on si on n’a pas Supervisor?

Voici ce qui pourrit se passer si on n’a pas de Supervisor qui fait du redémarrage automatique, du logging, du monitoring :

  1. Défaillance du conteneur suite à la défaillance d’un seul processus
    Sans Supervisor, si votre processus principal s’arrête, le conteneur entier s’arrête. Par exemple :

Si vous démarrez uniquement Nginx, vous n’avez pas de backend Python.
Si vous démarrez un seul serveur Python, vous n’avez pas d’équilibrage de charge.
Si vous essayez de démarrer plusieurs processus avec des scripts shell et que l’un d’eux plante, les autres peuvent continuer à s’exécuter, mais vous perdez la supervision.

  1. Absence de récupération automatique
    En cas de panne d’un serveur Python (due à des bugs, des problèmes de mémoire, etc.) :

Avec Supervisor : Redémarrage automatique du serveur en panne en quelques secondes.
Sans Supervisor : Le serveur reste inactif, réduisant votre capacité de 3 à 2 serveurs, puis à 1, puis à 0.

  1. Processus d’arrière-plan peu fiables
    Si vous essayez de démarrer plusieurs processus avec des scripts shell, comme :
bashpython3 app.py 8001 &
python3 app.py 8002 &
python3 app.py 8003 &
nginx -g "daemon off;" Risques :

Les processus d’arrière-plan peuvent devenir orphelins.
Si le processus nginx principal meurt, le conteneur s’arrête, mais les processus Python peuvent continuer à s’exécuter comme des zombies.
Il est impossible de vérifier facilement si les processus d’arrière-plan sont réellement en cours d’exécution.
La gestion des journaux devient un cauchemar.

Pannes silencieuses
Sans surveillance des processus :

Vous ne saurez pas quand un serveur back-end tombe en panne.
Nginx tentera sans cesse de rediriger le trafic vers des serveurs inactifs.
Les utilisateurs reçoivent des erreurs 502/503, sans que vous sachiez pourquoi.
Absence de journalisation centralisée pour déboguer les problèmes.

  1. Difficultés opérationnelles

Déploiement : Difficile de garantir le bon démarrage de tous les processus.
Débogage : Impossible de déterminer facilement le processus à l’origine des problèmes.
Mise à l’échelle : Impossible d’ajouter ou de supprimer facilement des serveurs back-end.
Contrôles de santé : Impossible de vérifier par programmation si tous les services sont en cours d’exécution.

Tuto : Load balancer avec Nginx dans un unique container Docker

Ce tuto concerne la mise en place à titre d’illustration d’un load balancer pour en comprendre les principes, dans un seul container docker. Dans ce container docker, il ya aura Linux Debian et tous les programmes nécessaires, dont Nginx bien sûr, mais aussi Supervisor. En effet faire tourner dans un docker Nginx et plusieurs instances de serveur n’est pas très compatible, puisque Docker n’assure qu’un process, dans le cas du load balancer on a Nginx et trois instances de serveur.

Supervisor permet de gérer à l’instar de SystemD les process, c’est un programme qui manage les autres programmes.

On se procure l’image Docker de Debian Linux officiel

docker pull debian
docker run -it debian

Ensuite on installe les programmes nécessaire pour faire tourner le load balancer

// ici on installe dans le docker les programmes nécessaire
// mise à jour de la BDD des packages
apt update


apt install nginx python3 supervisor  curl

Configuration du fichier supervisord.conf

Tout d’abord nous allons créer deux répertoires, un pour stocker le fichier de configuration, et un pour les logs

mkdir -p /etc/supervisor/conf.d/
mkdir -p /var/log/supervisor/
// créons le fichier de configuration qui n'existe pas au début
nano /etc/supervisor/conf.d/supervisord.conf
[supervisord]
nodaemon=true
user=root

[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/nginx_err.log
stdout_logfile=/var/log/supervisor/nginx_out.log

[program:python-server-1]
command=python3 /app/app.py 8001
directory=/app
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/python1_err.log
stdout_logfile=/var/log/supervisor/python1_out.log

[program:python-server-2]
command=python3 /app/app.py 8002
directory=/app
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/python2_err.log
stdout_logfile=/var/log/supervisor/python2_out.log

[program:python-server-3]
command=python3 /app/app.py 8003
directory=/app
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/python3_err.log
stdout_logfile=/var/log/supervisor/python3_out.log

Le fichier de Supervisor démarre les 3 serveurs python, nous allons faire le script de serveur Python

Script Python du serveur

// Création du répertoire de l'application

mkdir -p /app
nano /app/app.py

// code du fichier serveur minimal

#!/usr/bin/env python3
import http.server
import socketserver
import sys

# Get port from command line argument
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8001

class MyHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        response = f"""
        <h1>Hello from Server {PORT}</h1>
        <p>Server ID: {PORT}</p>
        <p>Path: {self.path}</p>
        """
        self.wfile.write(response.encode())

if __name__ == '__main__':
    with socketserver.TCPServer(("", PORT), MyHandler) as httpd:
        print(f"Server running on port {PORT}")
        httpd.serve_forever()

Configuration de Nginx

events {
    worker_connections 1024;
}

http {
    # Define upstream servers (Python backends)
    upstream backend {
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
        server 127.0.0.1:8003;
        
        # Load balancing method options:
        # least_conn;     # Route to server with fewest active connections
        # ip_hash;        # Route based on client IP (sticky sessions)
        # Default is round-robin
    }
    
    # Health check configuration
    server {
        listen 80;
        
        location / {
            proxy_pass http://backend;
            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;
            
            # Connection and timeout settings
            proxy_connect_timeout 5s;
            proxy_send_timeout 60s;
            proxy_read_timeout 60s;
            
            # Health check
            proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
        }
        
        # Health check endpoint
        location /health {
            access_log off;
            return 200 "OK\n";
            add_header Content-Type text/plain;
        }
    }
}

Lancement du serveur Nginx

comme on n'a pas systemd on lance en direct depuis Supervisor

sudo supervisord -c /etc/supervisor/conf.d/supervisord.conf

Normalement tous les services sont lancés, et vous pouvez faire un curl pour tester le serveur frontal

curl http://localhost

<h1>Hello from Server 8001</h1>
        <p>Server ID: 8001</p>
        <p>Path: /</p>


curl http://localhost

<h1>Hello from Server 8002</h1>
        <p>Server ID: 8002</p>
        <p>Path: /</p>

curl http://localhost

<h1>Hello from Server 8003</h1>
        <p>Server ID: 8003</p>
        <p>Path: /</p>

curl http://localhost

<h1>Hello from Server 8001</h1>
        <p>Server ID: 8001</p>
        <p>Path: /</p>

Là nous voyons quelque chose de très intéressant, on requête la même url, mais ce n’est pas le même serveur qui nous retourne la réponse. Vous y êtes !

Et maintenant?

Là nous avons un seul container docker, c’est de l’expérience de laboratoire. Le docker est là pour des raisons de convénience, nonobstant la plateforme (mac, linux ou Windows) vous pouvez faire les mêmes commandes.

Vous pouvez faire ce tuto dans WSL, dans Linux. Cependant voyons quels scénarios nous pouvons explorer

-Load balancing vers des serveurs dans d’autres containers docker au sein d’une même plateforme.

-load balancing vers des serveurs par leurs urls

ff

Configuration Nginx comme load balancer

ON a vu comment mettre Nginx en reverse proxy, nous aller plus loin en configurant un load balancer

Qu’est ce qu’un load balancer?

Un load balancer permet de répartir la charge d’un site web sur plusieurs serveur qui tournent indépendamment. Un serveur web qui reçoit beaucoup de visite aura tendance à ralentir, ce n’est pas la résolution de la ressource qui est lente, car l’opération demande peu de ressource, mais le travail du serveur pour calculer la page web. D’où l’idée de monter plusieurs serveurs et de monter un reverse proxy qui va être le premier à intercepter la requête et à la rediriger vers un des serveurs.

Installation de Nginx

sudo apt install -y nginx

Fichier de configuration de Nginx

Le fichier de configuration se trouve dans /etc/nginx/nginx.conf

server {

listen 80;
server_name   myserver;

location /static {
    root /var/www/myserver;

}

location / {
    proxy_pass  http://localhost:8000;
    proxy_http_version. 1.1;
    proxy_set_header X-Forwarder-Host $server_name;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

}

}

Modification du fichier de configuration pour indiquer les 3 hôtes

log_format upstreamlog '$server_name to: $upstream_addr [$request] '
    'upstream_response_time $upstream_response_time'
    'msec $msec

upstream serveurs {
    server http://localhost:8000;
    server http://localhost:8001;
    server http://localhost:8002;

}


server {

    listen 80;
    server_name   myserver;
    access_log    /var/log/nginx/access.log  upstreamlog;

location /static {
    root /var/www/myserver;

}

location / {
    proxy_pass  http://serveurs;
    proxy_http_version 1.1;
    proxy_set_header X-Forwarder-Host $server_name;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

}

}

Dry run ou lancement d’essai pour vérifier que le script n’est pas en erreur

sudo nginx -t 

// pas d'erreur normalement


sudo systemctl reload nginx


sudo tail -f /var/www/nginx/access_log

Maintenant dans votre navigateur, allez à l’adresse http://myserver, et rechargez plusieurs fois. Regardez les logs, on doit voir les 3 port du localhost utilisés.

La stratégie round robin

Avec cette stratégie, le reverse proxy distribue à tout le monde équitablement, chacun son tour.

On va faire une application NodeJS qu’on va lancer trois fois avec des ports différents


		

Comment fonctionne le protocol FTP?

En 2025, je vois que de plus en plus de monde ne connait plus le protocole FTP. Les apprenants savent déployer vers Vercel ou Heroku, mais sont incapables de faire un simple FTP. Lorsque le web a pris son envol un peu avant l’an 2000, les webmaster(c’est comme ça qu’on les appelait à l’époque) déplayaient le site en FTP, et aujourd’hui ça fait un peu rigoler les jeunes quand je parle de ça.

Ce n’est pas l’objet de cet article, je vais vous parler de FTP dans sa globalité.

FTP (File Transfert Protocole)

Le FTP a été inventé en 1971, et servait à transférer des fichiers entre deux ordinateurs, plutôt entre un client et le serveur, qui est à la base de l’architecture FTP. En effet quand vous allez sur la page de Filezilla pour télécharger le client FTP, vous avez aussi la possibilité de télécharger les serveurs. Si vous êtes webmaster vous avez besoin de la version client.

Le client FTP se connecte au serveur FTP, il faut souvent un identifiant et un mot de passe, un fois la connexion établie, un échange de données et de commandes se fait entre les deux entités.

Deux ports sont ouverts de part et d’autres pour la communication, le port 20 pour les données et le port 21 pour les commandes. Avant d’avoir un client graphique tel que Filezilla, le FTP se pratiquait en ligne de commande.

Le langage FTP

Voici une liste succincte de commandes FTP que vous devez faire depuis votre client pour manipuler des fichiers. Pour se connecter à FTP en ligne de commande depuis le terminal vous faites ftp nom_hote, ce dernier peut être une adresse IP, après la connexion réussie, vous aurez une invite de commande de type ftp>. Ceci indique que vous êtes déjà sur le serveur.

  • cd change de répertoire sur le serveur
  • lcd change de répertoire sur le client
  • get : télécharge un fichier du serveur vers le client
  • mget : télécharge plusieurs fichiers (wildcards acceptés)
  • put : envoi un fichier vers le serveur
  • mput : envoi multiple fichiers vers le serveur
  • prompt : active ou désactive la confirmation lors de transfert de fichier, utile en multiple
  • delete
  • mkdir : crée un répertoire sur le serveur
  • rmdir : efface un répertoire sur le serveur
  • rename : renomme un fichier
  • help ou ? : affiche la liste des commandes disponibles
  • status : affiche les paramètres actuels

Les commandes brutes FTP

  • CWD : change de répertoire, cd est un wrapper sur CWD
  • STOR : wrapper sur put
  • RETR : wrapper sur get
  • LIST : wrapper sur ls

Les canaux de communication FTP 20 et 21 (channels)

FTP utilise deux canaux de communication :

Control channel : port 21

Ce canal est utilisé pour envoyer les commandes

Data channel : port 20

Ce canal est utilisé pour transmettre les données proprement dites.

Mode actif ou mode passif (pour le serveur)

Dans les logiciels comme Filezilla, la configuration vous propose une connexion passive ou active. Il faut savoir que l’on se place dans le contexte du serveur et non du client..

Le mode actif (Active Mode)

Mode par défaut du FTP, le client envoit par le canal de control sur quel port se connecter, et le serveur répond en se connectant à ce port pour le Data Channel

Le mode passif (Passive mode)

le serveur indique au client quel port auquel se connecter, et le client se connecte à ce port.

Pourquoi il existe deux modes de connexions?

Le parefeu (firewall) est la raison pour laquelle il y a les deux modes.

Problème avec le mode actif :

les serveur essaye de se connecter au port du client, mais le parefeu du client peut être configuré de façon à bloquer le port 21.

Solution avec le mode passif :

Le client se connecte au port du serveur que ce dernier a spécifié. Le pare feu du serveur normalement ne bloque pas le port puisqu’il est censé être configuré pour permettre les connexion.

Dans un cas comme l’autre des problème d’autorisation peuvent se produire, et il convient de tester les solutions pour établir la connexion.

FTP et le modèle OSI à 7 couches

Le fameux modèle à 7 couches, vraiment très abstrait, mais je vais essayer de vous expliquer.

C’est quoi le modèle à 7 couches ?

Le FTP est sur la couche Application, car elle sert directement les application et les utilisateurs. Est ce que le FTP est sur la couche. Par exemple pourquoi ne dit on pas que FTP est sur la couche Transport? La couche Transport s’occupe de découper et réassembler les paquets de données, or FTP ne s’occupe pas du tout de ça. FTP délègue à la couche Transport (TCP Transport Layer Protocol) le soin de le faire.

De même peut on dire que FTP est dans la couche Presentation? La couche Presentation s’occupe de l’encryption, de la compression/decompression, FTP ne s’occupe pas du fait que l’image qu’il transfère soit JPG ou PNG, il transfère juste.

De même est ce que FTP est dans la couche Session? On peut le penser car il y une authentification,et le maintient de la connexion de façon stateful, c’est à dire que le contact est maintenu. Pour autant FTP n’est pas dans la couche Session même si elle gère elle même par dessus TCP l’authentification et le maintient de la connexion.

Retour en haut