ok les recherche internret ne marche pas très bien...
Créé le : 12 février 2025
Répondu en utilisant O3 Mini High par Chat01
Créé le : 12 février 2025
Répondu en utilisant O3 Mini High par Chat01
ok les recherche internret ne marche pas très bien cela ne vas pas dans le contexte montre le code complet corrigé :
"""
from gevent import monkey
monkey.patch_all()
import os
import re
import json
import logging
from typing import Any, Dict, List, Optional, Tuple, Generator
import time
import requests
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import Flask, request, Response, jsonify, stream_with_context
from flask_cors import CORS
from groq import Groq
from e2b_code_interpreter import Sandbox
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
import httpx # Importez httpx pour vérifier le type d'exception
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(name)
app = Flask(name)
CORS(app)
GROQ_API_KEY = os.environ.get("GROQ_API_KEY")
E2B_API_KEY = os.environ.get("E2B_API_KEY")
PISTON_API_URL = "https://emkc.org/api/v2/piston/execute" # URL de l'API Piston
WHOOGLE_INSTANCE_URL = "https://chatapp-1-u8t2.onrender.com" # URL de votre instance Whoogle
groq_client = Groq(api_key=GROQ_API_KEY)
sandbox = Sandbox(api_key=E2B_API_KEY, timeout=600)
VALID_MODELS = [
"llama-3.3-70b-versatile","distil-whisper-large-v3-en", "gemma2-9b-it", "llama-3.1-8b-instant",
"llama-guard-3-8b", "llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "whisper-large-v3",
"whisper-large-v3-turbo", "deepseek-r1-distill-llama-70b-specdec", "deepseek-r1-distill-llama-70b",
"llama-3.3-specdec", "llama-3.2-1b-preview", "llama-3.2-3b-preview", "llama-3.2-11b-vision-preview",
"llama-3.2-90b-vision-preview", "llama3-70b-8192"
]
CODE_PATTERN = re.compile(r"python\s*([\s\S]*?)
")
CODE_REQUIRED_MARKER = "[CODE_REQUIRED]"
CANVAS_START_MARKER = "[CANVAS_START]"
CANVAS_END_MARKER = "[CANVAS_END]"
MAX_CONTEXT_CHARS_PER_PAGE = 2000
MAX_TOTAL_CONTEXT_CHARS = 7000
WHOOGLE_CACHE: Dict[str, str] = {}
http_session = requests.Session()
http_session.headers.update({"User-Agent": "PlinkoChatBot/1.0"})
def operate_requests(prompt) -> Generator[Dict[str, str], None, Tuple[str, List[str]]]:
"""
Fonction regroupant toutes les opérations :
1. Recherche sur Whoogle.
2. Limitation à 7 liens.
3. Extraction et résumé du contenu de chaque page (en parallèle).
4. Retourne le contexte obtenu et la liste des sources.
"""
# Utilisation de la clé API Groq définie globalement
global GROQ_API_KEY # Déclarer que nous utilisons la variable globale (optionnel ici, mais bonne pratique)
groq_client = Groq(api_key=GROQ_API_KEY)
textdef whoogle_search(query): """ Effectue une recherche via Whoogle et retourne une liste de liens. """ base_url = WHOOGLE_INSTANCE_URL + "/search" # Utilisation de WHOOGLE_INSTANCE_URL params = {"q": query} headers = {"User-Agent": "Mozilla/5.0"} try: response = requests.get(base_url, params=params, headers=headers) response.raise_for_status() except requests.exceptions.RequestException as e: print(f"Erreur lors de la requête Whoogle : {e}") return [] soup = BeautifulSoup(response.text, "html.parser") # Extraire uniquement les liens commençant par "http" links = [result["href"] for result in soup.find_all("a", href=True) if result["href"].startswith("http")] return links @retry(stop=stop_after_attempt(5), # Réessayer jusqu'à 5 fois wait=wait_exponential(multiplier=1, min=4, max=60), # Délai exponentiel (min 4s, max 60s) retry=retry_if_exception_type(httpx.HTTPError)) # Réessayer seulement pour les erreurs HTTP (incluant 429) def summarize_with_groq(query, text_content): """ Utilise l'API Groq pour extraire les informations importantes du texte. Retry avec tenacity en cas d'erreur 429. """ try: response = groq_client.chat.completions.create( model="qwen-2.5-32b", # Modèle Groq pour la synthèse (peut être configurable) messages=[ { "role": "system", "content": ( "Vous êtes un assistant expert en extraction d'informations. " "Votre tâche est de lire le texte fourni et d'extraire les informations les plus importantes " "qui répondent à la question de recherche suivante : '{}'. " "Créez une note concise résumant les points clés et les informations pertinentes. " "Soyez précis et factuel." ).format(query) }, { "role": "user", "content": text_content[:5000] # Limite du texte pour gérer le contexte } ], temperature=0.2 # Température basse pour des réponses factuelles ) return response.choices[0].message.content.strip() except httpx.HTTPError as e: # Intercepter explicitement httpx.HTTPError pour tenacity if e.response.status_code == 429: logger.warning(f"Erreur 429 (Too Many Requests) lors de l'appel à l'API Groq (summarize_with_groq). Réessai en cours... Détails: {e}") raise e # Raise pour que tenacity gère le retry else: # Pour les autres erreurs HTTP, logger et laisser remonter logger.error(f"Erreur HTTP lors de l'appel à l'API Groq (summarize_with_groq) (non-429). Détails: {e}") raise # Raise pour signaler l'erreur except Exception as e: logger.error(f"Erreur inattendue lors de l'appel à l'API Groq (summarize_with_groq). Détails: {e}") return "Erreur lors de la synthèse avec Groq." def fetch_and_summarize(link, query): """ Récupère le contenu d'une page et utilise Groq pour en extraire une note résumée. """ try: r = requests.get(link, timeout=20) # Timeout pour éviter les blocages r.raise_for_status() soup_page = BeautifulSoup(r.text, 'html.parser') text_content = soup_page.get_text(separator='\n', strip=True) print(f"{link} : OK - Extraction du contenu...") summary_note = summarize_with_groq(query, text_content) # Appel à la fonction retryable print(f"{link} : OK - Note Groq ajoutée.") return f"Source: {link}\nNote:\n{summary_note}\n\n" except requests.exceptions.RequestException: print(f"{link} : Échec de la récupération de la page.") return f"Source: {link}\nNote:\nErreur de récupération de la page.\n\n" except Exception as e: print(f"{link} : Erreur lors du traitement de la page : {e}") return f"Source: {link}\nNote:\nErreur lors du traitement de la page: {e}\n\n" def fetch_pages_and_summarize(links, query): """ Utilise un pool de threads pour traiter plusieurs pages en parallèle et retourne le contexte concaténé. """ results = [] with ThreadPoolExecutor(max_workers=10) as executor: future_to_link = {executor.submit(fetch_and_summarize, link, query): link for link in links} for future in as_completed(future_to_link): result = future.result() results.append(result) return "".join(results) # --- Déroulement principal de la fonction operate_requests --- yield {"type": "web_search_status", "content": "start"} # Signaler le début de la recherche web # 1. Recherche sur Whoogle all_links = whoogle_search(prompt) if not all_links: print("Aucun résultat trouvé.") yield {"type": "web_search_status", "content": "end"} # Signaler la fin de la recherche web yield "", [] # Retourner une liste de sources vide return # 2. Limiter à 7 liens limited_links = all_links[:7] print(f"{len(limited_links)} lien(s) utilisé(s) pour la recherche.") # 3. Extraction et résumé en parallèle context = fetch_pages_and_summarize(limited_links, prompt) yield {"type": "web_search_status", "content": "end"} # Signaler la fin de la recherche web yield context, limited_links # Retourner aussi les liens utilisés return
def extract_code_blocks(text: str) -> Tuple[Optional[str], Optional[str]]:
"""Extrait les blocs de code Python et gère le marqueur [CODE_REQUIRED]."""
if CODE_REQUIRED_MARKER in text:
text_no_marker = text.replace(CODE_REQUIRED_MARKER, "", 1).strip()
code_match = CODE_PATTERN.search(text_no_marker)
if code_match:
return code_match.group(1).strip(), None
else:
code_match = CODE_PATTERN.search(text)
if code_match:
return None, code_match.group(1).strip() # Code à afficher (sans exécution)
return None, None
def run_code_in_sandbox(code: str) -> str:
"""Exécute le code Python dans le sandbox e2b et retourne le résultat."""
start_time = time.time()
try:
logger.info("Début exécution code dans sandbox e2b.")
execution = None
# Amélioration (mineure) : Utilisation de next pour trouver la première méthode disponible
method_name = next((method for method in ["run_code_sync", "execute_sync", "execute_code", "execute", "run_code", "run"] if hasattr(sandbox, method)), None)
if method_name:
logger.info(f"Utilisation de la méthode sandbox.{method_name}")
execution = getattr(sandbox, method_name)(code)
else:
error_msg = "Aucune méthode d'exécution de code disponible dans le sandbox."
logger.error(error_msg)
return error_msg
logs = execution.logs
stdout_logs = logs.stdout if hasattr(logs, 'stdout') else []
stderr_logs = logs.stderr if hasattr(logs, 'stderr') else []
stdout = "\n".join(stdout_logs)
stderr = "\n".join(stderr_logs)
if stderr:
return f"Stdout:\n{stdout}\n\nStderr:\n{stderr}"
return stdout
except Exception as e:
duration = time.time() - start_time
error_msg = f"Erreur lors de l'exécution du code dans e2b après {duration:.2f} secondes: {str(e)}"
logger.exception(error_msg)
return f"Erreur exécution code: {str(e)}"
def extract_numeric_result(exec_result: str) -> Optional[str]:
"""Extrait la dernière ligne numérique du résultat d'exécution de code."""
if "Stdout:" in exec_result:
stdout_section = exec_result.split("Stdout:\n", 1)[1].split("\n\nStderr:")[0].strip()
else:
stdout_section = exec_result.strip()
lines = [l.strip() for l in stdout_section.splitlines() if l.strip()]
if not lines:
return None
last_line = lines[-1]
if re.search(r'\d', last_line):
return last_line
return None
@app.route('/models', methods=['GET'])
def get_models() -> Response:
"""Retourne la liste des modèles valides."""
return jsonify({"models": VALID_MODELS})
@app.route('/chat', methods=['POST'])
def chat_endpoint() -> Response:
"""
Point d'entrée principal pour le chat.
Gère le streaming SSE, l'exécution de code et les canvas.
Intègre la recherche web si activée (configurable via requête).
"""
text@stream_with_context @retry(stop=stop_after_attempt(5), # Réessayer jusqu'à 5 fois wait=wait_exponential(multiplier=1, min=4, max=60), # Délai exponentiel (min 4s, max 60s) retry=retry_if_exception_type(httpx.HTTPError)) # Réessayer seulement pour les erreurs HTTP (incluant 429) def stream_response(): data = request.get_json() if not data: logger.warning("Requête chat invalide: corps de requête manquant.") yield format_sse_event({"type": "error", "content": "Requête chat invalide : corps de requête manquant."}) return enable_web_search = data.get('enable_web_search', True) # Recherche web configurable via requête, activée par défaut messages = data.get('messages', []) model: str = data.get('model', 'llama3-70b-8192').strip() execute_code: bool = data.get('execute_code', True) if not messages or not isinstance(messages, list) or not model or model not in VALID_MODELS: logger.warning("Requête chat invalide: messages=%s, model=%s", messages, model) yield format_sse_event({"type": "error", "content": "Requête chat invalide : messages ou modèle incorrects."}) return system_web_context = "" # Initialisation par défaut ici last_user_message = messages[-1]['content'] if messages else "" web_sources: List[str] = [] # Initialisation de la liste des sources web if enable_web_search and last_user_message: try: web_search_generator = operate_requests(last_user_message) # operate_requests devient un générateur while True: try: event = next(web_search_generator) # Récupérer les événements de statut de recherche if isinstance(event, dict) and event.get('type') == 'web_search_status': yield format_sse_event(event) # Forward l'événement de statut au client elif isinstance(event, tuple) and len(event) == 2: # Réception du contexte et des sources à la fin system_web_context, web_sources = event # unpack le tuple break # Sortir de la boucle après avoir reçu le contexte et les sources # Removed: elif event == "" - This was incorrect and could skip context assignment except StopIteration: # Handle StopIteration properly inside the loop system_web_context = "" # Ensure default context if generator finishes prematurely web_sources = [] break except Exception as e_outer: # Catch other potential exceptions during generator setup logger.error(f"Error during web search generator setup: {e_outer}") system_web_context = "" web_sources = [] else: system_web_context = "" # Prompt système factorisé et simplifié base_prompt = "🎯 **Rôle : Expert en calcul numérique précis et assistant polyvalent avec canvas interactifs (texte et code).**\n" \ "**Mission :** Répondre aux questions, effectuer des calculs numériques exacts, et assister l'utilisateur avec des canvas éditables et interactifs pour le **texte** (avec formatage Markdown) et le **code**. Adopte un style expert et méthodique. **Maintiens le contexte de conversation, en particulier autour du code, des canvas et des résultats précédents.**\n\n" web_context_prompt_part = f"**CONTEXTE DE RECHERCHE WEB :**\n{system_web_context}\n\n" if system_web_context else "" instructions_prompt_part = "**Si la question DEMANDE un CALCUL NUMÉRIQUE :**\n" \ "1. Réponds d'abord avec une courte phrase d'introduction expliquant que tu vas utiliser un code Python pour résoudre le problème. **Sois clair sur le contexte du calcul, en te référant si nécessaire aux étapes précédentes de la conversation et aux données ou résultats déjà obtenus.**\n" \ "2. Sur la ligne suivante, ajoute TOUJOURS le marqueur `[CODE_REQUIRED]` pour indiquer qu'un code va suivre.\n" \ "3. Ensuite, fournis le **BLOC DE CODE PYTHON** minimal, lisible et correct pour effectuer UNIQUEMENT LE CALCUL demandé et afficher UNIQUEMENT LE RÉSULTAT NUMÉRIQUE via `print(resultat_numerique)`.\n" \ "**IMPORTANT :** Ne génère RIEN D'AUTRE que ces 3 éléments, et dans cet ordre précis. Arrête-toi **ICI**. Attends l'exécution du code.\n\n" \ "**[INSTRUCTION SUITE - APRES EXECUTION DU CODE]**\n" \ "Après avoir fourni le résultat du code, CONTINUE la conversation pour expliquer le résultat EN DETAILS, en explicitant clairement et précisément **LE Resultat du code python** que tu viens d'utiliser pour le calcul. Il faut que l'explication du résultat qui fasse avancer la conversation soit adaptée à la situation. Concentre-toi UNIQUEMENT sur quelque chose qui répond à la question de l'utilisateur. Faites référence au contexte de la question initiale pour bien relier l'explication au besoin, et **mentionne explicitement que le résultat du code répond à la question ou fait avancer la conversation.**\n\n" \ "**Si la question DEMANDE la CRÉATION ou MODIFICATION de CODE dans un canvas :**\n" \ "1. Indique clairement que tu vas créer un **canvas de code** interactif pour cela. Précise que l'utilisateur pourra te demander des modifications dans un 'chat de canvas' dédié qui va s'ouvrir.\n" \ "2. Encadre le contenu initial du canvas avec les marqueurs `[CANVAS_START]` et `[CANVAS_END]`.\n" \ " Exemple : 'Très bien, un canvas de code interactif est lancé pour un quick sort en C. Vous pouvez maintenant me demander des modifications directement dans le chat de canvas qui vient de s'ouvrir :\n" \ "[CANVAS_START]\n```c\n// Code Quick Sort en C\n...\n```\n[CANVAS_END]'\n" \ "3. **IMPORTANT :** Ne fournis PAS de code markdown ```python ... ``` après le canvas, sauf si cela est explicitement demandé dans la question initiale et pertinent en dehors du canvas.\n\n" \ "**Si la question DEMANDE la CRÉATION ou MODIFICATION de TEXTE RICHE (avec markdown) dans un canvas :**\n" \ "1. Indique clairement que tu vas créer un **canvas de texte** interactif pour cela. Précise que l'utilisateur pourra te demander des modifications dans un 'chat de canvas' dédié qui va s'ouvrir. Le texte dans ce canvas **supporte le formatage markdown**.\n" \ "2. Encadre le contenu initial du canvas de texte avec les marqueurs `[CANVAS_START]` et `[CANVAS_END]`.\n" \ " Exemple : 'Très bien, un canvas de texte interactif est lancé. Le texte supporte le **gras**, l'*italique*, les listes, etc. Vous pouvez maintenant me demander des modifications directement dans le chat de canvas qui vient de s'ouvrir :\n" \ "[CANVAS_START]\n# Titre principal en gras\n\n* Item 1\n* Item 2\n\n[CANVAS_END]'\n" \ "3. **IMPORTANT :** Ne fournis PAS de code markdown ```python ... ``` après le canvas de texte, sauf si cela est explicitement demandé dans la question initiale et pertinent en dehors du canvas.\n\n" \ "**Si l'utilisateur pose une question dans le 'chat de canvas' :**\n" \ " - Interprète sa demande comme une instruction pour MODIFIER le contenu du canvas actuel.\n" \ " - Fournis le NOUVEAU contenu du canvas ENTIÈREMENT MIS À JOUR encadré par `[CANVAS_START]` et `[CANVAS_END]`.\n" \ " - Ajoute une courte phrase de confirmation indiquant que le canvas a été mis à jour. **Maintiens le contexte des modifications précédentes du canvas et des instructions données dans le chat principal.**\n\n" \ "**Si la question NE DEMANDE PAS de CALCUL NUMÉRIQUE NI DE CANVAS :**\n" \ "Réponds directement et clairement à la question. Fournis une réponse informative et concise. **Prends en compte le contexte de la conversation pour formuler ta réponse.**\n" \ "Si tu dois donner un exemple de code, utilise un bloc markdown ```python ... ``` MAIS SANS JAMAIS ajouter `[CODE_REQUIRED]` ni de canvas." system_prompt_content = base_prompt + web_context_prompt_part + instructions_prompt_part system_prompt = {"role": "system", "content": system_prompt_content} messages_with_system_prompt = [system_prompt] + messages full_reply_text = "" code_to_execute = None code_to_display = None is_code_used = False canvas_content = None code_executed = False prompt_tokens = 0 # Note: Comptage des tokens non implémenté ici completion_tokens = 0 # Note: Comptage des tokens non implémenté ici total_tokens = 0 # Note: Comptage des tokens non implémenté ici initial_response_completed = False yield format_sse_event({"type": "analysis_status", "content": "start"}) try: response_stream = groq_client.chat.completions.create( model=model, messages=messages_with_system_prompt, temperature=0.0, stream=True, ) for chunk in response_stream: if initial_response_completed: break delta_content = chunk.choices[0].delta.content or "" delta_content_no_marker = delta_content.replace(CODE_REQUIRED_MARKER, "") full_reply_text += delta_content_no_marker if CANVAS_START_MARKER in full_reply_text and CANVAS_END_MARKER in full_reply_text: start_index = full_reply_text.find(CANVAS_START_MARKER) + len(CANVAS_START_MARKER) end_index = full_reply_text.find(CANVAS_END_MARKER) canvas_content_extracted = full_reply_text[start_index:end_index].strip() canvas_content = canvas_content_extracted delta_content_for_chat = delta_content_no_marker.replace(CANVAS_START_MARKER, "").replace(CANVAS_END_MARKER, "") yield format_sse_event({"type": "text", "content": delta_content_for_chat}) yield format_sse_event({"type": "canvas_start", "content": canvas_content}) continue else: yield format_sse_event({"type": "text", "content": delta_content_no_marker}) code_to_execute, code_to_display = extract_code_blocks(full_reply_text) if code_to_execute: initial_response_completed = True if execute_code and code_to_execute: is_code_used = True logger.info("Envoi événement CODE_BLOCK") yield format_sse_event({"type": "code_block", "content": code_to_execute}) yield format_sse_event({"type": "code_execution_start"}) execution_result = run_code_in_sandbox(code_to_execute) numeric_result = extract_numeric_result(execution_result) if numeric_result: code_executed = True logger.info("Envoi événement EXECUTED_CODE_RESULT") yield format_sse_event({"type": "executed_code_result", "content": numeric_result}) conclusion_prompt_content = ( f"**[INSTRUCTION SUITE - APRES EXECUTION DU CODE]** Tu as exécuté le code Python suivant:\n" f"```python\n{code_to_execute}\n```\n" f"Le résultat du calcul est: **{numeric_result}**. " "Maintenant, continue pour une conversation logique et réponds à la question de l'utilisateur en te basant sur la question initiale." ) conclusion_prompt = [{"role": "user", "content": conclusion_prompt_content}] conclusion_stream = groq_client.chat.completions.create( model=model, messages=conclusion_prompt, temperature=0.0, stream=True, ) for chunk in conclusion_stream: delta_content = chunk.choices[0].delta.content or "" yield format_sse_event({"type": "text", "content": delta_content}) yield format_sse_event({"type": "final"}) else: code_executed = True logger.info("Envoi événement EXECUTED_CODE_ERROR") yield format_sse_event({"type": "executed_code_error", "content": execution_result}) yield format_sse_event({"type": "final", "result": "Fin de la réponse (erreur ou pas de résultat numérique)."}) if web_sources: # Envoyer les sources web si elles existent yield format_sse_event({"type": "web_sources", "content": web_sources}) yield format_sse_event({"type": "sources_start"}) # Indiquer le début des sources for source_url in web_sources: yield format_sse_event({"type": "source_item", "content": source_url}) # Envoyer chaque source individuellement yield format_sse_event({"type": "sources_end"}) # Indiquer la fin des sources prompt_tokens = 0 # Note: Comptage des tokens non implémenté ici completion_tokens = 0 # Note: Comptage des tokens non implémenté ici total_tokens = prompt_tokens + completion_tokens # Note: Comptage des tokens non implémenté ici if not code_executed: logger.info("Envoi événement FINAL (sans code exécuté)") yield format_sse_event({"type": "final", "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": total_tokens}) else: yield format_sse_event({"type": "final", "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": total_tokens}) except httpx.HTTPError as e: # Intercepter explicitement httpx.HTTPError pour tenacity dans stream_response if e.response.status_code == 429: error_message = f"Erreur 429 (Too Many Requests) lors de l'appel à l'API Groq (stream_response). Réessai automatique par Tenacity. Détails: {e}" logger.warning(error_message) # Log warning, tenacity va retry yield format_sse_event({"type": "error", "content": error_message}) # Envoyer au client que retry est en cours (optionnel, peut être perçu comme une erreur temporaire) raise e # Reraise pour que tenacity retry else: # Pour les autres erreurs HTTP error_message = f"Erreur HTTP lors de l'appel à l'API Groq (stream_response) (non-429). Détails: {e}" logger.exception(error_message) yield format_sse_event({"type": "error", "content": error_message}) except Exception as e: error_message = f"Erreur serveur IA: {str(e)}" logger.exception(error_message) yield format_sse_event({"type": "error", "content": error_message}) finally: yield format_sse_event({"type": "analysis_status", "content": "end"}) yield format_sse_event({"type": "done", "is_code_execution": is_code_used}) return Response(stream_with_context(stream_response()), mimetype='text/event-stream')
@app.route('/canvas_modify', methods=['POST'])
def canvas_modify_endpoint() -> Response:
data = request.get_json()
if not data:
logger.warning("Requête canvas_modify sans données.")
return jsonify({"error": "Requête canvas_modify invalide : corps de requête manquant."}), 400
user_message = data.get('message')
canvas_content = data.get('canvas_content')
model = data.get('model', 'llama3-70b-8192').strip()
if not user_message or canvas_content is None:
logger.warning("Requête canvas_modify incomplète: message=%s, canvas_content=%s", user_message, canvas_content)
return jsonify({"error": "Requête canvas_modify invalide : message ou contenu canvas manquant."}), 400
canvas_chat_messages = [
{"role": "system", "content": ("Tu es dans un chat de canvas. L'utilisateur va te demander de modifier le contenu du canvas suivant. "
"Fournis UNIQUEMENT le NOUVEAU contenu du canvas ENTIÈREMENT MIS À JOUR, encadré par [CANVAS_START] et [CANVAS_END]. "
"Ne réponds pas avec du texte en dehors de ces marqueurs, sauf une courte phrase de confirmation après le canvas. "
"Maintiens le contexte des modifications précédentes du canvas, en te basant sur l'historique de ce chat canvas.")},
{"role": "assistant", "content": f"[CANVAS_START]\n{canvas_content}\n[CANVAS_END] Voici le canvas actuel. Dites-moi comment le modifier."},
{"role": "user", "content": user_message}
]
text@retry(stop=stop_after_attempt(5), # Réessayer jusqu'à 5 fois wait=wait_exponential(multiplier=1, min=4, max=60), # Délai exponentiel (min 4s, max 60s) retry=retry_if_exception_type(httpx.HTTPError)) # Réessayer seulement pour les erreurs HTTP (incluant 429) def canvas_modify_groq_call(canvas_chat_messages, model): """Fonction retryable pour l'appel à Groq dans canvas_modify_endpoint""" return groq_client.chat.completions.create( model=model, messages=canvas_chat_messages, temperature=0.0, stream=False, ) try: response_stream = canvas_modify_groq_call(canvas_chat_messages, model) # Appel à la fonction retryable ai_response_text = response_stream.choices[0].message.content or "" if CANVAS_START_MARKER in ai_response_text and CANVAS_END_MARKER in ai_response_text: start_index = ai_response_text.find(CANVAS_START_MARKER) + len(CANVAS_START_MARKER) end_index = ai_response_text.find(CANVAS_END_MARKER) updated_canvas_content = ai_response_text[start_index:end_index].strip() response_message = "Canvas mis à jour." logger.info("Canvas modifié avec succès.") return jsonify({"updated_canvas_content": updated_canvas_content, "response_message": response_message}) else: logger.error("Réponse IA canvas_modify invalide: marqueurs CANVAS_START/END manquants.") return jsonify({"error": "Réponse IA invalide pour la modification du canvas", "full_response": ai_response_text}), 500 except httpx.HTTPError as e: # Intercepter explicitement httpx.HTTPError if e.response.status_code == 429: error_message = f"Erreur 429 (Too Many Requests) lors de l'appel à l'API Groq (canvas_modify_endpoint). Après plusieurs retries, l'opération a échoué. Détails: {e}" else: error_message = f"Erreur HTTP lors de l'appel à l'API Groq (canvas_modify_endpoint) (non-429). Détails: {e}" logger.exception(error_message) # Log l'exception complète (traceback) return jsonify({"error": "Erreur lors de la modification du canvas après plusieurs tentatives.", "details": error_message}), 500 except Exception as e: error_message = f"Erreur serveur (canvas_modify): {str(e)}" logger.exception(error_message) return jsonify({"error": error_message}), 500
@app.route('/run', methods=['POST'])
def run_code_endpoint() -> Response:
"""
Point d'accès pour exécuter du code via l'API Piston (pour canvas code).
Langages supportés par Piston.
"""
data = request.get_json()
if not data:
logger.warning("Requête run sans données.")
return jsonify({"error": "Requête run invalide : corps de requête manquant."}), 400
language = data.get("language")
code = data.get("code")
stdin = data.get("stdin", "")
if not language or not code:
logger.warning("Requête run incomplète: language=%s, code=%s", language, code)
return jsonify({"error": "Requête run invalide : langage et code requis."}), 400
payload = {
"language": language,
"version": "*",
"files": [{"content": code}],
"stdin": stdin
}
try:
piston_response = http_session.post(PISTON_API_URL, json=payload, timeout=30)
piston_response.raise_for_status()
logger.info("Code exécuté avec Piston avec succès.")
return jsonify(piston_response.json())
except requests.RequestException as e:
error_message = f"Erreur lors de l'appel à l'API Piston: {str(e)}"
logger.exception(error_message)
piston_text = piston_response.text if 'piston_response' in locals() and piston_response is not None else ""
return jsonify({"error": "Erreur d'exécution du code via l'API Piston.", "piston_response": piston_text, "details": error_message}), 500
except Exception as e:
error_message = f"Erreur inattendue lors de l'exécution du code: {str(e)}"
logger.exception(error_message)
return jsonify({"error": "Erreur inattendue lors de l'exécution du code.", "details": error_message}), 500
@app.route('/run', methods=['OPTIONS'])
def run_code_options() -> Response:
"""Gère les requêtes OPTIONS pour l'endpoint /run (CORS preflight)."""
return jsonify({'message': 'OPTIONS requests allowed'}), 200
def format_sse_event(data: Dict[str, Any]) -> str:
"""Formate les données en event SSE (Server-Sent Events)."""
return f"data: {json.dumps(data)}\n\n"
if name == 'main':
if os.environ.get('FLASK_ENV') == 'development':
app.run(debug=True, port=5000)
"""
Voici ci-dessous le code complet corrigé avec quelques ajustements pour mieux limiter et intégrer le contexte web dans la conversation (notamment en tronquant le contenu extrait de chaque page et le contexte total) :
pythonfrom gevent import monkey monkey.patch_all() import os import re import json import logging from typing import Any, Dict, List, Optional, Tuple, Generator import time import requests from bs4 import BeautifulSoup from concurrent.futures import ThreadPoolExecutor, as_completed from flask import Flask, request, Response, jsonify, stream_with_context from flask_cors import CORS from groq import Groq from e2b_code_interpreter import Sandbox # Importation des décorateurs tenacity from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type import httpx # Pour gérer les exceptions HTTP spécifiques # Configuration du logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = Flask(__name__) CORS(app) # Clés API (variables d'environnement ou valeurs par défaut sécurisées) GROQ_API_KEY = os.environ.get("GROQ_API_KEY") E2B_API_KEY = os.environ.get("E2B_API_KEY") PISTON_API_URL = "https://emkc.org/api/v2/piston/execute" # URL de l'API Piston WHOOGLE_INSTANCE_URL = "https://chatapp-1-u8t2.onrender.com" # URL de votre instance Whoogle groq_client = Groq(api_key=GROQ_API_KEY) sandbox = Sandbox(api_key=E2B_API_KEY, timeout=600) VALID_MODELS = [ "llama-3.3-70b-versatile", "distil-whisper-large-v3-en", "gemma2-9b-it", "llama-3.1-8b-instant", "llama-guard-3-8b", "llama3-70b-8192", "llama3-8b-8192", "mixtral-8x7b-32768", "whisper-large-v3", "whisper-large-v3-turbo", "deepseek-r1-distill-llama-70b-specdec", "deepseek-r1-distill-llama-70b", "llama-3.3-specdec", "llama-3.2-1b-preview", "llama-3.2-3b-preview", "llama-3.2-11b-vision-preview", "llama-3.2-90b-vision-preview", "llama3-70b-8192" ] CODE_PATTERN = re.compile(r"```python\s*([\s\S]*?)```") CODE_REQUIRED_MARKER = "[CODE_REQUIRED]" CANVAS_START_MARKER = "[CANVAS_START]" CANVAS_END_MARKER = "[CANVAS_END]" # Limites pour le contexte web (Whoogle) MAX_CONTEXT_CHARS_PER_PAGE = 2000 MAX_TOTAL_CONTEXT_CHARS = 7000 WHOOGLE_CACHE: Dict[str, str] = {} http_session = requests.Session() http_session.headers.update({"User-Agent": "PlinkoChatBot/1.0"}) def operate_requests(prompt) -> Generator[Dict[str, str], None, Tuple[str, List[str]]]: """ Regroupe toutes les opérations : 1. Recherche sur Whoogle. 2. Limitation à 7 liens. 3. Extraction et résumé du contenu de chaque page (en parallèle). 4. Retourne le contexte obtenu et la liste des sources. """ # On utilise la clé API Groq déjà définie globalement global GROQ_API_KEY groq_client = Groq(api_key=GROQ_API_KEY) def whoogle_search(query): """ Effectue une recherche via Whoogle et retourne une liste de liens. """ base_url = WHOOGLE_INSTANCE_URL + "/search" params = {"q": query} headers = {"User-Agent": "Mozilla/5.0"} try: response = requests.get(base_url, params=params, headers=headers) response.raise_for_status() except requests.exceptions.RequestException as e: logger.error(f"Erreur lors de la requête Whoogle : {e}") return [] soup = BeautifulSoup(response.text, "html.parser") # Extraire uniquement les liens commençant par "http" links = [result["href"] for result in soup.find_all("a", href=True) if result["href"].startswith("http")] return links @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=4, max=60), retry=retry_if_exception_type(httpx.HTTPError)) def summarize_with_groq(query, text_content): """ Utilise l'API Groq pour extraire les informations importantes du texte. En cas d'erreur (notamment 429), on réessaie automatiquement. """ try: response = groq_client.chat.completions.create( model="qwen-2.5-32b", # Modèle choisi pour la synthèse messages=[ { "role": "system", "content": ( "Vous êtes un assistant expert en extraction d'informations. " "Votre tâche est de lire le texte fourni et d'extraire les informations les plus importantes " "qui répondent à la question de recherche suivante : '{}'. " "Créez une note concise résumant les points clés et les informations pertinentes. " "Soyez précis et factuel." ).format(query) }, { "role": "user", "content": text_content # Le contenu est déjà tronqué si nécessaire } ], temperature=0.2 ) return response.choices[0].message.content.strip() except httpx.HTTPError as e: if e.response.status_code == 429: logger.warning(f"Erreur 429 (Too Many Requests) lors de l'appel à l'API Groq (summarize_with_groq). Détails: {e}") raise e else: logger.error(f"Erreur HTTP (non-429) dans summarize_with_groq: {e}") raise except Exception as e: logger.error(f"Erreur inattendue dans summarize_with_groq: {e}") return "Erreur lors de la synthèse avec Groq." def fetch_and_summarize(link, query): """ Récupère le contenu d'une page et utilise Groq pour en extraire une note résumée. On limite le texte extrait à MAX_CONTEXT_CHARS_PER_PAGE caractères. """ try: r = requests.get(link, timeout=20) r.raise_for_status() soup_page = BeautifulSoup(r.text, 'html.parser') text_content = soup_page.get_text(separator='\n', strip=True) # Limiter le contenu de la page text_content = text_content[:MAX_CONTEXT_CHARS_PER_PAGE] logger.info(f"{link} : OK - Extraction du contenu (tronqué à {MAX_CONTEXT_CHARS_PER_PAGE} caractères).") summary_note = summarize_with_groq(query, text_content) logger.info(f"{link} : OK - Note Groq ajoutée.") return f"Source: {link}\nNote:\n{summary_note}\n\n" except requests.exceptions.RequestException: logger.error(f"{link} : Échec de la récupération de la page.") return f"Source: {link}\nNote:\nErreur de récupération de la page.\n\n" except Exception as e: logger.error(f"{link} : Erreur lors du traitement de la page : {e}") return f"Source: {link}\nNote:\nErreur lors du traitement de la page: {e}\n\n" def fetch_pages_and_summarize(links, query): """ Traite plusieurs pages en parallèle et retourne le contexte concaténé. """ results = [] with ThreadPoolExecutor(max_workers=10) as executor: future_to_link = {executor.submit(fetch_and_summarize, link, query): link for link in links} for future in as_completed(future_to_link): result = future.result() results.append(result) return "".join(results) # --- Déroulement principal de la fonction operate_requests --- yield {"type": "web_search_status", "content": "start"} # 1. Recherche sur Whoogle all_links = whoogle_search(prompt) if not all_links: logger.info("Aucun résultat trouvé pour la recherche web.") yield {"type": "web_search_status", "content": "end"} yield "", [] # Retourne un contexte vide et aucune source return # 2. Limiter à 7 liens limited_links = all_links[:7] logger.info(f"{len(limited_links)} lien(s) utilisé(s) pour la recherche web.") # 3. Extraction et résumé en parallèle context = fetch_pages_and_summarize(limited_links, prompt) # Tronquer le contexte total si nécessaire if len(context) > MAX_TOTAL_CONTEXT_CHARS: context = context[:MAX_TOTAL_CONTEXT_CHARS] yield {"type": "web_search_status", "content": "end"} yield context, limited_links return def extract_code_blocks(text: str) -> Tuple[Optional[str], Optional[str]]: """Extrait les blocs de code Python et gère le marqueur [CODE_REQUIRED].""" if CODE_REQUIRED_MARKER in text: text_no_marker = text.replace(CODE_REQUIRED_MARKER, "", 1).strip() code_match = CODE_PATTERN.search(text_no_marker) if code_match: return code_match.group(1).strip(), None else: code_match = CODE_PATTERN.search(text) if code_match: return None, code_match.group(1).strip() # Code à afficher sans exécution return None, None def run_code_in_sandbox(code: str) -> str: """Exécute le code Python dans le sandbox e2b et retourne le résultat.""" start_time = time.time() try: logger.info("Début de l'exécution du code dans le sandbox e2b.") execution = None method_name = next((method for method in ["run_code_sync", "execute_sync", "execute_code", "execute", "run_code", "run"] if hasattr(sandbox, method)), None) if method_name: logger.info(f"Utilisation de la méthode sandbox.{method_name}") execution = getattr(sandbox, method_name)(code) else: error_msg = "Aucune méthode d'exécution de code disponible dans le sandbox." logger.error(error_msg) return error_msg logs = execution.logs stdout_logs = logs.stdout if hasattr(logs, 'stdout') else [] stderr_logs = logs.stderr if hasattr(logs, 'stderr') else [] stdout = "\n".join(stdout_logs) stderr = "\n".join(stderr_logs) if stderr: return f"Stdout:\n{stdout}\n\nStderr:\n{stderr}" return stdout except Exception as e: duration = time.time() - start_time error_msg = f"Erreur lors de l'exécution du code dans e2b après {duration:.2f} secondes: {str(e)}" logger.exception(error_msg) return f"Erreur exécution code: {str(e)}" def extract_numeric_result(exec_result: str) -> Optional[str]: """Extrait la dernière ligne numérique du résultat d'exécution de code.""" if "Stdout:" in exec_result: stdout_section = exec_result.split("Stdout:\n", 1)[1].split("\n\nStderr:")[0].strip() else: stdout_section = exec_result.strip() lines = [l.strip() for l in stdout_section.splitlines() if l.strip()] if not lines: return None last_line = lines[-1] if re.search(r'\d', last_line): return last_line return None @app.route('/models', methods=['GET']) def get_models() -> Response: """Retourne la liste des modèles valides.""" return jsonify({"models": VALID_MODELS}) @app.route('/chat', methods=['POST']) def chat_endpoint() -> Response: """ Point d'entrée principal pour le chat. Gère le streaming SSE, l'exécution de code et les canvas. Intègre la recherche web si activée (configurable via la requête). """ @stream_with_context @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=4, max=60), retry=retry_if_exception_type(httpx.HTTPError)) def stream_response(): data = request.get_json() if not data: logger.warning("Requête chat invalide: corps de requête manquant.") yield format_sse_event({"type": "error", "content": "Requête chat invalide : corps de requête manquant."}) return enable_web_search = data.get('enable_web_search', True) messages = data.get('messages', []) model: str = data.get('model', 'llama3-70b-8192').strip() execute_code: bool = data.get('execute_code', True) if not messages or not isinstance(messages, list) or not model or model not in VALID_MODELS: logger.warning("Requête chat invalide: messages=%s, model=%s", messages, model) yield format_sse_event({"type": "error", "content": "Requête chat invalide : messages ou modèle incorrects."}) return system_web_context = "" last_user_message = messages[-1]['content'] if messages else "" web_sources: List[str] = [] if enable_web_search and last_user_message: try: web_search_generator = operate_requests(last_user_message) while True: try: event = next(web_search_generator) if isinstance(event, dict) and event.get('type') == 'web_search_status': yield format_sse_event(event) elif isinstance(event, tuple) and len(event) == 2: system_web_context, web_sources = event break except StopIteration: system_web_context = "" web_sources = [] break except Exception as e_outer: logger.error(f"Erreur lors de la recherche web : {e_outer}") system_web_context = "" web_sources = "" else: system_web_context = "" # Construction du prompt système avec intégration du contexte web si présent base_prompt = ( "🎯 **Rôle : Expert en calcul numérique précis et assistant polyvalent avec canvas interactifs (texte et code).**\n" "**Mission :** Répondre aux questions, effectuer des calculs numériques exacts, et assister l'utilisateur avec des canvas éditables et interactifs pour le **texte** (avec formatage Markdown) et le **code**. " "Maintiens le contexte de conversation, en particulier autour du code, des canvas et des résultats précédents.\n\n" ) web_context_prompt_part = f"**CONTEXTE DE RECHERCHE WEB :**\n{system_web_context}\n\n" if system_web_context else "" instructions_prompt_part = ( "**Si la question DEMANDE un CALCUL NUMÉRIQUE :**\n" "1. Réponds d'abord avec une courte phrase d'introduction expliquant que tu vas utiliser un code Python pour résoudre le problème. **Sois clair sur le contexte du calcul, en te référant si nécessaire aux étapes précédentes de la conversation et aux données ou résultats déjà obtenus.**\n" "2. Sur la ligne suivante, ajoute TOUJOURS le marqueur `[CODE_REQUIRED]` pour indiquer qu'un code va suivre.\n" "3. Ensuite, fournis le **BLOC DE CODE PYTHON** minimal, lisible et correct pour effectuer UNIQUEMENT LE CALCUL demandé et afficher UNIQUEMENT LE RÉSULTAT NUMÉRIQUE via `print(resultat_numerique)`.\n" "**IMPORTANT :** Ne génère RIEN D'AUTRE que ces 3 éléments, et dans cet ordre précis. Arrête-toi **ICI**. Attends l'exécution du code.\n\n" "**[INSTRUCTION SUITE - APRES EXECUTION DU CODE]**\n" "Après avoir fourni le résultat du code, CONTINUE la conversation pour expliquer le résultat EN DETAILS, en explicitant clairement et précisément **LE Résultat du code Python** que tu viens d'utiliser pour le calcul. Il faut que l'explication du résultat réponde à la question de l'utilisateur.\n\n" "**Si la question DEMANDE la CRÉATION ou MODIFICATION de CODE dans un canvas :**\n" "1. Indique clairement que tu vas créer un **canvas de code** interactif pour cela. Précise que l'utilisateur pourra te demander des modifications dans un 'chat de canvas' dédié qui va s'ouvrir.\n" "2. Encadre le contenu initial du canvas avec les marqueurs `[CANVAS_START]` et `[CANVAS_END]`.\n" " Exemple : 'Très bien, un canvas de code interactif est lancé pour un quick sort en C. Vous pouvez maintenant me demander des modifications directement dans le chat de canvas qui vient de s'ouvrir :\n" "[CANVAS_START]\n```c\n// Code Quick Sort en C\n...\n```\n[CANVAS_END]'\n" "3. **IMPORTANT :** Ne fournis PAS de code markdown ```python ... ``` après le canvas, sauf si cela est explicitement demandé et pertinent.\n\n" "**Si la question DEMANDE la CRÉATION ou MODIFICATION de TEXTE RICHE (avec markdown) dans un canvas :**\n" "1. Indique clairement que tu vas créer un **canvas de texte** interactif pour cela. Précise que l'utilisateur pourra te demander des modifications dans un 'chat de canvas' dédié.\n" "2. Encadre le contenu initial du canvas de texte avec les marqueurs `[CANVAS_START]` et `[CANVAS_END]`.\n" " Exemple : 'Très bien, un canvas de texte interactif est lancé. Le texte supporte le **gras**, l'*italique*, les listes, etc. Vous pouvez maintenant me demander des modifications directement dans le chat de canvas qui vient de s'ouvrir :\n" "[CANVAS_START]\n# Titre principal en gras\n\n* Item 1\n* Item 2\n\n[CANVAS_END]'\n" "3. **IMPORTANT :** Ne fournis PAS de code markdown ```python ... ``` après le canvas de texte, sauf si cela est explicitement demandé.\n\n" "**Si l'utilisateur pose une question dans le 'chat de canvas' :**\n" " - Interprète sa demande comme une instruction pour MODIFIER le contenu du canvas actuel.\n" " - Fournis le NOUVEAU contenu du canvas ENTIÈREMENT MIS À JOUR encadré par `[CANVAS_START]` et `[CANVAS_END]`.\n" " - Ajoute une courte phrase de confirmation indiquant que le canvas a été mis à jour.\n\n" "**Si la question NE DEMANDE PAS de CALCUL NUMÉRIQUE NI DE CANVAS :**\n" "Réponds directement et clairement à la question en tenant compte du contexte de la conversation.\n" "Si tu dois donner un exemple de code, utilise un bloc markdown ```python ... ``` MAIS SANS JAMAIS ajouter `[CODE_REQUIRED]` ni de canvas." ) system_prompt_content = base_prompt + web_context_prompt_part + instructions_prompt_part system_prompt = {"role": "system", "content": system_prompt_content} messages_with_system_prompt = [system_prompt] + messages full_reply_text = "" code_to_execute = None code_to_display = None is_code_used = False canvas_content = None code_executed = False prompt_tokens = 0 completion_tokens = 0 total_tokens = 0 initial_response_completed = False yield format_sse_event({"type": "analysis_status", "content": "start"}) try: response_stream = groq_client.chat.completions.create( model=model, messages=messages_with_system_prompt, temperature=0.0, stream=True, ) for chunk in response_stream: if initial_response_completed: break delta_content = chunk.choices[0].delta.content or "" delta_content_no_marker = delta_content.replace(CODE_REQUIRED_MARKER, "") full_reply_text += delta_content_no_marker # Vérifier et extraire le canvas s'il est présent if CANVAS_START_MARKER in full_reply_text and CANVAS_END_MARKER in full_reply_text: start_index = full_reply_text.find(CANVAS_START_MARKER) + len(CANVAS_START_MARKER) end_index = full_reply_text.find(CANVAS_END_MARKER) canvas_content_extracted = full_reply_text[start_index:end_index].strip() canvas_content = canvas_content_extracted delta_content_for_chat = delta_content_no_marker.replace(CANVAS_START_MARKER, "").replace(CANVAS_END_MARKER, "") yield format_sse_event({"type": "text", "content": delta_content_for_chat}) yield format_sse_event({"type": "canvas_start", "content": canvas_content}) continue else: yield format_sse_event({"type": "text", "content": delta_content_no_marker}) code_to_execute, code_to_display = extract_code_blocks(full_reply_text) if code_to_execute: initial_response_completed = True if execute_code and code_to_execute: is_code_used = True logger.info("Envoi événement CODE_BLOCK") yield format_sse_event({"type": "code_block", "content": code_to_execute}) yield format_sse_event({"type": "code_execution_start"}) execution_result = run_code_in_sandbox(code_to_execute) numeric_result = extract_numeric_result(execution_result) if numeric_result: code_executed = True logger.info("Envoi événement EXECUTED_CODE_RESULT") yield format_sse_event({"type": "executed_code_result", "content": numeric_result}) conclusion_prompt_content = ( f"**[INSTRUCTION SUITE - APRES EXECUTION DU CODE]** Tu as exécuté le code Python suivant:\n" f"```python\n{code_to_execute}\n```\n" f"Le résultat du calcul est: **{numeric_result}**. " "Maintenant, continue pour une conversation logique en te basant sur la question initiale." ) conclusion_prompt = [{"role": "user", "content": conclusion_prompt_content}] conclusion_stream = groq_client.chat.completions.create( model=model, messages=conclusion_prompt, temperature=0.0, stream=True, ) for chunk in conclusion_stream: delta_content = chunk.choices[0].delta.content or "" yield format_sse_event({"type": "text", "content": delta_content}) yield format_sse_event({"type": "final"}) else: code_executed = True logger.info("Envoi événement EXECUTED_CODE_ERROR") yield format_sse_event({"type": "executed_code_error", "content": execution_result}) yield format_sse_event({"type": "final", "result": "Fin de la réponse (erreur ou pas de résultat numérique)."}) if web_sources: # Envoyer les sources web si présentes yield format_sse_event({"type": "web_sources", "content": web_sources}) yield format_sse_event({"type": "sources_start"}) for source_url in web_sources: yield format_sse_event({"type": "source_item", "content": source_url}) yield format_sse_event({"type": "sources_end"}) prompt_tokens = 0 # Comptage non implémenté completion_tokens = 0 total_tokens = prompt_tokens + completion_tokens if not code_executed: logger.info("Envoi événement FINAL (sans code exécuté)") yield format_sse_event({"type": "final", "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": total_tokens}) else: yield format_sse_event({"type": "final", "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": total_tokens}) except httpx.HTTPError as e: if e.response.status_code == 429: error_message = f"Erreur 429 (Too Many Requests) dans stream_response. Détails: {e}" logger.warning(error_message) yield format_sse_event({"type": "error", "content": error_message}) raise e else: error_message = f"Erreur HTTP (non-429) dans stream_response. Détails: {e}" logger.exception(error_message) yield format_sse_event({"type": "error", "content": error_message}) except Exception as e: error_message = f"Erreur serveur IA: {str(e)}" logger.exception(error_message) yield format_sse_event({"type": "error", "content": error_message}) finally: yield format_sse_event({"type": "analysis_status", "content": "end"}) yield format_sse_event({"type": "done", "is_code_execution": is_code_used}) return Response(stream_with_context(stream_response()), mimetype='text/event-stream') @app.route('/canvas_modify', methods=['POST']) def canvas_modify_endpoint() -> Response: data = request.get_json() if not data: logger.warning("Requête canvas_modify sans données.") return jsonify({"error": "Requête canvas_modify invalide : corps de requête manquant."}), 400 user_message = data.get('message') canvas_content = data.get('canvas_content') model = data.get('model', 'llama3-70b-8192').strip() if not user_message or canvas_content is None: logger.warning("Requête canvas_modify incomplète: message=%s, canvas_content=%s", user_message, canvas_content) return jsonify({"error": "Requête canvas_modify invalide : message ou contenu canvas manquant."}), 400 canvas_chat_messages = [ {"role": "system", "content": ( "Tu es dans un chat de canvas. L'utilisateur va te demander de modifier le contenu du canvas suivant. " "Fournis UNIQUEMENT le NOUVEAU contenu du canvas ENTIÈREMENT MIS À JOUR, encadré par [CANVAS_START] et [CANVAS_END]. " "Ne réponds pas avec du texte en dehors de ces marqueurs, sauf une courte phrase de confirmation après le canvas. " "Maintiens le contexte des modifications précédentes du canvas, en te basant sur l'historique de ce chat canvas." )}, {"role": "assistant", "content": f"[CANVAS_START]\n{canvas_content}\n[CANVAS_END] Voici le canvas actuel. Dites-moi comment le modifier."}, {"role": "user", "content": user_message} ] @retry(stop=stop_after_attempt(5), wait=wait_exponential(multiplier=1, min=4, max=60), retry=retry_if_exception_type(httpx.HTTPError)) def canvas_modify_groq_call(canvas_chat_messages, model): return groq_client.chat.completions.create( model=model, messages=canvas_chat_messages, temperature=0.0, stream=False, ) try: response_stream = canvas_modify_groq_call(canvas_chat_messages, model) ai_response_text = response_stream.choices[0].message.content or "" if CANVAS_START_MARKER in ai_response_text and CANVAS_END_MARKER in ai_response_text: start_index = ai_response_text.find(CANVAS_START_MARKER) + len(CANVAS_START_MARKER) end_index = ai_response_text.find(CANVAS_END_MARKER) updated_canvas_content = ai_response_text[start_index:end_index].strip() response_message = "Canvas mis à jour." logger.info("Canvas modifié avec succès.") return jsonify({"updated_canvas_content": updated_canvas_content, "response_message": response_message}) else: logger.error("Réponse IA canvas_modify invalide: marqueurs CANVAS_START/END manquants.") return jsonify({"error": "Réponse IA invalide pour la modification du canvas", "full_response": ai_response_text}), 500 except httpx.HTTPError as e: if e.response.status_code == 429: error_message = f"Erreur 429 (Too Many Requests) dans canvas_modify_endpoint. Détails: {e}" else: error_message = f"Erreur HTTP (non-429) dans canvas_modify_endpoint. Détails: {e}" logger.exception(error_message) return jsonify({"error": "Erreur lors de la modification du canvas après plusieurs tentatives.", "details": error_message}), 500 except Exception as e: error_message = f"Erreur serveur (canvas_modify): {str(e)}" logger.exception(error_message) return jsonify({"error": error_message}), 500 @app.route('/run', methods=['POST']) def run_code_endpoint() -> Response: """ Point d'accès pour exécuter du code via l'API Piston (pour canvas code). """ data = request.get_json() if not data: logger.warning("Requête run sans données.") return jsonify({"error": "Requête run invalide : corps de requête manquant."}), 400 language = data.get("language") code = data.get("code") stdin = data.get("stdin", "") if not language or not code: logger.warning("Requête run incomplète: language=%s, code=%s", language, code) return jsonify({"error": "Requête run invalide : langage et code requis."}), 400 payload = { "language": language, "version": "*", "files": [{"content": code}], "stdin": stdin } try: piston_response = http_session.post(PISTON_API_URL, json=payload, timeout=30) piston_response.raise_for_status() logger.info("Code exécuté avec Piston avec succès.") return jsonify(piston_response.json()) except requests.RequestException as e: error_message = f"Erreur lors de l'appel à l'API Piston: {str(e)}" logger.exception(error_message) piston_text = piston_response.text if 'piston_response' in locals() and piston_response is not None else "" return jsonify({"error": "Erreur d'exécution du code via l'API Piston.", "piston_response": piston_text, "details": error_message}), 500 except Exception as e: error_message = f"Erreur inattendue lors de l'exécution du code: {str(e)}" logger.exception(error_message) return jsonify({"error": "Erreur inattendue lors de l'exécution du code.", "details": error_message}), 500 @app.route('/run', methods=['OPTIONS']) def run_code_options() -> Response: """Gère les requêtes OPTIONS pour l'endpoint /run (CORS preflight).""" return jsonify({'message': 'OPTIONS requests allowed'}), 200 def format_sse_event(data: Dict[str, Any]) -> str: """Formate les données en événement SSE (Server-Sent Events).""" return f"data: {json.dumps(data)}\n\n" if __name__ == '__main__': if os.environ.get('FLASK_ENV') == 'development': app.run(debug=True, port=5000)
Limitation du contenu extrait :
Dans la fonction fetch_and_summarize
, le texte extrait de chaque page est tronqué à MAX_CONTEXT_CHARS_PER_PAGE
(2000 caractères) afin de garder le résumé pertinent et éviter d’inonder le contexte.
Limitation du contexte total :
Après avoir concaténé les notes de chaque page, le contexte total est tronqué à MAX_TOTAL_CONTEXT_CHARS
(7000 caractères) avant d’être intégré dans le prompt système.
Améliorations mineures de logging et gestion d’erreurs :
Des messages de log plus explicites ont été ajoutés pour faciliter le débogage.
Ce code devrait désormais mieux intégrer les résultats de la recherche web dans le contexte global de la conversation. N’hésitez pas à l’adapter selon vos besoins et à tester en environnement de développement.