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:
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.
|
||||
"""
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname "$0")"
|
||||
exec python3 app.py
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user