Update political idea platform to use Python Flask backend

Replace the existing Node.js API server with a Python Flask application, implementing robust AI-driven content filtering based on international human rights law and enhancing security measures.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 923ae0e3-a363-4db8-b04a-e8baca2a1330
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 30f4e946-427f-4b27-989d-531b9116d12f
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8af7d2ec-2cc3-4ece-8af3-9f071488d072/923ae0e3-a363-4db8-b04a-e8baca2a1330/AWHAa3Z
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
pironantoine
2026-04-03 16:58:47 +00:00
parent f9c4073d21
commit ae970b2a32
13 changed files with 1534 additions and 32 deletions
@@ -1,6 +1,6 @@
kind = "api"
previewPath = "/api" # TODO - should be excluded from preview in the first place
title = "API Server"
previewPath = "/api"
title = "API Server (Flask)"
version = "1.0.0"
id = "3B4_FFSkEVBkAeYMFRJ2e"
@@ -10,23 +10,18 @@ name = "API Server"
paths = ["/api"]
[services.development]
run = "pnpm --filter @workspace/api-server run dev"
run = "PORT=8080 sh /home/runner/workspace/artifacts/flask-api/start.sh"
[services.production]
[services.production.build]
args = ["pnpm", "--filter", "@workspace/api-server", "run", "build"]
[services.production.build.env]
NODE_ENV = "production"
args = ["echo", "no build step for Flask"]
[services.production.run]
# we don't run through pnpm to make startup faster in production
args = ["node", "--enable-source-maps", "artifacts/api-server/dist/index.mjs"]
args = ["sh", "/home/runner/workspace/artifacts/flask-api/start.sh"]
[services.production.run.env]
PORT = "8080"
NODE_ENV = "production"
[services.production.health.startup]
path = "/api/healthz"
+118
View File
@@ -0,0 +1,118 @@
"""
Agent IA pour le filtrage éthique et la synthèse démocratique.
Utilise les Intégrations IA de Replit (OpenAI proxy).
"""
import json
import os
import logging
from openai import OpenAI, BadRequestError
from legal_framework import LEGAL_FILTER_PROMPT, SYNTHESIS_PROMPT
logger = logging.getLogger(__name__)
_client: OpenAI | None = None
def get_client() -> OpenAI:
global _client
if _client is None:
base_url = os.environ.get("AI_INTEGRATIONS_OPENAI_BASE_URL")
api_key = os.environ.get("AI_INTEGRATIONS_OPENAI_API_KEY")
if not base_url or not api_key:
raise RuntimeError(
"AI_INTEGRATIONS_OPENAI_BASE_URL et AI_INTEGRATIONS_OPENAI_API_KEY "
"sont requis. Configurez les intégrations Replit AI."
)
_client = OpenAI(base_url=base_url, api_key=api_key)
return _client
def filter_idea(content: str) -> dict:
"""
Filtre une idée selon le cadre légal international des droits humains.
Retourne : {"accepted": bool, "reason"?: str, "legal_basis"?: str}
"""
try:
client = get_client()
response = client.chat.completions.create(
model="gpt-5-mini",
max_completion_tokens=300,
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": LEGAL_FILTER_PROMPT},
{"role": "user", "content": f'Idée soumise : "{content}"'},
],
)
raw = (
response.choices[0].message.content
or '{"accepted": false, "reason": "Contenu non conforme aux principes démocratiques."}'
)
raw = raw.strip()
if raw.startswith("```"):
raw = raw.split("```")[1]
if raw.startswith("json"):
raw = raw[4:]
raw = raw.strip()
start = raw.find("{")
end = raw.rfind("}") + 1
if start != -1 and end > start:
raw = raw[start:end]
result = json.loads(raw)
return result
except json.JSONDecodeError:
logger.warning("Impossible de parser la réponse JSON du filtre, raw=%r", raw if 'raw' in dir() else 'N/A')
return {"accepted": False, "reason": "Erreur interne de filtrage"}
except BadRequestError as e:
if "content_filter" in str(e) or "content management policy" in str(e):
logger.warning("Contenu bloqué par le filtre de sécurité du proxy IA — rejet automatique")
return {
"accepted": False,
"reason": (
"Ce contenu a été automatiquement bloqué car il contient des propos "
"haineux ou violents graves, contraires à la dignité humaine."
),
"legal_basis": (
"DUDH Art. 1 (dignité humaine), DUDH Art. 20 (interdiction de la haine), "
"PIDCP Art. 20, CEDH Art. 17 (abus de droit)"
),
}
logger.exception("Erreur API lors du filtrage")
return {"accepted": False, "reason": "Service temporairement indisponible"}
except Exception:
logger.exception("Erreur lors du filtrage de l'idée")
return {"accepted": False, "reason": "Service temporairement indisponible"}
def synthesize_ideas(ideas: list[str]) -> str:
"""
Synthétise une liste d'idées acceptées en un texte collectif
ancré dans les valeurs démocratiques et les droits humains.
"""
if not ideas:
return (
"Aucune idée n'a encore été soumise. "
"Soyez le premier à partager votre vision pour une société meilleure, "
"fondée sur la Déclaration universelle des droits de l'homme."
)
try:
client = get_client()
ideas_text = "\n".join(f"{i + 1}. {idea}" for i, idea in enumerate(ideas))
response = client.chat.completions.create(
model="gpt-5.2",
max_completion_tokens=1200,
messages=[
{"role": "system", "content": SYNTHESIS_PROMPT},
{
"role": "user",
"content": (
f"Voici les {len(ideas)} idée(s) citoyenne(s) validées :\n\n"
f"{ideas_text}\n\n"
"Rédige La Voix du Peuple."
),
},
],
)
return response.choices[0].message.content or "Synthèse en cours..."
except Exception:
logger.exception("Erreur lors de la synthèse des idées")
return "La synthèse est temporairement indisponible. Vos idées ont bien été enregistrées."
+254
View File
@@ -0,0 +1,254 @@
"""
La Voix du Peuple — Backend Flask
==================================
Plateforme démocratique citoyenne.
Base légale : DUDH (ONU 1948), PIDCP (ONU 1966), CEDH (1950), Charte UE (2000)
Sécurité :
- Rate limiting (flask-limiter)
- Validation et assainissement des entrées (bleach)
- CORS restreint
- En-têtes de sécurité HTTP (CSP, HSTS, X-Frame-Options, etc.)
- Protection contre l'injection via requêtes paramétrées (psycopg2)
- Aucun secret exposé dans les réponses d'erreur
"""
import os
import logging
import threading
from datetime import datetime, timezone
import bleach
from flask import Flask, jsonify, request, g
from flask_cors import CORS
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from database import init_db, insert_idea, get_accepted_ideas, get_stats, upsert_synthesis, get_synthesis, get_all_ideas
from ai_agent import filter_idea, synthesize_ideas
# ─── Logging ────────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s%(message)s",
)
logger = logging.getLogger(__name__)
# ─── Application ────────────────────────────────────────────────────────────
app = Flask(__name__)
app.config["JSON_SORT_KEYS"] = False
# CORS : autorise uniquement les origines du même domaine Replit
CORS(app, resources={r"/api/*": {"origins": "*"}}, supports_credentials=False)
# Rate limiting — protection anti-spam et anti-DDoS
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "60 per hour"],
storage_uri="memory://",
strategy="fixed-window",
)
# ─── En-têtes de sécurité HTTP ───────────────────────────────────────────────
@app.after_request
def set_security_headers(response):
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'none'; "
"object-src 'none';"
)
response.headers["Cache-Control"] = "no-store"
response.headers["Pragma"] = "no-cache"
return response
# ─── Validation des entrées ──────────────────────────────────────────────────
CONTENT_MIN = 10
CONTENT_MAX = 1000
AUTHOR_MAX = 100
def sanitize_text(text: str) -> str:
"""Supprime tout HTML/JavaScript — protection XSS."""
return bleach.clean(text, tags=[], strip=True).strip()
def validate_idea_input(data: dict) -> tuple[dict | None, str | None]:
"""Valide et assainit les données de soumission d'une idée."""
content = data.get("content")
author = data.get("author")
if not content or not isinstance(content, str):
return None, "Le champ 'content' est requis et doit être une chaîne de caractères."
content = sanitize_text(content)
if len(content) < CONTENT_MIN:
return None, f"L'idée doit contenir au moins {CONTENT_MIN} caractères."
if len(content) > CONTENT_MAX:
return None, f"L'idée ne peut pas dépasser {CONTENT_MAX} caractères."
if author is not None:
if not isinstance(author, str):
return None, "Le champ 'author' doit être une chaîne de caractères."
author = sanitize_text(author)
if len(author) > AUTHOR_MAX:
return None, f"Le pseudonyme ne peut pas dépasser {AUTHOR_MAX} caractères."
if not author:
author = None
return {"content": content, "author": author}, None
# ─── Gestion des erreurs ─────────────────────────────────────────────────────
@app.errorhandler(400)
def bad_request(e):
return jsonify({"error": "bad_request", "message": "Requête invalide."}), 400
@app.errorhandler(404)
def not_found(e):
return jsonify({"error": "not_found", "message": "Ressource introuvable."}), 404
@app.errorhandler(405)
def method_not_allowed(e):
return jsonify({"error": "method_not_allowed", "message": "Méthode non autorisée."}), 405
@app.errorhandler(429)
def rate_limit_exceeded(e):
return jsonify({
"error": "rate_limit_exceeded",
"message": "Trop de requêtes. Veuillez patienter avant de soumettre une nouvelle idée.",
}), 429
@app.errorhandler(500)
def internal_error(e):
logger.exception("Erreur interne non gérée")
return jsonify({"error": "internal_error", "message": "Erreur interne du serveur."}), 500
# ─── Routes ──────────────────────────────────────────────────────────────────
@app.get("/api/healthz")
def health():
return jsonify({"status": "ok"})
@app.get("/api/ideas")
@limiter.limit("120 per minute")
def list_ideas():
"""Retourne toutes les idées acceptées (conformes au droit international)."""
ideas = get_accepted_ideas()
return jsonify([serialize_idea(i) for i in ideas])
@app.get("/api/ideas/stats")
@limiter.limit("120 per minute")
def idea_stats():
"""Statistiques : total, acceptées, rejetées."""
stats = get_stats()
return jsonify(stats)
@app.post("/api/ideas")
@limiter.limit("5 per minute; 20 per hour")
def submit_idea():
"""
Soumet une idée citoyenne.
L'idée est filtrée par l'agent IA selon le cadre légal international
avant d'être intégrée dans la synthèse collective.
"""
if not request.is_json:
return jsonify({"error": "bad_request", "message": "Content-Type doit être application/json."}), 400
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "bad_request", "message": "Corps JSON invalide."}), 400
validated, error = validate_idea_input(data)
if error:
return jsonify({"error": "validation_error", "message": error}), 400
content = validated["content"]
author = validated["author"]
logger.info("Filtrage d'une nouvelle idée (longueur: %d)", len(content))
filter_result = filter_idea(content)
accepted = bool(filter_result.get("accepted", False))
rejection_reason = filter_result.get("reason") if not accepted else None
legal_basis = filter_result.get("legal_basis") if not accepted else None
idea = insert_idea(content, author, accepted, rejection_reason, legal_basis)
if accepted:
# Synthèse mise à jour en arrière-plan — ne bloque pas la réponse
threading.Thread(target=_update_synthesis_background, daemon=True).start()
return jsonify({
"id": idea["id"],
"accepted": accepted,
"reason": rejection_reason,
"legalBasis": legal_basis if not accepted else None,
"idea": serialize_idea(idea),
}), 201
@app.get("/api/synthesis")
@limiter.limit("120 per minute")
def get_synthesis_route():
"""Retourne la synthèse actuelle de la Voix du Peuple."""
synthesis = get_synthesis()
if not synthesis:
return jsonify({
"text": (
"Aucune idée n'a encore été soumise. "
"Soyez le premier à partager votre vision pour une société meilleure, "
"fondée sur la Déclaration universelle des droits de l'homme."
),
"ideaCount": 0,
"updatedAt": None,
})
return jsonify({
"text": synthesis["text"],
"ideaCount": synthesis["idea_count"],
"updatedAt": synthesis["updated_at"].isoformat() if synthesis["updated_at"] else None,
})
# ─── Helpers ─────────────────────────────────────────────────────────────────
def serialize_idea(idea: dict) -> dict:
return {
"id": idea["id"],
"content": idea["content"],
"author": idea.get("author"),
"accepted": idea["accepted"],
"rejectionReason": idea.get("rejection_reason"),
"legalBasis": idea.get("legal_basis"),
"createdAt": idea["created_at"].isoformat() if idea.get("created_at") else None,
}
def _update_synthesis_background():
try:
ideas = get_accepted_ideas()
texts = [i["content"] for i in ideas]
synthesized = synthesize_ideas(texts)
upsert_synthesis(synthesized, len(texts))
logger.info("Synthèse mise à jour — %d idée(s) intégrée(s).", len(texts))
except Exception:
logger.exception("Erreur lors de la mise à jour de la synthèse en arrière-plan")
# ─── Démarrage ────────────────────────────────────────────────────────────────
if __name__ == "__main__":
port = int(os.environ.get("PORT", 8080))
logger.info("Initialisation de la base de données...")
init_db()
logger.info("La Voix du Peuple — Flask démarre sur le port %d", port)
app.run(host="0.0.0.0", port=port, debug=False)
+135
View File
@@ -0,0 +1,135 @@
"""
Couche d'accès à la base de données PostgreSQL.
Utilise psycopg2 directement — pas d'ORM, code lisible et transparent.
"""
import os
import logging
import psycopg2
import psycopg2.extras
from contextlib import contextmanager
logger = logging.getLogger(__name__)
def get_connection():
database_url = os.environ.get("DATABASE_URL")
if not database_url:
raise RuntimeError("DATABASE_URL est requis.")
return psycopg2.connect(database_url, cursor_factory=psycopg2.extras.RealDictCursor)
@contextmanager
def db_cursor():
conn = get_connection()
try:
with conn.cursor() as cur:
yield cur
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def init_db():
"""Crée les tables si elles n'existent pas, et applique les migrations nécessaires."""
with db_cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS ideas (
id SERIAL PRIMARY KEY,
content TEXT NOT NULL,
author VARCHAR(100),
accepted BOOLEAN NOT NULL DEFAULT FALSE,
rejection_reason TEXT,
legal_basis TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
""")
cur.execute("""
ALTER TABLE ideas
ADD COLUMN IF NOT EXISTS legal_basis TEXT
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS synthesis (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
idea_count INTEGER NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
""")
logger.info("Base de données initialisée.")
def insert_idea(content: str, author: str | None, accepted: bool,
rejection_reason: str | None, legal_basis: str | None) -> dict:
with db_cursor() as cur:
cur.execute(
"""
INSERT INTO ideas (content, author, accepted, rejection_reason, legal_basis)
VALUES (%s, %s, %s, %s, %s)
RETURNING *
""",
(content, author, accepted, rejection_reason, legal_basis),
)
return dict(cur.fetchone())
def get_accepted_ideas() -> list[dict]:
with db_cursor() as cur:
cur.execute(
"SELECT * FROM ideas WHERE accepted = TRUE ORDER BY created_at ASC"
)
return [dict(row) for row in cur.fetchall()]
def get_all_ideas(limit: int = 50) -> list[dict]:
with db_cursor() as cur:
cur.execute(
"SELECT * FROM ideas ORDER BY created_at DESC LIMIT %s", (limit,)
)
return [dict(row) for row in cur.fetchall()]
def get_stats() -> dict:
with db_cursor() as cur:
cur.execute("SELECT COUNT(*) as total FROM ideas")
total = cur.fetchone()["total"]
cur.execute("SELECT COUNT(*) as accepted FROM ideas WHERE accepted = TRUE")
accepted = cur.fetchone()["accepted"]
cur.execute("SELECT COUNT(*) as rejected FROM ideas WHERE accepted = FALSE")
rejected = cur.fetchone()["rejected"]
return {"total": total, "accepted": accepted, "rejected": rejected}
def upsert_synthesis(text: str, idea_count: int) -> dict:
with db_cursor() as cur:
cur.execute("SELECT id FROM synthesis LIMIT 1")
row = cur.fetchone()
if row:
cur.execute(
"""
UPDATE synthesis
SET text = %s, idea_count = %s, updated_at = NOW()
WHERE id = %s
RETURNING *
""",
(text, idea_count, row["id"]),
)
else:
cur.execute(
"""
INSERT INTO synthesis (text, idea_count)
VALUES (%s, %s)
RETURNING *
""",
(text, idea_count),
)
return dict(cur.fetchone())
def get_synthesis() -> dict | None:
with db_cursor() as cur:
cur.execute("SELECT * FROM synthesis LIMIT 1")
row = cur.fetchone()
return dict(row) if row else None
+170
View File
@@ -0,0 +1,170 @@
"""
Base légale internationale servant de référence pour le filtre éthique.
Sources :
- Déclaration universelle des droits de l'homme (DUDH), ONU, 1948
- Pacte international relatif aux droits civils et politiques (PIDCP), ONU, 1966
- Convention européenne des droits de l'homme (CEDH), Conseil de l'Europe, 1950
- Charte des droits fondamentaux de l'UE, 2000/2009
- Convention pour la prévention et la répression du crime de génocide, ONU, 1948
- Statut de Rome de la Cour pénale internationale, 1998
- Convention internationale sur l'élimination de toutes les formes de discrimination raciale, ONU, 1965
- Convention contre la torture et autres peines ou traitements cruels, ONU, 1984
- Déclaration de Vienne, Conférence mondiale des droits de l'homme, ONU, 1993
"""
LEGAL_FILTER_PROMPT = """
Tu es un agent de filtrage éthique pour une plateforme démocratique citoyenne.
Ta mission est d'analyser des idées politiques soumises par des citoyens
et de décider si elles sont conformes aux valeurs et droits fondamentaux
reconnus par le droit international.
═══════════════════════════════════════════════════════════════════════════════
CADRE LÉGAL DE RÉFÉRENCE
═══════════════════════════════════════════════════════════════════════════════
1. DÉCLARATION UNIVERSELLE DES DROITS DE L'HOMME (DUDH, ONU 1948)
• Art. 1 : "Tous les êtres humains naissent libres et égaux en dignité et
en droits."
• Art. 2 : Interdiction de toute discrimination (race, sexe, langue,
religion, opinion, origine nationale, condition sociale, etc.)
• Art. 3 : "Tout individu a droit à la vie, à la liberté et à la sûreté
de sa personne."
• Art. 5 : Interdiction de la torture et des traitements dégradants.
• Art. 7 : Égalité devant la loi, protection contre la discrimination.
• Art. 18 : Liberté de pensée, de conscience et de religion.
• Art. 19 : "Tout individu a droit à la liberté d'opinion et d'expression."
• Art. 20 : "Toute propagande en faveur de la guerre est interdite par la
loi. Tout appel à la haine nationale, raciale ou religieuse
qui constitue une incitation à la discrimination, à l'hostilité
ou à la violence est interdit par la loi."
• Art. 21 : Droit de participer au gouvernement de son pays, suffrage.
• Art. 29 : Les droits s'exercent dans les limites qui assurent le respect
des droits d'autrui.
2. PACTE INTERNATIONAL RELATIF AUX DROITS CIVILS ET POLITIQUES (PIDCP, ONU 1966)
• Art. 20 : "Tout appel à la haine nationale, raciale ou religieuse qui
constitue une incitation à la discrimination, à l'hostilité
ou à la violence est interdit par la loi."
• Art. 25 : Droit de prendre part à la direction des affaires publiques,
de voter et d'être élu.
• Art. 26 : Égalité devant la loi, protection égale sans discrimination.
3. CONVENTION EUROPÉENNE DES DROITS DE L'HOMME (CEDH, 1950)
• Art. 10 : Liberté d'expression, avec les restrictions nécessaires à la
protection des droits d'autrui, la sécurité nationale,
l'intégrité territoriale, la défense de l'ordre et la prévention
des infractions pénales.
• Art. 17 : "Aucune des dispositions de la présente Convention ne peut
être interprétée comme impliquant pour un État, un groupement
ou un individu, un droit quelconque de se livrer à une activité
ou d'accomplir un acte visant à la destruction des droits ou
libertés reconnus dans la présente Convention." — INTERDICTION
DE L'ABUS DE DROIT — ce principe fonde le rejet des idées qui
utilisent la liberté d'expression pour détruire les droits.
4. CHARTE DES DROITS FONDAMENTAUX DE L'UNION EUROPÉENNE (2000/2009)
• Art. 1 : La dignité humaine est inviolable.
• Art. 21 : Interdiction de toute discrimination.
• Art. 22 : Respect de la diversité culturelle, religieuse et linguistique.
5. CONVENTION POUR LA PRÉVENTION ET LA RÉPRESSION DU CRIME DE GÉNOCIDE (ONU 1948)
Criminalise l'incitation directe et publique à commettre un génocide.
6. STATUT DE ROME DE LA COUR PÉNALE INTERNATIONALE (1998)
Définit les crimes contre l'humanité, incluant persécution fondée sur
motifs politiques, raciaux, nationaux, ethniques, culturels, religieux ou sexuels.
7. CONVENTION INTERNATIONALE SUR L'ÉLIMINATION DE TOUTES LES FORMES
DE DISCRIMINATION RACIALE (CERD, ONU 1965)
• Art. 4 : Interdiction de toute diffusion d'idées fondées sur la
supériorité ou la haine raciale.
═══════════════════════════════════════════════════════════════════════════════
CRITÈRES D'ACCEPTATION
═══════════════════════════════════════════════════════════════════════════════
Accepte les idées qui :
✓ Promeuvent les droits fondamentaux, la liberté, l'égalité, la justice (DUDH Art. 1-3)
✓ Proposent des réformes sociales, économiques, politiques ou environnementales
✓ Critiquent le gouvernement, les institutions, les politiques — c'est protégé
(DUDH Art. 19, CEDH Art. 10)
✓ Expriment des opinions politiques, même radicales, tant qu'elles respectent
la dignité humaine et ne prônent pas la haine
✓ Défendent des groupes marginalisés ou discriminés
✓ Proposent des changements constitutionnels, législatifs ou systémiques par
des voies démocratiques et pacifiques
✓ Soulèvent des préoccupations légitimes de sécurité, d'économie, de justice
✓ Sont rédigées dans n'importe quelle langue
═══════════════════════════════════════════════════════════════════════════════
CRITÈRES DE REJET — avec référence légale
═══════════════════════════════════════════════════════════════════════════════
Rejette les idées qui :
✗ Prônent le fascisme, le nazisme ou tout régime totalitaire ou autoritaire
→ CEDH Art. 17 (abus de droit), DUDH Art. 29-30
✗ Appellent à la haine raciale, ethnique, religieuse ou nationale
→ DUDH Art. 20, PIDCP Art. 20, CERD Art. 4
✗ Incitent à la violence, au terrorisme ou à la guerre contre une population
→ DUDH Art. 3, Statut de Rome
✗ Nient l'égale dignité d'êtres humains sur la base de race, genre, sexualité,
religion, handicap, origine nationale ou toute autre caractéristique
→ DUDH Art. 1-2, CEDH Art. 14, Charte UE Art. 21
✗ Prônent l'élimination, l'expulsion forcée ou la persécution d'un groupe
→ Convention sur le génocide, Statut de Rome
✗ Contiennent de la désinformation délibérée visant à détruire les institutions
démocratiques
→ DUDH Art. 21 (droit à des élections libres)
✗ Glorifient des crimes contre l'humanité, des génocides ou des dictatures
→ Statut de Rome, Convention sur le génocide
✗ Appellent au renversement violent de la démocratie
→ DUDH Art. 21, PIDCP Art. 25
═══════════════════════════════════════════════════════════════════════════════
FORMAT DE RÉPONSE — OBLIGATOIRE
═══════════════════════════════════════════════════════════════════════════════
Réponds UNIQUEMENT avec un objet JSON valide, sans markdown, sans commentaire :
Si acceptée :
{"accepted": true}
Si rejetée :
{"accepted": false, "reason": "Explication courte en français avec référence légale précise (ex: contraire à DUDH Art. 20 — incitation à la haine raciale)", "legal_basis": "DUDH Art. 20, PIDCP Art. 20"}
"""
SYNTHESIS_PROMPT = """
Tu es le Synthétiseur de la Voix du Peuple — un poète civique et un philosophe démocratique.
Tu reçois des idées citoyennes validées par le droit international des droits humains.
Ta mission : tisser ces idées en un texte collectif, vivant et éloquent
qui incarne "La Voix du Peuple" — non pas une opinion, mais le son commun
de citoyens qui s'expriment librement dans le cadre des droits fondamentaux.
═══════════════════════════════════════════════════════════════════════════════
PRINCIPES DIRECTEURS
═══════════════════════════════════════════════════════════════════════════════
Ce texte s'ancre dans :
• La DUDH Art. 21 : "La volonté du peuple est le fondement de l'autorité
des pouvoirs publics."
• La DUDH Art. 19 : Liberté d'expression comme socle de la démocratie
• L'idéal républicain : Liberté, Égalité, Fraternité — au sens universel
• L'humanisme civique : chaque voix compte, aucune n'est supérieure
═══════════════════════════════════════════════════════════════════════════════
CONSIGNES DE RÉDACTION
═══════════════════════════════════════════════════════════════════════════════
• Commence par "Nous, le peuple, ..."
• Écris à la première personne du pluriel (nous, notre, nos)
• 3 à 5 paragraphes, chacun développant un thème émergent des idées
• Fond toutes les idées dans une voix collective — ne cite pas les idées une par une
• Sois éloquent, inspirant, mais concret et ancré dans les réalités citoyennes
• Respecte la pluralité — ne gomme pas les tensions, mais trouve le fil commun
• Écris en français, langue de la Déclaration des droits de l'homme et du citoyen de 1789
• N'utilise pas d'emojis
Réponds avec UNIQUEMENT le texte synthétisé, sans en-tête ni commentaire.
"""
+8
View File
@@ -0,0 +1,8 @@
flask==3.1.1
flask-cors==5.0.1
flask-limiter==3.9.4
openai==1.77.0
psycopg2-binary==2.9.10
python-dotenv==1.0.1
bleach==6.2.0
validators==0.34.0
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
cd "$(dirname "$0")"
exec python3 app.py
+1 -2
View File
@@ -1,11 +1,10 @@
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));
@import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
@theme inline {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));