Spaces:
Sleeping
Sleeping
feat: Version finale de l'application full stack avec RAG amélioré
Browse files- PROCHAINES_ETAPES.md +0 -50
- README.md +1 -1
- backend/Dockerfile +6 -2
- backend/api/dependencies.py +3 -3
- backend/api/routes/documents.py +6 -6
- backend/api/routes/feedback.py +3 -3
- backend/api/routes/questions.py +6 -5
- backend/api/routes/users.py +3 -3
- backend/config/settings.py +7 -5
- backend/entrypoint.sh +14 -0
- backend/main.py +11 -9
- backend/models/document.py +1 -1
- backend/models/feedback.py +1 -1
- backend/models/question.py +2 -5
- backend/models/user.py +1 -1
- backend/services/document_processor.py +64 -13
- backend/services/ollama_client.py +42 -21
- backend/services/question_handler.py +29 -13
- backend/services/vector_store.py +25 -10
- docker-compose.yml +2 -0
- frontend/src/App.jsx +25 -16
- scripts/init_db.py +3 -3
- scripts/set_user_role.py +2 -2
PROCHAINES_ETAPES.md
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
# Prochaines Étapes pour l'Assistant Web Éducatif
|
| 2 |
-
|
| 3 |
-
Ce document détaille les prochaines étapes de développement pour faire évoluer le projet de son état actuel (backend fonctionnel) vers une application complète, en se basant sur le cahier des charges et la vision du produit.
|
| 4 |
-
|
| 5 |
-
---
|
| 6 |
-
### Phase 1 : Consolidation du Backend et de l'API
|
| 7 |
-
|
| 8 |
-
L'objectif est de rendre le backend plus robuste et complet.
|
| 9 |
-
|
| 10 |
-
1. **Gestion des Téléversements de Fichiers (Uploads) :**
|
| 11 |
-
* [cite_start]Modifier l'endpoint `POST /documents` pour accepter un **vrai téléversement de fichier PDF** au lieu d'un simple nom de fichier [cite: 244-247, 305-309].
|
| 12 |
-
* Sauvegarder le fichier téléversé dans le dossier `data/documents`.
|
| 13 |
-
* Déclencher automatiquement le processus d'extraction et de vectorisation juste après le téléversement.
|
| 14 |
-
|
| 15 |
-
2. **Affiner le Modèle de Réponse :**
|
| 16 |
-
* [cite_start]Enrichir la réponse de l'API `/ask` pour inclure les **sources exactes** (nom du document, numéro de page, etc.) qui ont servi de contexte [cite: 269-277].
|
| 17 |
-
* Cela implique de stocker plus de métadonnées (comme le numéro de page) lors du découpage du texte.
|
| 18 |
-
|
| 19 |
-
3. **Gestion des Utilisateurs :**
|
| 20 |
-
* [cite_start]Créer des modèles de données et des tables pour les **utilisateurs** (Étudiant, Enseignant, Administrateur) [cite: 1236-1240, 1253-1255].
|
| 21 |
-
* [cite_start]Mettre en place un système d'**authentification** (par exemple, avec JWT) pour sécuriser les endpoints[cite: 71, 132].
|
| 22 |
-
|
| 23 |
-
---
|
| 24 |
-
### Phase 2 : Développement du Frontend
|
| 25 |
-
|
| 26 |
-
L'objectif est de créer une interface utilisateur pour interagir avec le backend.
|
| 27 |
-
|
| 28 |
-
1. **Interface de Questions-Réponses :**
|
| 29 |
-
* [cite_start]Créer une page simple avec un champ de saisie pour poser une question et une zone pour afficher la réponse de l'IA [cite: 258-260, 1142].
|
| 30 |
-
* Connecter cette interface à l'endpoint `/api/v1/ask`.
|
| 31 |
-
|
| 32 |
-
2. **Interface d'Administration :**
|
| 33 |
-
* [cite_start]Développer une page sécurisée pour les enseignants et administrateurs[cite: 236].
|
| 34 |
-
* [cite_start]Créer un formulaire pour le **téléversement des manuels PDF** [cite: 237-242].
|
| 35 |
-
* Afficher la liste des documents déjà présents dans le système.
|
| 36 |
-
|
| 37 |
-
---
|
| 38 |
-
### Phase 3 : Améliorations et Déploiement
|
| 39 |
-
|
| 40 |
-
L'objectif est de préparer le projet pour une utilisation réelle.
|
| 41 |
-
|
| 42 |
-
1. **Amélioration de la Pertinence :**
|
| 43 |
-
* [cite_start]Explorer des **modèles de `sentence-transformers` multilingues** ou plus spécialisés en science pour améliorer la qualité de la recherche sémantique[cite: 80].
|
| 44 |
-
* [cite_start]Permettre à l'utilisateur de noter la pertinence des réponses pour un apprentissage continu (auto-amélioration)[cite: 20, 1271].
|
| 45 |
-
|
| 46 |
-
2. **Mise en place du Cache :**
|
| 47 |
-
* [cite_start]Intégrer **Redis** pour mettre en cache les questions fréquentes et accélérer les temps de réponse, comme spécifié dans l'architecture[cite: 25, 62, 1384].
|
| 48 |
-
|
| 49 |
-
3. **Conteneurisation Complète avec Docker Compose :**
|
| 50 |
-
* [cite_start]Écrire un fichier `docker-compose.yml` pour lancer toute l'application (Backend, PostgreSQL, Redis, Ollama) avec une seule commande, simplifiant ainsi le déploiement [cite: 148-193].
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Assistant Web Éducatif - Full Stack
|
| 2 |
|
| 3 |
-
Ce dépôt contient le code source complet de l'Assistant Web Éducatif, une application full stack conçue pour aider les étudiants
|
| 4 |
|
| 5 |
## Fonctionnalités Principales ✨
|
| 6 |
- **API Backend Robuste** : Construite avec FastAPI, elle gère la logique de traitement des documents, l'authentification des utilisateurs et la génération des réponses.
|
|
|
|
| 1 |
# Assistant Web Éducatif - Full Stack
|
| 2 |
|
| 3 |
+
Ce dépôt contient le code source complet de l'Assistant Web Éducatif, une application full stack conçue pour aider les étudiants dans un domaine spécifique. Le système analyse des manuels PDF, permet de poser des questions en langage naturel et fournit des réponses sourcées grâce à un modèle de langage local.
|
| 4 |
|
| 5 |
## Fonctionnalités Principales ✨
|
| 6 |
- **API Backend Robuste** : Construite avec FastAPI, elle gère la logique de traitement des documents, l'authentification des utilisateurs et la génération des réponses.
|
backend/Dockerfile
CHANGED
|
@@ -3,9 +3,13 @@
|
|
| 3 |
FROM python:3.12-slim
|
| 4 |
|
| 5 |
WORKDIR /app
|
|
|
|
| 6 |
COPY backend/requirements.txt .
|
| 7 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
-
COPY backend/ .
|
| 9 |
COPY scripts/ ./scripts/
|
|
|
|
|
|
|
|
|
|
| 10 |
#ENV PYTHONPATH="/app"
|
| 11 |
-
|
|
|
|
| 3 |
FROM python:3.12-slim
|
| 4 |
|
| 5 |
WORKDIR /app
|
| 6 |
+
RUN apt-get update && apt-get install -y netcat-openbsd && rm -rf /var/lib/apt/lists/*
|
| 7 |
COPY backend/requirements.txt .
|
| 8 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 9 |
+
COPY backend/ .
|
| 10 |
COPY scripts/ ./scripts/
|
| 11 |
+
|
| 12 |
+
COPY backend/entrypoint.sh .
|
| 13 |
+
RUN chmod +x ./entrypoint.sh
|
| 14 |
#ENV PYTHONPATH="/app"
|
| 15 |
+
ENTRYPOINT ["./entrypoint.sh"]
|
backend/api/dependencies.py
CHANGED
|
@@ -5,9 +5,9 @@ from fastapi.security import OAuth2PasswordBearer
|
|
| 5 |
from sqlalchemy.orm import Session
|
| 6 |
from jose import JWTError, jwt
|
| 7 |
|
| 8 |
-
from
|
| 9 |
-
from
|
| 10 |
-
from
|
| 11 |
|
| 12 |
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token")
|
| 13 |
|
|
|
|
| 5 |
from sqlalchemy.orm import Session
|
| 6 |
from jose import JWTError, jwt
|
| 7 |
|
| 8 |
+
from models.user import User
|
| 9 |
+
from utils.security import SECRET_KEY, ALGORITHM
|
| 10 |
+
from config.database import SessionLocal
|
| 11 |
|
| 12 |
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token")
|
| 13 |
|
backend/api/routes/documents.py
CHANGED
|
@@ -3,12 +3,12 @@ from sqlalchemy.orm import Session
|
|
| 3 |
import aiofiles
|
| 4 |
import os
|
| 5 |
|
| 6 |
-
from
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
-
from
|
| 10 |
-
from
|
| 11 |
-
from
|
| 12 |
|
| 13 |
|
| 14 |
router = APIRouter()
|
|
|
|
| 3 |
import aiofiles
|
| 4 |
import os
|
| 5 |
|
| 6 |
+
from models.document import Document
|
| 7 |
+
from api.dependencies import get_db
|
| 8 |
+
from services.document_processor import extract_pages_from_pdf, split_text_into_chunks
|
| 9 |
+
from services.vector_store import VectorStore
|
| 10 |
+
from models.user import User as UserModel, RoleEnum
|
| 11 |
+
from api.dependencies import get_current_user
|
| 12 |
|
| 13 |
|
| 14 |
router = APIRouter()
|
backend/api/routes/feedback.py
CHANGED
|
@@ -3,9 +3,9 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
|
| 3 |
from sqlalchemy.orm import Session
|
| 4 |
from pydantic import BaseModel
|
| 5 |
|
| 6 |
-
from
|
| 7 |
-
from
|
| 8 |
-
from
|
| 9 |
|
| 10 |
router = APIRouter()
|
| 11 |
|
|
|
|
| 3 |
from sqlalchemy.orm import Session
|
| 4 |
from pydantic import BaseModel
|
| 5 |
|
| 6 |
+
from api.dependencies import get_db, get_current_user
|
| 7 |
+
from models.feedback import Feedback as FeedbackModel
|
| 8 |
+
from models.user import User as UserModel
|
| 9 |
|
| 10 |
router = APIRouter()
|
| 11 |
|
backend/api/routes/questions.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
| 1 |
-
from fastapi import APIRouter
|
| 2 |
-
|
| 3 |
-
from
|
| 4 |
-
from
|
|
|
|
| 5 |
|
| 6 |
router = APIRouter()
|
| 7 |
handler = QuestionHandler()
|
| 8 |
|
| 9 |
# MODIFIÉ: Utilisation du response_model pour garantir le format de sortie
|
| 10 |
@router.post("/ask", response_model=QuestionResponse)
|
| 11 |
-
def ask_question(request: QuestionRequest):
|
| 12 |
"""
|
| 13 |
Receives a question, finds context, and returns an AI-generated answer.
|
| 14 |
"""
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from api.dependencies import get_db
|
| 4 |
+
from models.question import QuestionRequest, QuestionResponse
|
| 5 |
+
from services.question_handler import QuestionHandler
|
| 6 |
|
| 7 |
router = APIRouter()
|
| 8 |
handler = QuestionHandler()
|
| 9 |
|
| 10 |
# MODIFIÉ: Utilisation du response_model pour garantir le format de sortie
|
| 11 |
@router.post("/ask", response_model=QuestionResponse)
|
| 12 |
+
def ask_question(request: QuestionRequest, db: Session = Depends(get_db)):
|
| 13 |
"""
|
| 14 |
Receives a question, finds context, and returns an AI-generated answer.
|
| 15 |
"""
|
backend/api/routes/users.py
CHANGED
|
@@ -5,9 +5,9 @@ from fastapi.security import OAuth2PasswordRequestForm
|
|
| 5 |
from sqlalchemy.orm import Session
|
| 6 |
from datetime import timedelta
|
| 7 |
|
| 8 |
-
from
|
| 9 |
-
from
|
| 10 |
-
from
|
| 11 |
|
| 12 |
router = APIRouter()
|
| 13 |
|
|
|
|
| 5 |
from sqlalchemy.orm import Session
|
| 6 |
from datetime import timedelta
|
| 7 |
|
| 8 |
+
from models.user import UserCreate, UserResponse, Token, User as UserModel
|
| 9 |
+
from api.dependencies import get_db
|
| 10 |
+
from utils.security import get_password_hash, verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES
|
| 11 |
|
| 12 |
router = APIRouter()
|
| 13 |
|
backend/config/settings.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from pydantic_settings import BaseSettings
|
| 2 |
|
| 3 |
class Settings(BaseSettings):
|
|
@@ -6,13 +7,14 @@ class Settings(BaseSettings):
|
|
| 6 |
"""
|
| 7 |
PROJECT_NAME: str = "Assistant Web Éducatif"
|
| 8 |
API_V1_STR: str = "/api/v1"
|
| 9 |
-
|
| 10 |
-
# Base de données (PostgreSQL)
|
| 11 |
DATABASE_URL: str = "postgresql://postgres:postgres@postgres/assistantWed_db"
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
| 13 |
class Config:
|
| 14 |
case_sensitive = True
|
| 15 |
-
# Permet de lire les variables depuis un fichier .env
|
| 16 |
env_file = ".env"
|
| 17 |
|
| 18 |
-
settings = Settings()
|
|
|
|
| 1 |
+
# backend/config/settings.py
|
| 2 |
from pydantic_settings import BaseSettings
|
| 3 |
|
| 4 |
class Settings(BaseSettings):
|
|
|
|
| 7 |
"""
|
| 8 |
PROJECT_NAME: str = "Assistant Web Éducatif"
|
| 9 |
API_V1_STR: str = "/api/v1"
|
| 10 |
+
|
|
|
|
| 11 |
DATABASE_URL: str = "postgresql://postgres:postgres@postgres/assistantWed_db"
|
| 12 |
+
|
| 13 |
+
OLLAMA_HOST: str = "http://ollama:11434"
|
| 14 |
+
OLLAMA_MODEL: str = "tinyllama"
|
| 15 |
+
|
| 16 |
class Config:
|
| 17 |
case_sensitive = True
|
|
|
|
| 18 |
env_file = ".env"
|
| 19 |
|
| 20 |
+
settings = Settings()
|
backend/entrypoint.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
|
| 3 |
+
echo "En attente de PostgreSQL..."
|
| 4 |
+
while ! nc -z postgres 5432; do
|
| 5 |
+
sleep 1
|
| 6 |
+
done
|
| 7 |
+
echo "PostgreSQL démarré."
|
| 8 |
+
|
| 9 |
+
# Créer les tables de la base de données
|
| 10 |
+
echo "Initialisation de la base de données..."
|
| 11 |
+
python -m scripts.init_db
|
| 12 |
+
|
| 13 |
+
# Lancer le serveur Uvicorn
|
| 14 |
+
exec uvicorn main:app --host 0.0.0.0 --port 8000
|
backend/main.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
|
|
|
|
|
| 1 |
from fastapi import FastAPI
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
-
from
|
| 4 |
-
from
|
| 5 |
-
from backend.api.routes import questions, documents, users,feedback
|
| 6 |
|
| 7 |
app = FastAPI(
|
| 8 |
title=settings.PROJECT_NAME,
|
|
@@ -10,24 +11,25 @@ app = FastAPI(
|
|
| 10 |
version="0.1.0"
|
| 11 |
)
|
| 12 |
|
| 13 |
-
app.include_router(users.router, prefix="/api/v1", tags=["Users"])
|
| 14 |
-
|
| 15 |
app.add_middleware(
|
| 16 |
CORSMiddleware,
|
| 17 |
-
allow_origins=["*"],
|
| 18 |
allow_credentials=True,
|
| 19 |
-
allow_methods=["*"],
|
| 20 |
-
allow_headers=["*"],
|
| 21 |
)
|
| 22 |
|
|
|
|
|
|
|
| 23 |
app.include_router(questions.router, prefix="/api/v1", tags=["Questions"])
|
| 24 |
app.include_router(documents.router, prefix="/api/v1", tags=["Documents"])
|
| 25 |
app.include_router(feedback.router, prefix="/api/v1", tags=["Feedback"])
|
| 26 |
|
|
|
|
| 27 |
@app.get("/")
|
| 28 |
def read_root():
|
| 29 |
return {"message": "Bienvenue sur l'API de l'Assistant Web Éducatif"}
|
| 30 |
|
| 31 |
@app.get("/health")
|
| 32 |
def health_check():
|
| 33 |
-
return {"status": "ok"}
|
|
|
|
| 1 |
+
# backend/main.py
|
| 2 |
+
|
| 3 |
from fastapi import FastAPI
|
| 4 |
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
from api.routes import questions, documents, users, feedback
|
| 6 |
+
from config.settings import settings
|
|
|
|
| 7 |
|
| 8 |
app = FastAPI(
|
| 9 |
title=settings.PROJECT_NAME,
|
|
|
|
| 11 |
version="0.1.0"
|
| 12 |
)
|
| 13 |
|
|
|
|
|
|
|
| 14 |
app.add_middleware(
|
| 15 |
CORSMiddleware,
|
| 16 |
+
allow_origins=["*"],
|
| 17 |
allow_credentials=True,
|
| 18 |
+
allow_methods=["*"],
|
| 19 |
+
allow_headers=["*"],
|
| 20 |
)
|
| 21 |
|
| 22 |
+
# Les inclusions de routeurs sont correctes
|
| 23 |
+
app.include_router(users.router, prefix="/api/v1", tags=["Users"])
|
| 24 |
app.include_router(questions.router, prefix="/api/v1", tags=["Questions"])
|
| 25 |
app.include_router(documents.router, prefix="/api/v1", tags=["Documents"])
|
| 26 |
app.include_router(feedback.router, prefix="/api/v1", tags=["Feedback"])
|
| 27 |
|
| 28 |
+
|
| 29 |
@app.get("/")
|
| 30 |
def read_root():
|
| 31 |
return {"message": "Bienvenue sur l'API de l'Assistant Web Éducatif"}
|
| 32 |
|
| 33 |
@app.get("/health")
|
| 34 |
def health_check():
|
| 35 |
+
return {"status": "ok"}
|
backend/models/document.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from sqlalchemy import Column, Integer, String, DateTime
|
| 2 |
from sqlalchemy.sql import func
|
| 3 |
-
from
|
| 4 |
|
| 5 |
class Document(Base):
|
| 6 |
__tablename__ = "documents"
|
|
|
|
| 1 |
from sqlalchemy import Column, Integer, String, DateTime
|
| 2 |
from sqlalchemy.sql import func
|
| 3 |
+
from config.database import Base
|
| 4 |
|
| 5 |
class Document(Base):
|
| 6 |
__tablename__ = "documents"
|
backend/models/feedback.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# backend/models/feedback.py
|
| 2 |
from sqlalchemy import Column, Integer, String, Text, DateTime
|
| 3 |
from sqlalchemy.sql import func
|
| 4 |
-
from
|
| 5 |
|
| 6 |
class Feedback(Base):
|
| 7 |
__tablename__ = "feedback"
|
|
|
|
| 1 |
# backend/models/feedback.py
|
| 2 |
from sqlalchemy import Column, Integer, String, Text, DateTime
|
| 3 |
from sqlalchemy.sql import func
|
| 4 |
+
from config.database import Base
|
| 5 |
|
| 6 |
class Feedback(Base):
|
| 7 |
__tablename__ = "feedback"
|
backend/models/question.py
CHANGED
|
@@ -1,17 +1,14 @@
|
|
| 1 |
-
# backend/models/question.py
|
| 2 |
-
|
| 3 |
from pydantic import BaseModel, Field
|
| 4 |
from typing import Optional, List
|
| 5 |
|
| 6 |
class QuestionRequest(BaseModel):
|
| 7 |
question: str = Field(..., min_length=5, max_length=500)
|
| 8 |
-
|
| 9 |
class Source(BaseModel):
|
| 10 |
-
|
| 11 |
page: Optional[int]
|
| 12 |
|
| 13 |
class QuestionResponse(BaseModel):
|
| 14 |
question: str
|
| 15 |
answer: str
|
| 16 |
sources: List[Source]
|
| 17 |
-
cached: bool = False
|
|
|
|
|
|
|
|
|
|
| 1 |
from pydantic import BaseModel, Field
|
| 2 |
from typing import Optional, List
|
| 3 |
|
| 4 |
class QuestionRequest(BaseModel):
|
| 5 |
question: str = Field(..., min_length=5, max_length=500)
|
|
|
|
| 6 |
class Source(BaseModel):
|
| 7 |
+
document: str
|
| 8 |
page: Optional[int]
|
| 9 |
|
| 10 |
class QuestionResponse(BaseModel):
|
| 11 |
question: str
|
| 12 |
answer: str
|
| 13 |
sources: List[Source]
|
| 14 |
+
cached: bool = False
|
backend/models/user.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
import enum
|
| 4 |
from sqlalchemy import Column, Integer, String, Enum
|
| 5 |
-
from
|
| 6 |
from pydantic import BaseModel
|
| 7 |
|
| 8 |
|
|
|
|
| 2 |
|
| 3 |
import enum
|
| 4 |
from sqlalchemy import Column, Integer, String, Enum
|
| 5 |
+
from config.database import Base
|
| 6 |
from pydantic import BaseModel
|
| 7 |
|
| 8 |
|
backend/services/document_processor.py
CHANGED
|
@@ -1,35 +1,86 @@
|
|
| 1 |
import fitz # PyMuPDF
|
| 2 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 3 |
from typing import List, Dict
|
|
|
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
def extract_pages_from_pdf(file_path: str) -> List[Dict]:
|
| 7 |
"""Extrait le contenu de chaque page et son numéro."""
|
| 8 |
doc = fitz.open(file_path)
|
| 9 |
pages_content = []
|
|
|
|
| 10 |
for page_num, page in enumerate(doc):
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
return pages_content
|
| 16 |
|
| 17 |
-
|
| 18 |
def split_text_into_chunks(pages: List[Dict]) -> List[Dict]:
|
| 19 |
"""Découpe le texte de chaque page en morceaux en conservant les métadonnées."""
|
| 20 |
text_splitter = RecursiveCharacterTextSplitter(
|
| 21 |
-
chunk_size=
|
| 22 |
-
chunk_overlap=
|
| 23 |
-
length_function=len
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
all_chunks = []
|
|
|
|
|
|
|
| 27 |
for page in pages:
|
|
|
|
| 28 |
chunks_on_page = text_splitter.split_text(page["content"])
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
all_chunks.append({
|
| 31 |
-
"text":
|
| 32 |
-
"metadata": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
})
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import fitz # PyMuPDF
|
| 2 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 3 |
from typing import List, Dict
|
| 4 |
+
import re
|
| 5 |
|
| 6 |
+
def clean_text(text: str) -> str:
|
| 7 |
+
"""Nettoie le texte extrait du PDF."""
|
| 8 |
+
text = re.sub(r' +', ' ', text)
|
| 9 |
+
text = re.sub(r'\n{3,}', '\n\n', text)
|
| 10 |
+
text = '\n'.join(line.strip() for line in text.split('\n'))
|
| 11 |
+
return text.strip()
|
| 12 |
|
| 13 |
def extract_pages_from_pdf(file_path: str) -> List[Dict]:
|
| 14 |
"""Extrait le contenu de chaque page et son numéro."""
|
| 15 |
doc = fitz.open(file_path)
|
| 16 |
pages_content = []
|
| 17 |
+
|
| 18 |
for page_num, page in enumerate(doc):
|
| 19 |
+
raw_text = page.get_text()
|
| 20 |
+
cleaned_text = clean_text(raw_text)
|
| 21 |
+
if len(cleaned_text.strip()) > 50:
|
| 22 |
+
pages_content.append({
|
| 23 |
+
"page_number": page_num + 1,
|
| 24 |
+
"content": cleaned_text
|
| 25 |
+
})
|
| 26 |
+
else:
|
| 27 |
+
print(f"⚠️ Page {page_num + 1} ignorée (trop courte)")
|
| 28 |
+
|
| 29 |
+
print(f"✅ {len(pages_content)} pages extraites du PDF")
|
| 30 |
return pages_content
|
| 31 |
|
|
|
|
| 32 |
def split_text_into_chunks(pages: List[Dict]) -> List[Dict]:
|
| 33 |
"""Découpe le texte de chaque page en morceaux en conservant les métadonnées."""
|
| 34 |
text_splitter = RecursiveCharacterTextSplitter(
|
| 35 |
+
chunk_size=1500,
|
| 36 |
+
chunk_overlap=300,
|
| 37 |
+
length_function=len,
|
| 38 |
+
separators=[
|
| 39 |
+
"\n\n\n",
|
| 40 |
+
"\n\n",
|
| 41 |
+
"\n",
|
| 42 |
+
". ",
|
| 43 |
+
" ",
|
| 44 |
+
""
|
| 45 |
+
]
|
| 46 |
)
|
| 47 |
|
| 48 |
all_chunks = []
|
| 49 |
+
chunk_global_index = 0
|
| 50 |
+
|
| 51 |
for page in pages:
|
| 52 |
+
page_num = page["page_number"]
|
| 53 |
chunks_on_page = text_splitter.split_text(page["content"])
|
| 54 |
+
|
| 55 |
+
for local_idx, chunk_text in enumerate(chunks_on_page):
|
| 56 |
+
if len(chunk_text.strip()) < 100:
|
| 57 |
+
print(f"⚠️ Chunk ignoré (trop court) - Page {page_num}")
|
| 58 |
+
continue
|
| 59 |
all_chunks.append({
|
| 60 |
+
"text": chunk_text.strip(),
|
| 61 |
+
"metadata": {
|
| 62 |
+
"page": page_num,
|
| 63 |
+
"chunk_index_on_page": local_idx,
|
| 64 |
+
"global_chunk_index": chunk_global_index,
|
| 65 |
+
"chunk_length": len(chunk_text)
|
| 66 |
+
}
|
| 67 |
})
|
| 68 |
+
chunk_global_index += 1
|
| 69 |
+
|
| 70 |
+
print(f"✅ {len(all_chunks)} chunks créés au total")
|
| 71 |
+
if all_chunks:
|
| 72 |
+
avg_length = sum(c["metadata"]["chunk_length"] for c in all_chunks) / len(all_chunks)
|
| 73 |
+
print(f"📊 Longueur moyenne des chunks : {avg_length:.0f} caractères")
|
| 74 |
+
return all_chunks
|
| 75 |
+
def preview_chunks(chunks: List[Dict], n: int = 3):
|
| 76 |
+
"""Affiche les n premiers chunks pour vérifier la qualité du découpage."""
|
| 77 |
+
print(f"\n🔍 Aperçu des {min(n, len(chunks))} premiers chunks :\n")
|
| 78 |
+
|
| 79 |
+
for i, chunk in enumerate(chunks[:n]):
|
| 80 |
+
text = chunk["text"]
|
| 81 |
+
meta = chunk["metadata"]
|
| 82 |
+
|
| 83 |
+
print(f"--- Chunk #{i+1} (Page {meta['page']}) ---")
|
| 84 |
+
print(f"Longueur : {meta['chunk_length']} caractères")
|
| 85 |
+
print(f"Texte (100 premiers caractères) : {text[:100]}...")
|
| 86 |
+
print()
|
backend/services/ollama_client.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
# backend/services/ollama_client.py
|
| 2 |
import requests
|
| 3 |
import json
|
| 4 |
import os
|
|
@@ -6,32 +5,54 @@ import os
|
|
| 6 |
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
|
| 7 |
|
| 8 |
def generate_response(question: str, context: str) -> str:
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
|
|
|
| 21 |
try:
|
| 22 |
-
|
|
|
|
| 23 |
f"{OLLAMA_HOST}/api/generate",
|
| 24 |
json={
|
| 25 |
-
"model": "mistral",
|
| 26 |
"prompt": prompt,
|
| 27 |
-
"stream":
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
},
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
except requests.exceptions.RequestException as e:
|
| 36 |
-
print(f"
|
| 37 |
return "Désolé, une erreur est survenue lors de la génération de la réponse."
|
|
|
|
|
|
|
| 1 |
import requests
|
| 2 |
import json
|
| 3 |
import os
|
|
|
|
| 5 |
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
|
| 6 |
|
| 7 |
def generate_response(question: str, context: str) -> str:
|
| 8 |
+
prompt = f"""Tu es un assistant éducatif spécialisé en Recherche Opérationnelle.
|
| 9 |
+
|
| 10 |
+
CONTEXTE DU DOCUMENT :
|
| 11 |
+
{context}
|
| 12 |
+
|
| 13 |
+
QUESTION DE L'ÉTUDIANT :
|
| 14 |
+
{question}
|
| 15 |
+
|
| 16 |
+
INSTRUCTIONS IMPORTANTES :
|
| 17 |
+
1. Réponds UNIQUEMENT en utilisant les informations du contexte ci-dessus
|
| 18 |
+
2. Si l'information n'existe PAS dans le contexte, réponds EXACTEMENT : "L'information n'est pas disponible dans le document fourni."
|
| 19 |
+
3. Sois précis, pédagogique et structuré
|
| 20 |
+
4. Utilise des exemples du contexte si disponibles
|
| 21 |
+
5. Ne cite JAMAIS d'informations externes au contexte
|
| 22 |
+
|
| 23 |
+
RÉPONSE :"""
|
| 24 |
|
| 25 |
+
full_response_text = ""
|
| 26 |
try:
|
| 27 |
+
print(f"Envoi de la requête en streaming à Ollama sur l'hôte : {OLLAMA_HOST}")
|
| 28 |
+
with requests.post(
|
| 29 |
f"{OLLAMA_HOST}/api/generate",
|
| 30 |
json={
|
| 31 |
+
"model": "mistral",
|
| 32 |
"prompt": prompt,
|
| 33 |
+
"stream": True,
|
| 34 |
+
"options": {
|
| 35 |
+
"temperature": 0.1,
|
| 36 |
+
"num_predict": 500
|
| 37 |
+
}
|
| 38 |
},
|
| 39 |
+
stream=True,
|
| 40 |
+
timeout=300
|
| 41 |
+
) as response:
|
| 42 |
+
response.raise_for_status()
|
| 43 |
+
for line in response.iter_lines():
|
| 44 |
+
if line:
|
| 45 |
+
try:
|
| 46 |
+
chunk = json.loads(line)
|
| 47 |
+
full_response_text += chunk.get("response", "")
|
| 48 |
+
if chunk.get("done"):
|
| 49 |
+
break
|
| 50 |
+
except json.JSONDecodeError:
|
| 51 |
+
print(f"Ligne JSON invalide reçue d'Ollama: {line}")
|
| 52 |
+
|
| 53 |
+
print("Réponse complète reçue d'Ollama.")
|
| 54 |
+
return full_response_text.strip()
|
| 55 |
|
| 56 |
except requests.exceptions.RequestException as e:
|
| 57 |
+
print(f"Erreur lors de l'appel à Ollama : {e}")
|
| 58 |
return "Désolé, une erreur est survenue lors de la génération de la réponse."
|
backend/services/question_handler.py
CHANGED
|
@@ -1,35 +1,51 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
from
|
| 4 |
-
from
|
| 5 |
-
from
|
| 6 |
-
|
| 7 |
class QuestionHandler:
|
| 8 |
def __init__(self):
|
| 9 |
self.vector_store = VectorStore()
|
| 10 |
-
self.cache = CacheManager()
|
| 11 |
|
| 12 |
-
def get_answer(self, question: str):
|
| 13 |
cached_answer = self.cache.get(question)
|
| 14 |
if cached_answer:
|
| 15 |
-
print(f"Réponse trouvée dans le cache pour : '{question}'")
|
| 16 |
return {**cached_answer, "cached": True}
|
| 17 |
-
|
| 18 |
-
search_results = self.vector_store.find_similar_chunks(question)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
context_texts = search_results["documents"][0]
|
| 20 |
-
sources_metadata = search_results["metadatas"][0]
|
| 21 |
context = "\n---\n".join(context_texts)
|
|
|
|
| 22 |
answer = generate_response(question, context)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
sources = []
|
| 24 |
for meta in sources_metadata:
|
|
|
|
| 25 |
sources.append({
|
| 26 |
-
"
|
| 27 |
"page": meta.get("page")
|
| 28 |
})
|
|
|
|
| 29 |
final_response = {
|
| 30 |
"question": question,
|
| 31 |
"answer": answer,
|
| 32 |
"sources": sources
|
| 33 |
}
|
|
|
|
| 34 |
self.cache.set(question, final_response)
|
| 35 |
return {**final_response, "cached": False}
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from services.vector_store import VectorStore
|
| 3 |
+
from services.ollama_client import generate_response
|
| 4 |
+
from services.cache_manager import CacheManager
|
| 5 |
+
from models.document import Document
|
|
|
|
| 6 |
class QuestionHandler:
|
| 7 |
def __init__(self):
|
| 8 |
self.vector_store = VectorStore()
|
| 9 |
+
self.cache = CacheManager()
|
| 10 |
|
| 11 |
+
def get_answer(self, question: str, db: Session):
|
| 12 |
cached_answer = self.cache.get(question)
|
| 13 |
if cached_answer:
|
|
|
|
| 14 |
return {**cached_answer, "cached": True}
|
| 15 |
+
|
| 16 |
+
search_results = self.vector_store.find_similar_chunks(question, n_results=5)
|
| 17 |
+
|
| 18 |
+
if not search_results or not search_results.get("documents") or not search_results["documents"][0]:
|
| 19 |
+
return {
|
| 20 |
+
"question": question,
|
| 21 |
+
"answer": "Désolé, je n'ai trouvé aucune information pertinente dans les documents fournis pour répondre à cette question.",
|
| 22 |
+
"sources": [],
|
| 23 |
+
"cached": False
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
context_texts = search_results["documents"][0]
|
| 27 |
+
sources_metadata = search_results["metadatas"][0]
|
| 28 |
context = "\n---\n".join(context_texts)
|
| 29 |
+
|
| 30 |
answer = generate_response(question, context)
|
| 31 |
+
|
| 32 |
+
doc_ids = {meta.get("document_id") for meta in sources_metadata if meta.get("document_id") is not None}
|
| 33 |
+
documents = db.query(Document).filter(Document.id.in_(doc_ids)).all()
|
| 34 |
+
doc_titles = {doc.id: doc.file_name for doc in documents}
|
| 35 |
+
|
| 36 |
sources = []
|
| 37 |
for meta in sources_metadata:
|
| 38 |
+
doc_id = meta.get("document_id")
|
| 39 |
sources.append({
|
| 40 |
+
"document": doc_titles.get(doc_id, "Titre inconnu"),
|
| 41 |
"page": meta.get("page")
|
| 42 |
})
|
| 43 |
+
|
| 44 |
final_response = {
|
| 45 |
"question": question,
|
| 46 |
"answer": answer,
|
| 47 |
"sources": sources
|
| 48 |
}
|
| 49 |
+
|
| 50 |
self.cache.set(question, final_response)
|
| 51 |
return {**final_response, "cached": False}
|
backend/services/vector_store.py
CHANGED
|
@@ -5,19 +5,25 @@ from typing import List, Dict
|
|
| 5 |
class VectorStore:
|
| 6 |
def __init__(self):
|
| 7 |
self.client = chromadb.PersistentClient(path="data/chroma_db")
|
| 8 |
-
|
| 9 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
def add_document_chunks(self, doc_id: int, chunks: List[Dict]):
|
| 12 |
if not chunks:
|
| 13 |
return
|
| 14 |
|
| 15 |
texts = [chunk["text"] for chunk in chunks]
|
| 16 |
-
|
|
|
|
| 17 |
|
| 18 |
metadatas = []
|
| 19 |
for i, chunk in enumerate(chunks):
|
| 20 |
-
meta = chunk["metadata"]
|
| 21 |
meta["document_id"] = doc_id
|
| 22 |
meta["chunk_index"] = i
|
| 23 |
metadatas.append(meta)
|
|
@@ -25,21 +31,30 @@ class VectorStore:
|
|
| 25 |
ids = [f"doc_{doc_id}_chunk_{i}" for i, _ in enumerate(chunks)]
|
| 26 |
|
| 27 |
self.collection.add(
|
| 28 |
-
embeddings=embeddings,
|
| 29 |
metadatas=metadatas,
|
| 30 |
-
documents=texts,
|
| 31 |
ids=ids
|
| 32 |
)
|
| 33 |
-
print(f"Ajout de {len(chunks)} chunks pour le document {doc_id} à ChromaDB.")
|
| 34 |
|
| 35 |
-
def find_similar_chunks(self, question: str, n_results: int =
|
| 36 |
"""Trouve les chunks pertinents et retourne leur contenu et métadonnées."""
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
|
| 39 |
results = self.collection.query(
|
| 40 |
query_embeddings=[query_embedding.tolist()],
|
| 41 |
n_results=n_results,
|
| 42 |
-
include=["documents", "metadatas"
|
| 43 |
)
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
return results
|
|
|
|
| 5 |
class VectorStore:
|
| 6 |
def __init__(self):
|
| 7 |
self.client = chromadb.PersistentClient(path="data/chroma_db")
|
| 8 |
+
|
| 9 |
+
self.embedding_model = SentenceTransformer('intfloat/multilingual-e5-large')
|
| 10 |
+
|
| 11 |
+
self.collection = self.client.get_or_create_collection(
|
| 12 |
+
name="documents",
|
| 13 |
+
metadata={"hnsw:space": "cosine"}
|
| 14 |
+
)
|
| 15 |
|
| 16 |
def add_document_chunks(self, doc_id: int, chunks: List[Dict]):
|
| 17 |
if not chunks:
|
| 18 |
return
|
| 19 |
|
| 20 |
texts = [chunk["text"] for chunk in chunks]
|
| 21 |
+
prefixed_texts = [f"passage: {text}" for text in texts]
|
| 22 |
+
embeddings = self.embedding_model.encode(prefixed_texts, normalize_embeddings=True)
|
| 23 |
|
| 24 |
metadatas = []
|
| 25 |
for i, chunk in enumerate(chunks):
|
| 26 |
+
meta = chunk["metadata"].copy()
|
| 27 |
meta["document_id"] = doc_id
|
| 28 |
meta["chunk_index"] = i
|
| 29 |
metadatas.append(meta)
|
|
|
|
| 31 |
ids = [f"doc_{doc_id}_chunk_{i}" for i, _ in enumerate(chunks)]
|
| 32 |
|
| 33 |
self.collection.add(
|
| 34 |
+
embeddings=embeddings.tolist(),
|
| 35 |
metadatas=metadatas,
|
| 36 |
+
documents=texts,
|
| 37 |
ids=ids
|
| 38 |
)
|
| 39 |
+
print(f"✅ Ajout de {len(chunks)} chunks pour le document {doc_id} à ChromaDB.")
|
| 40 |
|
| 41 |
+
def find_similar_chunks(self, question: str, n_results: int = 5) -> Dict:
|
| 42 |
"""Trouve les chunks pertinents et retourne leur contenu et métadonnées."""
|
| 43 |
+
|
| 44 |
+
prefixed_question = f"query: {question}"
|
| 45 |
+
query_embedding = self.embedding_model.encode(prefixed_question, normalize_embeddings=True)
|
| 46 |
|
| 47 |
results = self.collection.query(
|
| 48 |
query_embeddings=[query_embedding.tolist()],
|
| 49 |
n_results=n_results,
|
| 50 |
+
include=["documents", "metadatas", "distances"]
|
| 51 |
)
|
| 52 |
|
| 53 |
+
if results.get("distances"):
|
| 54 |
+
distances = results["distances"][0]
|
| 55 |
+
print(f"\n📊 Scores de similarité pour '{question}':")
|
| 56 |
+
for i, dist in enumerate(distances[:3]):
|
| 57 |
+
similarity = 1 - dist
|
| 58 |
+
print(f" Chunk {i+1}: {similarity:.3f}")
|
| 59 |
+
|
| 60 |
return results
|
docker-compose.yml
CHANGED
|
@@ -22,6 +22,8 @@ services:
|
|
| 22 |
- ollama_data:/root/.ollama
|
| 23 |
ports:
|
| 24 |
- "11434:11434"
|
|
|
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
redis:
|
|
|
|
| 22 |
- ollama_data:/root/.ollama
|
| 23 |
ports:
|
| 24 |
- "11434:11434"
|
| 25 |
+
environment:
|
| 26 |
+
- OLLAMA_KEEP_ALIVE=-1
|
| 27 |
|
| 28 |
|
| 29 |
redis:
|
frontend/src/App.jsx
CHANGED
|
@@ -15,6 +15,18 @@ function App() {
|
|
| 15 |
return;
|
| 16 |
}
|
| 17 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
await fetch('http://localhost:8000/api/v1/feedback', {
|
| 19 |
method: 'POST',
|
| 20 |
headers: {
|
|
@@ -22,7 +34,7 @@ function App() {
|
|
| 22 |
'Authorization': `Bearer ${token}`
|
| 23 |
},
|
| 24 |
body: JSON.stringify({
|
| 25 |
-
question:
|
| 26 |
answer: message.text,
|
| 27 |
rating: rating
|
| 28 |
})
|
|
@@ -44,13 +56,10 @@ function App() {
|
|
| 44 |
setPrompt('');
|
| 45 |
setIsLoading(true);
|
| 46 |
|
| 47 |
-
// --- NOUVEAU : Appel à l'API Backend ---
|
| 48 |
try {
|
| 49 |
const response = await fetch('http://localhost:8000/api/v1/ask', {
|
| 50 |
method: 'POST',
|
| 51 |
-
headers: {
|
| 52 |
-
'Content-Type': 'application/json',
|
| 53 |
-
},
|
| 54 |
body: JSON.stringify({ question: currentPrompt }),
|
| 55 |
});
|
| 56 |
|
|
@@ -60,10 +69,10 @@ function App() {
|
|
| 60 |
|
| 61 |
const data = await response.json();
|
| 62 |
|
| 63 |
-
const apiResponse = {
|
| 64 |
-
sender: 'bot',
|
| 65 |
text: data.answer,
|
| 66 |
-
sources: data.sources
|
| 67 |
};
|
| 68 |
setMessages(prev => [...prev, apiResponse]);
|
| 69 |
|
|
@@ -77,34 +86,34 @@ function App() {
|
|
| 77 |
} finally {
|
| 78 |
setIsLoading(false);
|
| 79 |
}
|
| 80 |
-
// --- FIN DU NOUVEAU BLOC ---
|
| 81 |
};
|
| 82 |
|
| 83 |
return (
|
| 84 |
<div className="app-container">
|
| 85 |
<header className="app-header">
|
| 86 |
-
<h1>Assistant Web
|
| 87 |
</header>
|
| 88 |
|
| 89 |
<div className="chat-window">
|
| 90 |
{messages.map((msg, index) => (
|
| 91 |
<div key={index} className={`message ${msg.sender}`}>
|
| 92 |
<p>{msg.text}</p>
|
| 93 |
-
|
| 94 |
-
{msg.sender === 'bot' && msg.sources && (
|
| 95 |
<div className="sources">
|
| 96 |
<strong>Sources:</strong>
|
| 97 |
<ul>
|
| 98 |
{msg.sources.map((source, i) => (
|
| 99 |
<li key={i}>
|
| 100 |
-
|
|
|
|
| 101 |
</li>
|
| 102 |
))}
|
| 103 |
</ul>
|
| 104 |
</div>
|
| 105 |
)}
|
| 106 |
-
|
| 107 |
-
{msg.sender === 'bot' &&
|
| 108 |
<div className="feedback-buttons">
|
| 109 |
<button onClick={() => handleFeedback(msg, 1)}>👍</button>
|
| 110 |
<button onClick={() => handleFeedback(msg, -1)}>👎</button>
|
|
@@ -120,7 +129,7 @@ function App() {
|
|
| 120 |
type="text"
|
| 121 |
value={prompt}
|
| 122 |
onChange={(e) => setPrompt(e.target.value)}
|
| 123 |
-
placeholder="Posez votre question
|
| 124 |
disabled={isLoading}
|
| 125 |
/>
|
| 126 |
<button type="submit" disabled={isLoading}>Envoyer</button>
|
|
|
|
| 15 |
return;
|
| 16 |
}
|
| 17 |
try {
|
| 18 |
+
let questionText = '';
|
| 19 |
+
const messageIndex = messages.indexOf(message);
|
| 20 |
+
for (let i = messageIndex - 1; i >= 0; i--) {
|
| 21 |
+
if (messages[i].sender === 'user') {
|
| 22 |
+
questionText = messages[i].text;
|
| 23 |
+
break;
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
if (!questionText) {
|
| 27 |
+
alert("Impossible de trouver la question originale.");
|
| 28 |
+
return;
|
| 29 |
+
}
|
| 30 |
await fetch('http://localhost:8000/api/v1/feedback', {
|
| 31 |
method: 'POST',
|
| 32 |
headers: {
|
|
|
|
| 34 |
'Authorization': `Bearer ${token}`
|
| 35 |
},
|
| 36 |
body: JSON.stringify({
|
| 37 |
+
question: questionText,
|
| 38 |
answer: message.text,
|
| 39 |
rating: rating
|
| 40 |
})
|
|
|
|
| 56 |
setPrompt('');
|
| 57 |
setIsLoading(true);
|
| 58 |
|
|
|
|
| 59 |
try {
|
| 60 |
const response = await fetch('http://localhost:8000/api/v1/ask', {
|
| 61 |
method: 'POST',
|
| 62 |
+
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
|
| 63 |
body: JSON.stringify({ question: currentPrompt }),
|
| 64 |
});
|
| 65 |
|
|
|
|
| 69 |
|
| 70 |
const data = await response.json();
|
| 71 |
|
| 72 |
+
const apiResponse = {
|
| 73 |
+
sender: 'bot',
|
| 74 |
text: data.answer,
|
| 75 |
+
sources: data.sources
|
| 76 |
};
|
| 77 |
setMessages(prev => [...prev, apiResponse]);
|
| 78 |
|
|
|
|
| 86 |
} finally {
|
| 87 |
setIsLoading(false);
|
| 88 |
}
|
|
|
|
| 89 |
};
|
| 90 |
|
| 91 |
return (
|
| 92 |
<div className="app-container">
|
| 93 |
<header className="app-header">
|
| 94 |
+
<h1>Assistant Web Éducatif</h1>
|
| 95 |
</header>
|
| 96 |
|
| 97 |
<div className="chat-window">
|
| 98 |
{messages.map((msg, index) => (
|
| 99 |
<div key={index} className={`message ${msg.sender}`}>
|
| 100 |
<p>{msg.text}</p>
|
| 101 |
+
|
| 102 |
+
{msg.sender === 'bot' && msg.sources && msg.sources.length > 0 && (
|
| 103 |
<div className="sources">
|
| 104 |
<strong>Sources:</strong>
|
| 105 |
<ul>
|
| 106 |
{msg.sources.map((source, i) => (
|
| 107 |
<li key={i}>
|
| 108 |
+
{/* LIGNE CORRIGÉE CI-DESSOUS */}
|
| 109 |
+
{source.document}, Page: {source.page}
|
| 110 |
</li>
|
| 111 |
))}
|
| 112 |
</ul>
|
| 113 |
</div>
|
| 114 |
)}
|
| 115 |
+
|
| 116 |
+
{msg.sender === 'bot' && msg.text !== "Désolé, une erreur est survenue. Veuillez réessayer." && (
|
| 117 |
<div className="feedback-buttons">
|
| 118 |
<button onClick={() => handleFeedback(msg, 1)}>👍</button>
|
| 119 |
<button onClick={() => handleFeedback(msg, -1)}>👎</button>
|
|
|
|
| 129 |
type="text"
|
| 130 |
value={prompt}
|
| 131 |
onChange={(e) => setPrompt(e.target.value)}
|
| 132 |
+
placeholder="Posez votre question..."
|
| 133 |
disabled={isLoading}
|
| 134 |
/>
|
| 135 |
<button type="submit" disabled={isLoading}>Envoyer</button>
|
scripts/init_db.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
from
|
| 2 |
-
from
|
| 3 |
-
from
|
| 4 |
|
| 5 |
def init_db():
|
| 6 |
print("Création des tables de la base de données...")
|
|
|
|
| 1 |
+
from config.database import Base, engine
|
| 2 |
+
from models import document
|
| 3 |
+
from models import user
|
| 4 |
|
| 5 |
def init_db():
|
| 6 |
print("Création des tables de la base de données...")
|
scripts/set_user_role.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
# scripts/set_user_role.py
|
| 2 |
import sys
|
| 3 |
from sqlalchemy.orm import Session
|
| 4 |
-
from
|
| 5 |
-
from
|
| 6 |
|
| 7 |
def set_user_role(email: str, role_input: str):
|
| 8 |
db: Session = SessionLocal()
|
|
|
|
| 1 |
# scripts/set_user_role.py
|
| 2 |
import sys
|
| 3 |
from sqlalchemy.orm import Session
|
| 4 |
+
from config.database import SessionLocal
|
| 5 |
+
from models.user import User, RoleEnum
|
| 6 |
|
| 7 |
def set_user_role(email: str, role_input: str):
|
| 8 |
db: Session = SessionLocal()
|