Chatm / app.py
Docfile's picture
Update app.py
60b2cb1 verified
raw
history blame
12.9 kB
import os
import json
from flask import Flask, render_template, request, session, redirect, url_for, flash
from dotenv import load_dotenv
import google.generativeai as genai
import requests
from werkzeug.utils import secure_filename
import mimetypes
load_dotenv()
app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('FLASK_SECRET_KEY', 'une-clé-secrète-par-défaut-pour-dev')
UPLOAD_FOLDER = 'temp'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg'}
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# --- Configuration Gemini (inchangée) ---
try:
genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
safety_settings = [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
]
model = genai.GenerativeModel(
'gemini-2.0-flash',
safety_settings=safety_settings,
system_instruction="Tu es un assistant intelligent. ton but est d'assister au mieux que tu peux. tu as été créé par Aenir et tu t'appelles Mariam"
)
print("Modèle Gemini chargé.")
except Exception as e:
print(f"Erreur lors de la configuration de Gemini : {e}")
model = None
# --- Fonctions Utilitaires (inchangées) ---
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def perform_web_search(query):
conn_key = "9b90a274d9e704ff5b21c0367f9ae1161779b573"
if not conn_key:
print("Clé API SERPER manquante dans .env")
return None
search_url = "https://google.serper.dev/search"
headers = { 'X-API-KEY': conn_key, 'Content-Type': 'application/json' }
payload = json.dumps({"q": query, "gl": "fr", "hl": "fr"}) # Ajout localisation FR
try:
response = requests.post(search_url, headers=headers, data=payload, timeout=10)
response.raise_for_status()
data = response.json()
print("Résultats de recherche obtenus.")
return data
except requests.exceptions.RequestException as e:
print(f"Erreur lors de la recherche web : {e}")
return None
except json.JSONDecodeError as e:
print(f"Erreur lors du décodage JSON de Serper : {e}")
print(f"Réponse reçue : {response.text}")
return None
def format_search_results(data):
if not data: return "Aucun résultat de recherche trouvé."
result = "Résultats de recherche web pertinents :\n"
# (Formatage légèrement simplifié pour clarté)
if kg := data.get('knowledgeGraph'):
result += f"\n## {kg.get('title', '')} ({kg.get('type', '')})\n{kg.get('description', '')}\n"
if ab := data.get('answerBox'):
result += f"\n## Réponse rapide :\n{ab.get('title','')}\n{ab.get('snippet') or ab.get('answer','')}\n"
if org := data.get('organic'):
result += "\n## Principaux résultats :\n"
for i, item in enumerate(org[:3], 1):
result += f"{i}. {item.get('title', 'N/A')}\n {item.get('snippet', 'N/A')}\n [{item.get('link', '#')}]\n"
# People Also Ask peut être bruyant, on peut l'omettre pour le prompt
return result
def prepare_gemini_history(chat_history):
gemini_history = []
for message in chat_history:
role = 'user' if message['role'] == 'user' else 'model'
# Utilise la référence stockée si elle existe
parts = [message.get('gemini_file')] if message.get('gemini_file') else []
parts.append(message['text_for_gemini']) # Utilise le texte destiné à Gemini
# Filtrer les parts None qui pourraient survenir si gemini_file était None
gemini_history.append({'role': role, 'parts': [p for p in parts if p]})
return gemini_history
# --- Routes Flask ---
@app.route('/', methods=['GET'])
def index():
if 'chat_history' not in session:
session['chat_history'] = []
if 'web_search' not in session:
session['web_search'] = False
# Récupérer l'état de traitement et l'erreur pour les afficher
processing = session.get('processing', False)
error = session.pop('error', None) # Utilise pop pour ne l'afficher qu'une fois
return render_template(
'index.html',
chat_history=session.get('chat_history', []),
web_search_active=session.get('web_search', False),
error=error,
processing_message=processing # Passer l'état de traitement
)
@app.route('/chat', methods=['POST'])
def chat():
if not model:
session['error'] = "Le modèle Gemini n'a pas pu être chargé."
return redirect(url_for('index'))
prompt = request.form.get('prompt', '').strip()
session['web_search'] = 'web_search' in request.form
file = request.files.get('file')
uploaded_gemini_file = None
file_display_name = None # Pour l'affichage
# Marquer le début du traitement DANS la session
session['processing'] = True
session['processing_web_search'] = False # Reset au début
session.modified = True # Sauvegarder la session maintenant
if not prompt and not file:
session['error'] = "Veuillez entrer un message ou uploader un fichier."
session['processing'] = False # Annuler le traitement
return redirect(url_for('index'))
# --- Gestion Upload ---
if file and file.filename != '':
if allowed_file(file.filename):
try:
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
print(f"Fichier sauvegardé: {filepath}")
mime_type = mimetypes.guess_type(filepath)[0] or 'application/octet-stream'
print("Upload vers Gemini...")
gemini_file_obj = genai.upload_file(path=filepath, mime_type=mime_type)
uploaded_gemini_file = gemini_file_obj
file_display_name = filename # Garder le nom pour affichage
print(f"Fichier {filename} uploadé. MimeType: {mime_type}")
# Optionnel: Supprimer après upload
# os.remove(filepath)
except Exception as e:
print(f"Erreur upload fichier : {e}")
session['error'] = f"Erreur lors du traitement du fichier : {e}"
# Ne pas arrêter le traitement, continuer sans le fichier si erreur upload
else:
session['error'] = "Type de fichier non autorisé."
session['processing'] = False # Annuler le traitement
return redirect(url_for('index'))
# --- Préparation Message Utilisateur ---
# Texte à afficher dans le chat
display_text = prompt
if file_display_name:
display_text = f"[Fichier joint : {file_display_name}]\n\n{prompt}" if prompt else f"[Fichier joint : {file_display_name}]"
# Texte à envoyer à Gemini (peut être différent si recherche web)
text_for_gemini = prompt
# Ajouter à l'historique de session (pour affichage) SEULEMENT s'il y a du contenu
if display_text:
# Garder une trace de l'objet fichier Gemini et du texte original pour l'API
session['chat_history'].append({
'role': 'user',
'text': display_text, # Texte pour affichage HTML
'text_for_gemini': text_for_gemini, # Texte initial pour l'API Gemini
'gemini_file': uploaded_gemini_file # Référence à l'objet fichier si uploadé
})
session.modified = True
# --- Logique principale (Recherche Web + Gemini) ---
try:
final_prompt_parts = []
if uploaded_gemini_file:
final_prompt_parts.append(uploaded_gemini_file)
# Recherche Web si activée ET prompt textuel existe
if session['web_search'] and prompt:
print("Activation recherche web...")
session['processing_web_search'] = True # Indiquer que la recherche est active
session.modified = True
# !! Important: Forcer la sauvegarde de session AVANT l'appel bloquant
# Ceci est un workaround car Flask sauvegarde normalement en fin de requête.
# Pour une vraie MAJ live, il faudrait AJAX/WebSockets.
from flask.sessions import SessionInterface
app.session_interface.save_session(app, session, Response()) # Pseudo-réponse
web_results = perform_web_search(prompt)
if web_results:
formatted_results = format_search_results(web_results)
text_for_gemini = f"Question originale: {prompt}\n\n{formatted_results}\n\nRéponds à la question originale en te basant sur ces informations et ta connaissance."
print("Prompt enrichi avec recherche web.")
else:
print("Pas de résultats web ou erreur.")
text_for_gemini = prompt # Garde le prompt original
session['processing_web_search'] = False # Recherche terminée
# Ajouter le texte (original ou enrichi) aux parts
if text_for_gemini: # S'assurer qu'on ajoute pas une string vide
final_prompt_parts.append(text_for_gemini)
# Préparer l'historique pour Gemini en utilisant les données stockées
# On prend tout sauf le dernier message utilisateur qui est en cours de traitement
gemini_history = prepare_gemini_history(session['chat_history'][:-1])
print(f"\n--- Envoi à Gemini ({len(gemini_history)} hist + {len(final_prompt_parts)} new parts) ---")
# Appel API Gemini
if not final_prompt_parts:
# Cas où seul un fichier a été envoyé sans prompt textuel,
# et la recherche web n'était pas activée ou n'a rien retourné.
# Il faut quand même envoyer qqchose, par ex., demander de décrire le fichier.
if uploaded_gemini_file:
final_prompt_parts.append("Décris le contenu de ce fichier.")
else:
# Ne devrait pas arriver vu les checks précédents, mais par sécurité
raise ValueError("Tentative d'envoyer une requête vide à Gemini.")
full_conversation = gemini_history + [{'role': 'user', 'parts': final_prompt_parts}]
response = model.generate_content(full_conversation)
# --- Traitement Réponse ---
response_text = response.text
print(f"--- Réponse Gemini reçue ---")
# Ajouter la réponse à l'historique (version simple pour affichage)
session['chat_history'].append({
'role': 'assistant',
'text': response_text,
'text_for_gemini': response_text # Pour symétrie, même si on ne réutilise pas directement
# Pas de 'gemini_file' pour les réponses du modèle
})
session.modified = True
except Exception as e:
print(f"Erreur lors de l'appel à Gemini ou traitement : {e}")
session['error'] = f"Une erreur s'est produite : {e}"
# En cas d'erreur, retirer le dernier message utilisateur de l'historique
# pour éviter boucle d'erreur si le prompt est problématique.
if session['chat_history'] and session['chat_history'][-1]['role'] == 'user':
session['chat_history'].pop()
session.modified = True
finally:
# Marquer la fin du traitement DANS la session
session['processing'] = False
session.pop('processing_web_search', None) # Nettoyer au cas où
session.modified = True
# Pas besoin de Response() ici, la sauvegarde se fera avec le redirect
return redirect(url_for('index'))
# Ajouter une route pour effacer la conversation
@app.route('/clear', methods=['POST'])
def clear_chat():
session.pop('chat_history', None)
session.pop('web_search', None)
session.pop('processing', None) # Nettoyer aussi l'état de process
session.pop('error', None)
print("Historique de chat effacé.")
flash("Conversation effacée.", "info") # Message feedback (optionnel)
return redirect(url_for('index'))
# Classe Response factice pour sauvegarde session précoce (workaround)
# Attention: N'est PAS une vraie réponse HTTP. A utiliser avec prudence.
class Response:
def __init__(self):
self.headers = {}
def set_cookie(self, key, value, **kwargs):
# Potentiellement stocker les cookies si nécessaire, mais ici on ignore
pass
if __name__ == '__main__':
# Utiliser un port différent si 5000 est pris
app.run(debug=True, host='0.0.0.0', port=5002)