renvois un code complet avec des recherche interne...
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
renvois un code complet avec des recherche internet améliorer car il renvois bien les source mais ne fouille pas le contenue pour avoir de bonne information j'aimerais que vvraiment le contenu esois ajouté aux contexte pour qu'on puisse l'utilisé comme un moteur de recherhce quand le mode recherche est activé :
"""
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 # Pour gérer les exceptions HTTP spécifiques
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]]]:
"""
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)
textdef 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
textenable_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}
]
text@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)
"""
Voici ci-dessous un code complet mis à jour dans lequel la fonction de recherche web a été améliorée afin d’extraire le contenu des pages (en se focalisant sur les paragraphes ou l’élément <article>
) et de l’ajouter directement au contexte. Vous pouvez ainsi utiliser ce contexte comme un moteur de recherche lorsque le mode recherche est activé.
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 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 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é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 # Fonction de summarisation via Groq (anciennement utilisée pour résumer le texte) # Nous ne l'utilisons plus ici car nous voulons intégrer directement le contenu extrait. @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): try: response = groq_client.chat.completions.create( model="qwen-2.5-32b", 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 } ], 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 lors de l'appel à Groq (summarize_with_groq). Détails: {e}") raise e else: logger.error(f"Erreur HTTP 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 l'extrait de manière plus approfondie. Pour chaque page, on tente d'extraire le texte contenu dans un <article> ou, à défaut, dans les balises <p>. On tronque ensuite le contenu à 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') # Tentative d'extraction du contenu principal article = soup_page.find('article') if article: text_content = article.get_text(separator='\n', strip=True) else: paragraphs = soup_page.find_all('p') if paragraphs: text_content = "\n".join(p.get_text() for p in paragraphs) else: text_content = soup_page.get_text(separator='\n', strip=True) # Tronquer le contenu s'il est trop long if len(text_content) > MAX_CONTEXT_CHARS_PER_PAGE: text_content = text_content[:MAX_CONTEXT_CHARS_PER_PAGE] + "..." logger.info(f"{link} : OK - Extraction du contenu (limité à {MAX_CONTEXT_CHARS_PER_PAGE} caractères).") # On renvoie directement le contenu extrait (plutôt qu'un résumé généré) return f"Source: {link}\nContenu:\n{text_content}\n\n" except requests.exceptions.RequestException: logger.error(f"{link} : Échec de la récupération de la page.") return f"Source: {link}\nContenu:\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}\nContenu:\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 du contenu 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)
Extraction du contenu complet des pages
La fonction fetch_and_summarize
a été modifiée pour tenter d’extraire le contenu principal d’une page en cherchant d’abord un élément <article>
, puis en regroupant le texte des balises <p>
. Le contenu est ensuite tronqué à MAX_CONTEXT_CHARS_PER_PAGE
(2000 caractères par défaut) afin d’éviter des contextes trop volumineux.
Intégration directe du contenu dans le contexte
Plutôt que de résumer le contenu via l’API Groq, le texte extrait est directement renvoyé et ajouté au contexte web. Ceci permet d’avoir une “fouille” plus détaillée du contenu des pages.
Conservation des autres fonctionnalités
Les endpoints pour le chat, l’exécution de code, et la modification de canvas restent inchangés afin de garder la logique complète de l’application.
Vous pouvez adapter les constantes MAX_CONTEXT_CHARS_PER_PAGE
et MAX_TOTAL_CONTEXT_CHARS
selon vos besoins et, si nécessaire, ajouter d’autres méthodes d’extraction de contenu (par exemple via une librairie de type readability).
Ce code constitue une base complète qui intègre la recherche web améliorée en ajoutant le contenu extrait au contexte utilisé lors du chat.