Add initial project structure with chatbot implementation and requirements
This commit is contained in:
parent
2023059e3d
commit
229ba53246
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
apigit.txt
|
||||
53
README.md
53
README.md
@ -0,0 +1,53 @@
|
||||
# RAG Chatbot
|
||||
|
||||
This repository contains a Retrieval Augmented Generation (RAG) chatbot implementation that can process data and answer questions based on the provided context.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Python Version
|
||||
⚠️ **Important**: This project requires Python version lower than 3.12. Python 3.11 works correctly.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone this repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd <repository-name>
|
||||
```
|
||||
|
||||
2. Install the required dependencies:
|
||||
```bash
|
||||
pip install -r requirement.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line Interface
|
||||
Run the chatbot in terminal mode:
|
||||
```bash
|
||||
python cli.py
|
||||
```
|
||||
|
||||
### Web Interface
|
||||
Launch the Gradio web interface:
|
||||
```bash
|
||||
python gradio_chatbot.py
|
||||
```
|
||||
|
||||
### RAG Implementation
|
||||
If you want to import the RAG functionality in your own Python script:
|
||||
```python
|
||||
from rag_chatbot import RagChatbot
|
||||
|
||||
chatbot = RagChatbot()
|
||||
response = chatbot.query("your question here")
|
||||
```
|
||||
|
||||
## PDF Processing
|
||||
The repository includes a Jupyter notebook [`final_pdf.ipynb`](final_pdf.ipynb) for processing PDF documents as knowledge sources for the chatbot.
|
||||
|
||||
## Project Structure
|
||||
- [`cli.py`](cli.py): Command-line interface implementation
|
||||
- [`gradio_chatbot.py`](gradio_chatbot.py): Gradio web interface
|
||||
- [`rag_chatbot.py`](rag_chatbot.py): Core RAG implementation
|
||||
- [`final_pdf.ipynb`](final_pdf.ipynb): Jupyter notebook for PDF processing
|
||||
BIN
__pycache__/rag_chatbot.cpython-313.pyc
Normal file
BIN
__pycache__/rag_chatbot.cpython-313.pyc
Normal file
Binary file not shown.
11
chat_bot_rag.code-workspace
Normal file
11
chat_bot_rag.code-workspace
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "../Rag_Modeling/document"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
70
cli.py
Normal file
70
cli.py
Normal file
@ -0,0 +1,70 @@
|
||||
# cli.py
|
||||
from rag_chatbot import MultimodalRAGChatbot
|
||||
|
||||
def main():
|
||||
# Initialiser le chatbot
|
||||
chatbot = MultimodalRAGChatbot(
|
||||
qdrant_url="http://localhost:6333",
|
||||
qdrant_collection_name="my_documents",
|
||||
ollama_model="llama3.2"
|
||||
)
|
||||
|
||||
print("Chatbot RAG Multimodal")
|
||||
print("Tapez 'exit' pour quitter ou 'clear' pour effacer l'historique")
|
||||
|
||||
while True:
|
||||
# Récupérer la question
|
||||
query = input("\nVotre question: ")
|
||||
|
||||
# Quitter si demandé
|
||||
if query.lower() in ["exit", "quit", "q"]:
|
||||
break
|
||||
|
||||
# Effacer l'historique si demandé
|
||||
if query.lower() == "clear":
|
||||
chatbot.clear_history()
|
||||
print("Historique effacé")
|
||||
continue
|
||||
|
||||
# Demander si mode streaming
|
||||
stream_mode = input("Mode streaming? (y/n): ").lower() == 'y'
|
||||
|
||||
# Traitement de la requête
|
||||
result = chatbot.chat(query, stream=stream_mode)
|
||||
|
||||
# Si pas de streaming, afficher la réponse texte
|
||||
if not stream_mode:
|
||||
print("\n" + "="*50)
|
||||
print("Réponse:")
|
||||
print(result["response"])
|
||||
print("="*50)
|
||||
|
||||
# Afficher les informations sur les sources
|
||||
print("\nSources trouvées:")
|
||||
print(f"- {len(result['texts'])} textes")
|
||||
print(f"- {len(result['images'])} images")
|
||||
print(f"- {len(result['tables'])} tableaux")
|
||||
|
||||
# Afficher les images si demandé
|
||||
if result["images"]:
|
||||
show_images = input("\nAfficher les images? (y/n): ").lower() == 'y'
|
||||
if show_images:
|
||||
for i, img in enumerate(result["images"]):
|
||||
print(f"\nImage {i+1}: {img['caption']} (Source: {img['source']}, Page: {img['page']})")
|
||||
print(f"Description: {img['description']}")
|
||||
chatbot.display_image(img["image_data"], img["caption"])
|
||||
|
||||
# Afficher les tableaux si demandé
|
||||
if result["tables"]:
|
||||
show_tables = input("\nAfficher les tableaux? (y/n): ").lower() == 'y'
|
||||
if show_tables:
|
||||
for i, table in enumerate(result["tables"]):
|
||||
print(f"\nTableau {i+1}: {table['caption']} (Source: {table['source']}, Page: {table['page']})")
|
||||
print(f"Description: {table['description']}")
|
||||
print("\nContenu:")
|
||||
print("```")
|
||||
print(chatbot.format_table(table["table_data"]))
|
||||
print("```")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
2295
final_pdf.ipynb
Normal file
2295
final_pdf.ipynb
Normal file
File diff suppressed because one or more lines are too long
514
gradio_chatbot.py
Normal file
514
gradio_chatbot.py
Normal file
@ -0,0 +1,514 @@
|
||||
import gradio as gr
|
||||
import base64
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
import pandas as pd
|
||||
import traceback
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
|
||||
from rag_chatbot import MultimodalRAGChatbot
|
||||
from langchain.callbacks.base import BaseCallbackHandler
|
||||
|
||||
# Handler personnalisé pour capturer les tokens en streaming
|
||||
class GradioStreamingHandler(BaseCallbackHandler):
|
||||
def __init__(self):
|
||||
self.tokens_queue = queue.Queue()
|
||||
self.full_text = ""
|
||||
|
||||
def on_llm_new_token(self, token, **kwargs):
|
||||
self.tokens_queue.put(token)
|
||||
self.full_text += token
|
||||
|
||||
# Fonction pour créer un objet Image à partir des données base64
|
||||
def base64_to_image(base64_data):
|
||||
"""Convertit une image base64 en objet Image pour l'affichage direct"""
|
||||
try:
|
||||
if not base64_data:
|
||||
return None
|
||||
image_bytes = base64.b64decode(base64_data)
|
||||
image = Image.open(BytesIO(image_bytes))
|
||||
return image
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la conversion d'image: {e}")
|
||||
return None
|
||||
|
||||
# Configuration pour initialiser le chatbot
|
||||
qdrant_url = "http://localhost:6333"
|
||||
qdrant_collection_name = "my_documents"
|
||||
embedding_model = "mxbai-embed-large"
|
||||
ollama_url = "http://127.0.0.1:11434"
|
||||
default_model = "llama3.1"
|
||||
|
||||
# Liste des modèles disponibles
|
||||
AVAILABLE_MODELS = ["llama3.1", "llama3.2", "deepseek-r1:14b"]
|
||||
|
||||
# Initialiser le chatbot RAG avec le modèle par défaut
|
||||
rag_bot = MultimodalRAGChatbot(
|
||||
qdrant_url=qdrant_url,
|
||||
qdrant_collection_name=qdrant_collection_name,
|
||||
ollama_model=default_model,
|
||||
embedding_model=embedding_model,
|
||||
ollama_url=ollama_url
|
||||
)
|
||||
print(f"Chatbot initialisé avec modèle: {default_model}")
|
||||
|
||||
# Variables globales pour stocker les images et tableaux de la dernière requête
|
||||
current_images = []
|
||||
current_tables = []
|
||||
|
||||
# Fonction pour changer de modèle
|
||||
def change_model(model_name):
|
||||
global rag_bot
|
||||
|
||||
try:
|
||||
# Réinitialiser le chatbot avec le nouveau modèle
|
||||
rag_bot = MultimodalRAGChatbot(
|
||||
qdrant_url=qdrant_url,
|
||||
qdrant_collection_name=qdrant_collection_name,
|
||||
ollama_model=model_name,
|
||||
embedding_model=embedding_model,
|
||||
ollama_url=ollama_url
|
||||
)
|
||||
print(f"Modèle changé pour: {model_name}")
|
||||
return f"✅ Modèle changé pour: {model_name}"
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du changement de modèle: {e}")
|
||||
return f"❌ Erreur: {str(e)}"
|
||||
|
||||
# Fonction de traitement des requêtes avec support du streaming dans Gradio
|
||||
def process_query(message, history, streaming, show_sources, max_images):
|
||||
global current_images, current_tables
|
||||
|
||||
if not message.strip():
|
||||
return history, "", None, None
|
||||
|
||||
current_images = []
|
||||
current_tables = []
|
||||
|
||||
try:
|
||||
if streaming:
|
||||
# Version avec streaming dans Gradio
|
||||
history = history + [(message, "")]
|
||||
|
||||
# 1. Récupérer les documents pertinents
|
||||
docs = rag_bot._retrieve_relevant_documents(message)
|
||||
|
||||
# 2. Préparer le contexte et l'historique
|
||||
context = rag_bot._format_documents(docs)
|
||||
history_text = rag_bot._format_chat_history()
|
||||
|
||||
# 3. Préparer le prompt
|
||||
from langchain.prompts import ChatPromptTemplate
|
||||
prompt_template = ChatPromptTemplate.from_template("""
|
||||
Tu es un assistant documentaire spécialisé qui utilise toutes les informations disponibles dans le contexte fourni.
|
||||
|
||||
Instructions spécifiques:
|
||||
1. Pour chaque image mentionnée dans le contexte, inclue TOUJOURS dans ta réponse:
|
||||
- La légende/caption exacte de l'image
|
||||
- La source et le numéro de page
|
||||
- Une description brève de ce qu'elle montre
|
||||
|
||||
2. Pour chaque tableau mentionné dans le contexte, inclue TOUJOURS:
|
||||
- Le titre/caption exact du tableau
|
||||
- La source et le numéro de page
|
||||
- Ce que contient et signifie le tableau
|
||||
|
||||
3. Lorsque tu cites des équations mathématiques:
|
||||
- Utilise la syntaxe LaTeX exacte comme dans le document ($...$ ou $$...$$)
|
||||
- Reproduis-les fidèlement sans modification
|
||||
|
||||
4. IMPORTANT: Ne pas inventer d'informations - si une donnée n'est pas explicitement fournie dans le contexte,
|
||||
indique clairement "Cette information n'est pas disponible dans les documents fournis."
|
||||
|
||||
5. Cite précisément les sources pour chaque élément d'information (format: [Source, Page]).
|
||||
|
||||
Historique de conversation:
|
||||
{chat_history}
|
||||
|
||||
Contexte (à utiliser pour répondre):
|
||||
{context}
|
||||
|
||||
Question: {question}
|
||||
|
||||
Réponds de façon structurée et précise en intégrant activement les images, tableaux et équations disponibles dans le contexte.
|
||||
""")
|
||||
|
||||
# 4. Formater les messages pour le LLM
|
||||
messages = prompt_template.format_messages(
|
||||
chat_history=history_text,
|
||||
context=context,
|
||||
question=message
|
||||
)
|
||||
|
||||
# 5. Créer un handler de streaming personnalisé
|
||||
from langchain_ollama import ChatOllama
|
||||
handler = GradioStreamingHandler()
|
||||
|
||||
# 6. Créer un modèle LLM avec notre handler
|
||||
streaming_llm = ChatOllama(
|
||||
model=rag_bot.llm.model,
|
||||
base_url=rag_bot.llm.base_url,
|
||||
streaming=True,
|
||||
callbacks=[handler]
|
||||
)
|
||||
|
||||
# 7. Lancer la génération dans un thread pour ne pas bloquer l'UI
|
||||
def generate_response():
|
||||
streaming_llm.invoke(messages)
|
||||
|
||||
thread = threading.Thread(target=generate_response)
|
||||
thread.start()
|
||||
|
||||
# 8. Récupérer les tokens et mettre à jour l'interface
|
||||
partial_response = ""
|
||||
|
||||
# Attendre les tokens avec un timeout
|
||||
while thread.is_alive() or not handler.tokens_queue.empty():
|
||||
try:
|
||||
token = handler.tokens_queue.get(timeout=0.05)
|
||||
partial_response += token
|
||||
history[-1] = (message, partial_response)
|
||||
yield history, "", None, None
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
# 9. Thread terminé, mettre à jour l'historique de conversation du chatbot
|
||||
rag_bot.chat_history.append({"role": "user", "content": message})
|
||||
rag_bot.chat_history.append({"role": "assistant", "content": partial_response})
|
||||
|
||||
# 10. Récupérer les sources, images, tableaux
|
||||
texts, images, tables = rag_bot._process_documents(docs)
|
||||
|
||||
# Préparer les informations sur les sources
|
||||
source_info = ""
|
||||
if texts:
|
||||
source_info += f"📚 {len(texts)} textes • "
|
||||
if images:
|
||||
source_info += f"🖼️ {len(images)} images • "
|
||||
if tables:
|
||||
source_info += f"📊 {len(tables)} tableaux"
|
||||
|
||||
if source_info:
|
||||
source_info = "Sources trouvées: " + source_info
|
||||
|
||||
# 11. Traiter les images
|
||||
if show_sources and images:
|
||||
images = images[:max_images]
|
||||
for img in images:
|
||||
img_data = img.get("image_data")
|
||||
if img_data:
|
||||
image = base64_to_image(img_data)
|
||||
if image:
|
||||
current_images.append({
|
||||
"image": image,
|
||||
"caption": img.get("caption", ""),
|
||||
"source": img.get("source", ""),
|
||||
"page": img.get("page", ""),
|
||||
"description": img.get("description", "")
|
||||
})
|
||||
|
||||
# 12. Traiter les tableaux
|
||||
if show_sources and tables:
|
||||
for table in tables:
|
||||
current_tables.append({
|
||||
"data": rag_bot.format_table(table.get("table_data", "")),
|
||||
"caption": table.get("caption", ""),
|
||||
"source": table.get("source", ""),
|
||||
"page": table.get("page", ""),
|
||||
"description": table.get("description", "")
|
||||
})
|
||||
|
||||
# 13. Retourner les résultats finaux
|
||||
yield history, source_info, display_images(), display_tables()
|
||||
else:
|
||||
# Version sans streaming (code existant)
|
||||
result = rag_bot.chat(message, stream=False)
|
||||
history = history + [(message, result["response"])]
|
||||
|
||||
# Préparer les informations sur les sources
|
||||
source_info = ""
|
||||
if "texts" in result:
|
||||
source_info += f"📚 {len(result['texts'])} textes • "
|
||||
if "images" in result:
|
||||
source_info += f"🖼️ {len(result['images'])} images • "
|
||||
if "tables" in result:
|
||||
source_info += f"📊 {len(result['tables'])} tableaux"
|
||||
|
||||
if source_info:
|
||||
source_info = "Sources trouvées: " + source_info
|
||||
|
||||
# Traiter les images et tableaux
|
||||
if show_sources and "images" in result and result["images"]:
|
||||
images = result["images"][:max_images]
|
||||
for img in images:
|
||||
img_data = img.get("image_data")
|
||||
if img_data:
|
||||
image = base64_to_image(img_data)
|
||||
if image:
|
||||
current_images.append({
|
||||
"image": image,
|
||||
"caption": img.get("caption", ""),
|
||||
"source": img.get("source", ""),
|
||||
"page": img.get("page", ""),
|
||||
"description": img.get("description", "")
|
||||
})
|
||||
|
||||
if show_sources and "tables" in result and result["tables"]:
|
||||
tables = result["tables"]
|
||||
for table in tables:
|
||||
current_tables.append({
|
||||
"data": rag_bot.format_table(table.get("table_data", "")),
|
||||
"caption": table.get("caption", ""),
|
||||
"source": table.get("source", ""),
|
||||
"page": table.get("page", ""),
|
||||
"description": table.get("description", "")
|
||||
})
|
||||
|
||||
return history, source_info, display_images(), display_tables()
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Une erreur est survenue: {str(e)}"
|
||||
traceback_text = traceback.format_exc()
|
||||
print(error_msg)
|
||||
print(traceback_text)
|
||||
history = history + [(message, error_msg)]
|
||||
return history, "Erreur lors du traitement de la requête", None, None
|
||||
|
||||
# Fonctions pour afficher les images et tableaux
|
||||
def display_images():
|
||||
if not current_images:
|
||||
return None
|
||||
|
||||
gallery = []
|
||||
for img_data in current_images:
|
||||
image = img_data["image"]
|
||||
if image:
|
||||
caption = f"{img_data['caption']} (Source: {img_data['source']}, Page: {img_data['page']})"
|
||||
gallery.append((image, caption))
|
||||
|
||||
return gallery if gallery else None
|
||||
|
||||
def display_tables():
|
||||
if not current_tables:
|
||||
return None
|
||||
|
||||
html = ""
|
||||
for table in current_tables:
|
||||
html += f"""
|
||||
<div style="margin-bottom: 20px; border: 1px solid #ddd; padding: 15px; border-radius: 8px;">
|
||||
<h3>{table['caption']}</h3>
|
||||
<p style="color:#666; font-size:0.9em;">Source: {table['source']}, Page: {table['page']}</p>
|
||||
<p><strong>Description:</strong> {table['description']}</p>
|
||||
<div style="background-color:#f5f5f5; padding:10px; border-radius:5px; overflow:auto;">
|
||||
<pre>{table['data']}</pre>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return html if html else None
|
||||
|
||||
# Fonction pour réinitialiser l'historique
|
||||
def reset_conversation():
|
||||
global current_images, current_tables
|
||||
current_images = []
|
||||
current_tables = []
|
||||
|
||||
rag_bot.clear_history()
|
||||
|
||||
return [], "", None, None
|
||||
|
||||
# Interface Gradio
|
||||
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue")) as demo:
|
||||
gr.Markdown("# 📚 Assistant documentaire intelligent")
|
||||
|
||||
with gr.Row():
|
||||
with gr.Column(scale=2):
|
||||
chat_interface = gr.Chatbot(
|
||||
height=600,
|
||||
show_label=False,
|
||||
layout="bubble",
|
||||
elem_id="chatbot"
|
||||
)
|
||||
|
||||
with gr.Row():
|
||||
msg = gr.Textbox(
|
||||
show_label=False,
|
||||
placeholder="Posez votre question...",
|
||||
container=False,
|
||||
scale=4
|
||||
)
|
||||
submit_btn = gr.Button("Envoyer", variant="primary", scale=1)
|
||||
|
||||
clear_btn = gr.Button("Effacer la conversation")
|
||||
source_info = gr.Markdown("", elem_id="sources_info")
|
||||
|
||||
with gr.Column(scale=1):
|
||||
with gr.Accordion("Options", open=True):
|
||||
# Sélecteur de modèle
|
||||
model_selector = gr.Dropdown(
|
||||
choices=AVAILABLE_MODELS,
|
||||
value=default_model,
|
||||
label="Modèle Ollama",
|
||||
info="Choisir le modèle de language à utiliser"
|
||||
)
|
||||
model_status = gr.Markdown(f"Modèle actuel: **{default_model}**")
|
||||
|
||||
streaming = gr.Checkbox(
|
||||
label="Mode streaming",
|
||||
value=True,
|
||||
info="Voir les réponses s'afficher progressivement"
|
||||
)
|
||||
show_sources = gr.Checkbox(label="Afficher les sources", value=True)
|
||||
max_images = gr.Slider(
|
||||
minimum=1,
|
||||
maximum=10,
|
||||
value=3,
|
||||
step=1,
|
||||
label="Nombre max d'images"
|
||||
)
|
||||
|
||||
gr.Markdown("---")
|
||||
|
||||
gr.Markdown("### 🖼️ Images pertinentes")
|
||||
image_gallery = gr.Gallery(
|
||||
label="Images pertinentes",
|
||||
show_label=False,
|
||||
columns=2,
|
||||
height=300,
|
||||
object_fit="contain"
|
||||
)
|
||||
|
||||
gr.Markdown("### 📊 Tableaux")
|
||||
tables_display = gr.HTML()
|
||||
|
||||
# Connecter le changement de modèle
|
||||
model_selector.change(
|
||||
fn=change_model,
|
||||
inputs=model_selector,
|
||||
outputs=model_status
|
||||
)
|
||||
|
||||
# Configuration des actions
|
||||
msg.submit(
|
||||
process_query,
|
||||
inputs=[msg, chat_interface, streaming, show_sources, max_images],
|
||||
outputs=[chat_interface, source_info, image_gallery, tables_display]
|
||||
).then(lambda: "", outputs=msg)
|
||||
|
||||
submit_btn.click(
|
||||
process_query,
|
||||
inputs=[msg, chat_interface, streaming, show_sources, max_images],
|
||||
outputs=[chat_interface, source_info, image_gallery, tables_display]
|
||||
).then(lambda: "", outputs=msg)
|
||||
|
||||
clear_btn.click(
|
||||
reset_conversation,
|
||||
outputs=[chat_interface, source_info, image_gallery, tables_display]
|
||||
)
|
||||
|
||||
# Support amélioré pour les équations mathématiques avec KaTeX
|
||||
gr.Markdown("""
|
||||
<style>
|
||||
.gradio-container {max-width: 1200px !important}
|
||||
#chatbot {height: 600px; overflow-y: auto;}
|
||||
#sources_info {margin-top: 10px; color: #666;}
|
||||
|
||||
/* Style pour les équations */
|
||||
.katex { font-size: 1.1em !important; }
|
||||
.math-inline { background: #f8f9fa; padding: 2px 5px; border-radius: 4px; }
|
||||
.math-display { background: #f8f9fa; margin: 10px 0; padding: 10px; border-radius: 5px; overflow-x: auto; text-align: center; }
|
||||
</style>
|
||||
|
||||
<!-- Chargement de KaTeX -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/contrib/auto-render.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Fonction pour rendre les équations avec KaTeX
|
||||
function renderMathInElement(element) {
|
||||
if (!window.renderMathInElement) return;
|
||||
|
||||
window.renderMathInElement(element, {
|
||||
delimiters: [
|
||||
{left: '$$', right: '$$', display: true},
|
||||
{left: '$', right: '$', display: false},
|
||||
{left: '\\(', right: '\\)', display: false},
|
||||
{left: '\\[', right: '\\]', display: true}
|
||||
],
|
||||
throwOnError: false,
|
||||
trust: true,
|
||||
strict: false
|
||||
});
|
||||
}
|
||||
|
||||
// Fonction pour remplacer les underscores échappés qui posent problème
|
||||
function fixUnderscores(element) {
|
||||
const messages = element.querySelectorAll('.message');
|
||||
messages.forEach(msg => {
|
||||
const text = msg.innerHTML;
|
||||
// Remplacer les patterns comme u_(i) par u_{i} pour une meilleure compatibilité LaTeX
|
||||
const fixed = text.replace(/([a-zA-Z])_\(([^)]+)\)/g, '$1_{$2}');
|
||||
|
||||
// Remplacer également les & qui peuvent causer des problèmes
|
||||
const cleanAmpersand = fixed.replace(/&/g, '');
|
||||
|
||||
if (text !== cleanAmpersand) {
|
||||
msg.innerHTML = cleanAmpersand;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Observer les changements dans le chat
|
||||
function setupMathObserver() {
|
||||
const chatElement = document.getElementById('chatbot');
|
||||
if (!chatElement) {
|
||||
setTimeout(setupMathObserver, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.type === 'childList' || mutation.type === 'subtree') {
|
||||
const messages = chatElement.querySelectorAll('.message');
|
||||
if (messages.length > 0) {
|
||||
// D'abord corriger les underscores problématiques
|
||||
fixUnderscores(chatElement);
|
||||
|
||||
// Puis rendre les équations
|
||||
messages.forEach(msg => {
|
||||
renderMathInElement(msg);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(chatElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
|
||||
// Rendre les équations déjà présentes
|
||||
renderMathInElement(document);
|
||||
}
|
||||
|
||||
// Initialisation lorsque la page est chargée
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Attendre que KaTeX soit chargé
|
||||
if (window.renderMathInElement) {
|
||||
setupMathObserver();
|
||||
} else {
|
||||
// Attendre le chargement de KaTeX
|
||||
document.querySelector('script[src*="auto-render.min.js"]').onload = setupMathObserver;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
""")
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo.queue()
|
||||
demo.launch(share=False, inbrowser=True)
|
||||
286
rag_chatbot.py
Normal file
286
rag_chatbot.py
Normal file
@ -0,0 +1,286 @@
|
||||
from typing import Dict, List, Any, Optional
|
||||
import base64
|
||||
from io import BytesIO
|
||||
import pandas as pd
|
||||
from PIL import Image
|
||||
|
||||
# Remplacer les imports dépréciés par les nouveaux packages
|
||||
from langchain_qdrant import QdrantVectorStore
|
||||
from langchain_ollama import OllamaEmbeddings, ChatOllama
|
||||
from langchain.prompts import ChatPromptTemplate
|
||||
from langchain.schema import Document
|
||||
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
|
||||
from qdrant_client import QdrantClient
|
||||
|
||||
class MultimodalRAGChatbot:
|
||||
"""
|
||||
Chatbot RAG multimodal qui utilise Qdrant pour stocker les documents
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
qdrant_url: str = "http://localhost:6333",
|
||||
qdrant_collection_name: str = "my_documents",
|
||||
ollama_model: str = "llama3.1",
|
||||
embedding_model: str = "mxbai-embed-large",
|
||||
ollama_url: str = "http://localhost:11434" # Ajout de ce paramètre
|
||||
):
|
||||
"""
|
||||
Initialise le chatbot RAG avec Qdrant
|
||||
"""
|
||||
# Initialiser le modèle d'embedding
|
||||
self.embeddings = OllamaEmbeddings(
|
||||
model=embedding_model,
|
||||
base_url=ollama_url # Utilisation de l'URL d'Ollama
|
||||
)
|
||||
|
||||
# Créer le client Qdrant
|
||||
self.client = QdrantClient(url=qdrant_url)
|
||||
|
||||
# Se connecter à la collection existante
|
||||
self.vector_store = QdrantVectorStore(
|
||||
client=self.client,
|
||||
collection_name=qdrant_collection_name,
|
||||
embedding=self.embeddings
|
||||
)
|
||||
|
||||
# Initialiser le retriever
|
||||
self.retriever = self.vector_store.as_retriever(
|
||||
search_type="similarity",
|
||||
search_kwargs={"k": 5}
|
||||
)
|
||||
|
||||
# Initialiser les modèles LLM
|
||||
self.llm = ChatOllama(
|
||||
model=ollama_model,
|
||||
base_url=ollama_url # Utilisation de l'URL d'Ollama
|
||||
)
|
||||
self.streaming_llm = ChatOllama(
|
||||
model=ollama_model,
|
||||
base_url=ollama_url, # Utilisation de l'URL d'Ollama
|
||||
streaming=True,
|
||||
callbacks=[StreamingStdOutCallbackHandler()]
|
||||
)
|
||||
|
||||
# Historique des conversations
|
||||
self.chat_history = []
|
||||
|
||||
print(f"Chatbot initialisé avec modèle: {ollama_model}")
|
||||
print(f"Utilisant embeddings: {embedding_model}")
|
||||
print(f"Connecté à Qdrant: {qdrant_url}, collection: {qdrant_collection_name}")
|
||||
print(f"Ollama URL: {ollama_url}")
|
||||
|
||||
def chat(self, query: str, stream: bool = False):
|
||||
"""
|
||||
Traite une question de l'utilisateur et retourne une réponse
|
||||
"""
|
||||
# 1. Récupérer les documents pertinents
|
||||
docs = self._retrieve_relevant_documents(query)
|
||||
|
||||
# 2. Préparer le contexte à partir des documents
|
||||
context = self._format_documents(docs)
|
||||
|
||||
# 3. Préparer l'historique des conversations
|
||||
history_text = self._format_chat_history()
|
||||
|
||||
# 4. Créer le prompt
|
||||
prompt_template = ChatPromptTemplate.from_template("""
|
||||
Tu es un assistant intelligent qui répond aux questions en utilisant uniquement
|
||||
les informations fournies dans le contexte. Si tu ne trouves pas l'information
|
||||
dans le contexte, dis simplement que tu ne sais pas. Lorsque tu mentionnes une
|
||||
image ou un tableau, décris brièvement son contenu en te basant sur les
|
||||
descriptions fournies.
|
||||
|
||||
Historique de conversation:
|
||||
{chat_history}
|
||||
|
||||
Contexte:
|
||||
{context}
|
||||
|
||||
Question de l'utilisateur: {question}
|
||||
|
||||
Réponds de façon concise et précise en citant les sources pertinentes.
|
||||
""")
|
||||
|
||||
# 5. Générer la réponse
|
||||
llm = self.streaming_llm if stream else self.llm
|
||||
|
||||
if stream:
|
||||
print("\nRéponse:")
|
||||
|
||||
# Formater les messages pour le LLM
|
||||
messages = prompt_template.format_messages(
|
||||
chat_history=history_text,
|
||||
context=context,
|
||||
question=query
|
||||
)
|
||||
|
||||
# Appeler le LLM
|
||||
response = llm.invoke(messages)
|
||||
answer = response.content
|
||||
|
||||
# 6. Mettre à jour l'historique des conversations
|
||||
self.chat_history.append({"role": "user", "content": query})
|
||||
self.chat_history.append({"role": "assistant", "content": answer})
|
||||
|
||||
# 7. Traiter les documents pour la sortie
|
||||
texts, images, tables = self._process_documents(docs)
|
||||
|
||||
# 8. Préparer la réponse
|
||||
result = {
|
||||
"response": answer,
|
||||
"texts": texts,
|
||||
"images": images,
|
||||
"tables": tables
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def _retrieve_relevant_documents(self, query: str, k: int = 5) -> List[Document]:
|
||||
"""
|
||||
Récupère les documents pertinents de la base Qdrant
|
||||
"""
|
||||
return self.vector_store.similarity_search(query, k=k)
|
||||
|
||||
def _format_documents(self, docs: List[Document]) -> str:
|
||||
"""
|
||||
Formate les documents pour le contexte
|
||||
"""
|
||||
formatted_docs = []
|
||||
|
||||
for i, doc in enumerate(docs):
|
||||
metadata = doc.metadata
|
||||
|
||||
# Déterminer le type de document et le formater en conséquence
|
||||
if "image_base64" in metadata:
|
||||
# Image
|
||||
formatted_docs.append(
|
||||
f"[IMAGE {i+1}]\n"
|
||||
f"Source: {metadata.get('source', 'Inconnue')}\n"
|
||||
f"Page: {metadata.get('page_number', '')}\n"
|
||||
f"Caption: {metadata.get('caption', '')}\n"
|
||||
f"Description: {doc.page_content}\n"
|
||||
)
|
||||
elif "table_content" in metadata:
|
||||
# Tableau
|
||||
formatted_docs.append(
|
||||
f"[TABLEAU {i+1}]\n"
|
||||
f"Source: {metadata.get('source', 'Inconnue')}\n"
|
||||
f"Page: {metadata.get('page_number', '')}\n"
|
||||
f"Caption: {metadata.get('caption', '')}\n"
|
||||
f"Description: {doc.page_content}\n"
|
||||
)
|
||||
else:
|
||||
# Texte
|
||||
formatted_docs.append(
|
||||
f"[TEXTE {i+1}]\n"
|
||||
f"Source: {metadata.get('source', 'Inconnue')}\n"
|
||||
f"Page: {metadata.get('page_number', '')}\n"
|
||||
f"{doc.page_content}\n"
|
||||
)
|
||||
|
||||
return "\n".join(formatted_docs)
|
||||
|
||||
def _format_chat_history(self) -> str:
|
||||
"""
|
||||
Formate l'historique des conversations
|
||||
"""
|
||||
if not self.chat_history:
|
||||
return "Pas d'historique de conversation."
|
||||
|
||||
formatted_history = []
|
||||
|
||||
for message in self.chat_history:
|
||||
role = "Utilisateur" if message["role"] == "user" else "Assistant"
|
||||
formatted_history.append(f"{role}: {message['content']}")
|
||||
|
||||
return "\n".join(formatted_history)
|
||||
|
||||
def _process_documents(self, docs: List[Document]):
|
||||
"""
|
||||
Traite les documents pour séparer textes, images et tableaux
|
||||
"""
|
||||
texts = []
|
||||
images = []
|
||||
tables = []
|
||||
|
||||
for doc in docs:
|
||||
metadata = doc.metadata
|
||||
|
||||
# Déterminer le type de document
|
||||
if "image_base64" in metadata:
|
||||
# C'est une image
|
||||
images.append({
|
||||
"image_data": metadata.get("image_base64", ""),
|
||||
"description": doc.page_content,
|
||||
"caption": metadata.get("caption", ""),
|
||||
"source": metadata.get("source", ""),
|
||||
"page": metadata.get("page_number", "")
|
||||
})
|
||||
elif "table_content" in metadata:
|
||||
# C'est un tableau
|
||||
tables.append({
|
||||
"table_data": metadata.get("table_content", ""),
|
||||
"description": doc.page_content,
|
||||
"caption": metadata.get("caption", ""),
|
||||
"source": metadata.get("source", ""),
|
||||
"page": metadata.get("page_number", "")
|
||||
})
|
||||
else:
|
||||
# C'est du texte
|
||||
texts.append({
|
||||
"content": doc.page_content,
|
||||
"source": metadata.get("source", ""),
|
||||
"page": metadata.get("page_number", "")
|
||||
})
|
||||
|
||||
return texts, images, tables
|
||||
|
||||
def clear_history(self):
|
||||
"""
|
||||
Efface l'historique de conversation
|
||||
"""
|
||||
self.chat_history = []
|
||||
|
||||
def display_image(self, image_data: str, caption: str = ""):
|
||||
"""
|
||||
Affiche une image à partir de sa représentation base64
|
||||
"""
|
||||
try:
|
||||
# Décodage de l'image base64
|
||||
image_bytes = base64.b64decode(image_data)
|
||||
image = Image.open(BytesIO(image_bytes))
|
||||
|
||||
# Affichage selon l'environnement
|
||||
try:
|
||||
from IPython.display import display
|
||||
print(f"Caption: {caption}")
|
||||
display(image)
|
||||
except ImportError:
|
||||
image.show()
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'affichage de l'image: {e}")
|
||||
return False
|
||||
|
||||
def format_table(self, table_data: str) -> str:
|
||||
"""
|
||||
Formate les données d'un tableau pour l'affichage
|
||||
"""
|
||||
try:
|
||||
# Si format markdown
|
||||
if isinstance(table_data, str) and table_data.strip().startswith("|"):
|
||||
return table_data
|
||||
|
||||
# Essayer de parser comme JSON
|
||||
import json
|
||||
try:
|
||||
data = json.loads(table_data)
|
||||
df = pd.DataFrame(data)
|
||||
return df.to_string(index=False)
|
||||
except:
|
||||
# Si échec, retourner les données brutes
|
||||
return str(table_data)
|
||||
except Exception as e:
|
||||
return f"Erreur lors du formatage du tableau: {e}\n{table_data}"
|
||||
45
requirement.txt
Normal file
45
requirement.txt
Normal file
@ -0,0 +1,45 @@
|
||||
# Core LangChain packages
|
||||
langchain>=0.1.0
|
||||
langchain-community>=0.0.1
|
||||
langchain-ollama>=0.0.1
|
||||
langchain-qdrant>=0.0.1
|
||||
|
||||
# Vector database
|
||||
qdrant-client>=1.6.0
|
||||
|
||||
# LLM interface
|
||||
ollama>=0.1.0
|
||||
|
||||
# Document processing with specific versions
|
||||
pytesseract>=0.3.10
|
||||
unstructured==0.10.30
|
||||
pdfminer.six==20221105
|
||||
pdf2image>=1.16.0
|
||||
pypdf>=3.15.0
|
||||
|
||||
# OCR and image processing
|
||||
pillow_heif>=0.13.0
|
||||
Pillow>=10.0.0
|
||||
|
||||
# Data processing and visualization
|
||||
pandas>=2.0.0
|
||||
|
||||
# UI and interface
|
||||
gradio>=4.0.0
|
||||
|
||||
# Other utilities
|
||||
ipython>=8.0.0
|
||||
uuid>=1.30
|
||||
|
||||
onnx
|
||||
pdf2image
|
||||
pdfminer.six
|
||||
pikepdf
|
||||
pi_heif
|
||||
pypdf
|
||||
google-cloud-vision
|
||||
effdet
|
||||
# Do not move to constraints.in, otherwise unstructured-inference will not be upgraded
|
||||
# when unstructured library is.
|
||||
unstructured-inference>=0.8.7
|
||||
unstructured.pytesseract>=0.3.12
|
||||
Loading…
x
Reference in New Issue
Block a user