Spaces:
Runtime error
Runtime error
Upload 25 files
Browse files- app/__init__.py +24 -0
- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/__pycache__/config.cpython-313.pyc +0 -0
- app/__pycache__/decorators.cpython-313.pyc +0 -0
- app/__pycache__/models.cpython-313.pyc +0 -0
- app/config.py +7 -0
- app/decorators.py +16 -0
- app/models.py +30 -0
- app/routes/__init__.py +0 -0
- app/routes/__pycache__/__init__.cpython-313.pyc +0 -0
- app/routes/__pycache__/admin.cpython-313.pyc +0 -0
- app/routes/__pycache__/auth.cpython-313.pyc +0 -0
- app/routes/__pycache__/chat.cpython-313.pyc +0 -0
- app/routes/__pycache__/profile.cpython-313.pyc +0 -0
- app/routes/admin.py +14 -0
- app/routes/auth.py +57 -0
- app/routes/chat.py +48 -0
- app/routes/profile.py +15 -0
- app/scripts/load_documents.py +70 -0
- app/services/__init__.py +0 -0
- app/services/__pycache__/__init__.cpython-313.pyc +0 -0
- app/services/__pycache__/auth_utils.cpython-313.pyc +0 -0
- app/services/__pycache__/chatbot.cpython-313.pyc +0 -0
- app/services/auth_utils.py +7 -0
- app/services/chatbot.py +25 -0
app/__init__.py
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask
|
2 |
+
from flask_sqlalchemy import SQLAlchemy
|
3 |
+
from flask_bcrypt import Bcrypt
|
4 |
+
from flask_jwt_extended import JWTManager
|
5 |
+
|
6 |
+
db = SQLAlchemy()
|
7 |
+
bcrypt = Bcrypt()
|
8 |
+
jwt = JWTManager()
|
9 |
+
|
10 |
+
def create_app():
|
11 |
+
app = Flask(__name__)
|
12 |
+
app.config.from_object('app.config.Config')
|
13 |
+
|
14 |
+
db.init_app(app)
|
15 |
+
bcrypt.init_app(app)
|
16 |
+
jwt.init_app(app)
|
17 |
+
|
18 |
+
from app.routes import auth, chat, admin, profile
|
19 |
+
app.register_blueprint(auth.bp)
|
20 |
+
app.register_blueprint(chat.bp)
|
21 |
+
app.register_blueprint(admin.bp)
|
22 |
+
app.register_blueprint(profile.bp)
|
23 |
+
|
24 |
+
return app
|
app/__pycache__/__init__.cpython-313.pyc
ADDED
Binary file (1.33 kB). View file
|
|
app/__pycache__/config.cpython-313.pyc
ADDED
Binary file (665 Bytes). View file
|
|
app/__pycache__/decorators.cpython-313.pyc
ADDED
Binary file (1.4 kB). View file
|
|
app/__pycache__/models.cpython-313.pyc
ADDED
Binary file (3.09 kB). View file
|
|
app/config.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
class Config:
|
2 |
+
SECRET_KEY = 'Hala_Madrid'
|
3 |
+
JWT_SECRET_KEY = 'Hala_Madrid'
|
4 |
+
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:@localhost/loimaroc'
|
5 |
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
6 |
+
PERSIST_DIR = "chroma_storage"
|
7 |
+
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
app/decorators.py
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from functools import wraps
|
2 |
+
from flask_jwt_extended import verify_jwt_in_request, get_jwt
|
3 |
+
from flask import jsonify
|
4 |
+
|
5 |
+
def roles_required(*roles):
|
6 |
+
def wrapper(fn):
|
7 |
+
@wraps(fn)
|
8 |
+
def decorator(*args, **kwargs):
|
9 |
+
verify_jwt_in_request()
|
10 |
+
claims = get_jwt()
|
11 |
+
user_roles = claims.get("roles", [])
|
12 |
+
if not any(role in roles for role in user_roles):
|
13 |
+
return jsonify(msg="Forbidden: insufficient permissions"), 403
|
14 |
+
return fn(*args, **kwargs)
|
15 |
+
return decorator
|
16 |
+
return wrapper
|
app/models.py
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app import db
|
2 |
+
from datetime import datetime
|
3 |
+
|
4 |
+
user_roles = db.Table('user_role',
|
5 |
+
db.Column('user_id', db.Integer, db.ForeignKey('users.id')),
|
6 |
+
db.Column('role_id', db.Integer, db.ForeignKey('role.id'))
|
7 |
+
)
|
8 |
+
|
9 |
+
class User(db.Model):
|
10 |
+
__tablename__ = 'users'
|
11 |
+
id = db.Column(db.Integer, primary_key=True)
|
12 |
+
first_name = db.Column(db.String(150))
|
13 |
+
last_name = db.Column(db.String(150))
|
14 |
+
email = db.Column(db.String(255), unique=True, nullable=False)
|
15 |
+
password = db.Column(db.String(255), nullable=False)
|
16 |
+
roles = db.relationship('Role', secondary=user_roles, backref='users')
|
17 |
+
|
18 |
+
class Role(db.Model):
|
19 |
+
id = db.Column(db.Integer, primary_key=True)
|
20 |
+
name = db.Column(db.String(50), unique=True, nullable=False)
|
21 |
+
|
22 |
+
class Question(db.Model):
|
23 |
+
__tablename__ = 'questions'
|
24 |
+
id = db.Column(db.Integer, primary_key=True)
|
25 |
+
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
|
26 |
+
content = db.Column(db.Text, nullable=False)
|
27 |
+
date = db.Column(db.DateTime, default=datetime.utcnow)
|
28 |
+
status = db.Column(db.String(20), default='pending')
|
29 |
+
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
30 |
+
user = db.relationship('User', backref=db.backref('questions', lazy=True))
|
app/routes/__init__.py
ADDED
File without changes
|
app/routes/__pycache__/__init__.cpython-313.pyc
ADDED
Binary file (168 Bytes). View file
|
|
app/routes/__pycache__/admin.cpython-313.pyc
ADDED
Binary file (1.09 kB). View file
|
|
app/routes/__pycache__/auth.cpython-313.pyc
ADDED
Binary file (3.72 kB). View file
|
|
app/routes/__pycache__/chat.cpython-313.pyc
ADDED
Binary file (2.26 kB). View file
|
|
app/routes/__pycache__/profile.cpython-313.pyc
ADDED
Binary file (1.09 kB). View file
|
|
app/routes/admin.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint, jsonify
|
2 |
+
from app.models import User
|
3 |
+
from app.decorators import roles_required
|
4 |
+
|
5 |
+
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
6 |
+
|
7 |
+
@bp.route('/users', methods=['GET'])
|
8 |
+
@roles_required('admin')
|
9 |
+
def all_users():
|
10 |
+
users = User.query.all()
|
11 |
+
return jsonify([
|
12 |
+
{"id": u.id, "email": u.email, "roles": [r.name for r in u.roles]}
|
13 |
+
for u in users
|
14 |
+
])
|
app/routes/auth.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint, request, jsonify
|
2 |
+
from app.models import db, User, Role
|
3 |
+
from flask_bcrypt import generate_password_hash
|
4 |
+
from app import db, bcrypt
|
5 |
+
from flask_jwt_extended import create_access_token
|
6 |
+
from datetime import timedelta
|
7 |
+
bp = Blueprint('auth', __name__, url_prefix='/auth')
|
8 |
+
|
9 |
+
@bp.route('/register', methods=['POST'])
|
10 |
+
def register():
|
11 |
+
data = request.get_json()
|
12 |
+
|
13 |
+
email = data.get('email')
|
14 |
+
password = data.get('password')
|
15 |
+
first_name = data.get('first_name')
|
16 |
+
last_name = data.get('last_name')
|
17 |
+
|
18 |
+
if not email or not password:
|
19 |
+
return jsonify({"error": "Missing email or password"}), 400
|
20 |
+
|
21 |
+
if User.query.filter_by(email=email).first():
|
22 |
+
return jsonify({"error": "User already exists"}), 409
|
23 |
+
|
24 |
+
hashed_password = generate_password_hash(password).decode('utf-8')
|
25 |
+
user = User(email=email, password=hashed_password, first_name=first_name, last_name=last_name)
|
26 |
+
|
27 |
+
# ROLE LOGIC
|
28 |
+
if email == '[email protected]':
|
29 |
+
role = Role.query.filter_by(name='admin').first()
|
30 |
+
elif email == '[email protected]':
|
31 |
+
role = Role.query.filter_by(name='expert').first()
|
32 |
+
else:
|
33 |
+
role = Role.query.filter_by(name='client').first()
|
34 |
+
|
35 |
+
if not role:
|
36 |
+
return jsonify({"error": f"Role not found in DB"}), 500
|
37 |
+
|
38 |
+
user.roles.append(role)
|
39 |
+
db.session.add(user)
|
40 |
+
db.session.commit()
|
41 |
+
|
42 |
+
return jsonify({"message": "User registered successfully"}), 201
|
43 |
+
@bp.route('/login', methods=['POST'])
|
44 |
+
def login():
|
45 |
+
data = request.get_json()
|
46 |
+
email = data.get('email')
|
47 |
+
password = data.get('password')
|
48 |
+
|
49 |
+
if not email or not password:
|
50 |
+
return jsonify({"error": "Email and password are required"}), 400
|
51 |
+
|
52 |
+
user = User.query.filter_by(email=email).first()
|
53 |
+
if not user or not bcrypt.check_password_hash(user.password, password):
|
54 |
+
return jsonify({"error": "Invalid credentials"}), 401
|
55 |
+
|
56 |
+
access_token = create_access_token(identity=str(user.id), expires_delta=timedelta(days=1))
|
57 |
+
return jsonify({"access_token": access_token}), 200
|
app/routes/chat.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint, request, jsonify
|
2 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
3 |
+
from app.services.chatbot import ask
|
4 |
+
from app.models import Question, db
|
5 |
+
from datetime import datetime
|
6 |
+
|
7 |
+
bp = Blueprint('chat', __name__, url_prefix='/chat')
|
8 |
+
|
9 |
+
@bp.route('/ask', methods=['POST'])
|
10 |
+
@jwt_required()
|
11 |
+
def ask_question():
|
12 |
+
user_id = str(get_jwt_identity())
|
13 |
+
data = request.get_json()
|
14 |
+
question_text = data.get('question')
|
15 |
+
|
16 |
+
if not question_text:
|
17 |
+
return jsonify({'error': 'Missing question'}), 400
|
18 |
+
|
19 |
+
question = Question(
|
20 |
+
user_id=user_id,
|
21 |
+
content=question_text,
|
22 |
+
status='pending'
|
23 |
+
)
|
24 |
+
db.session.add(question)
|
25 |
+
db.session.commit()
|
26 |
+
|
27 |
+
try:
|
28 |
+
k = int(data.get("k", 6)) # if k is missing, fallback to 6
|
29 |
+
results = ask(question_text, k=k)
|
30 |
+
question.status = 'answered'
|
31 |
+
except Exception as e:
|
32 |
+
results = []
|
33 |
+
question.status = 'error'
|
34 |
+
|
35 |
+
question.updated_at = datetime.utcnow()
|
36 |
+
db.session.commit()
|
37 |
+
|
38 |
+
return jsonify({
|
39 |
+
"question": {
|
40 |
+
"id": question.id,
|
41 |
+
"user_id": question.user_id,
|
42 |
+
"content": question.content,
|
43 |
+
"date": question.date.isoformat(),
|
44 |
+
"status": question.status,
|
45 |
+
"updated_at": question.updated_at.isoformat()
|
46 |
+
},
|
47 |
+
"results": results
|
48 |
+
}), 200
|
app/routes/profile.py
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Blueprint, jsonify
|
2 |
+
from flask_jwt_extended import jwt_required, get_jwt_identity
|
3 |
+
from app.models import User
|
4 |
+
|
5 |
+
bp = Blueprint('profile', __name__, url_prefix='/profile')
|
6 |
+
|
7 |
+
@bp.route('/', methods=['GET'])
|
8 |
+
@jwt_required()
|
9 |
+
def profile():
|
10 |
+
user = User.query.get(get_jwt_identity())
|
11 |
+
return jsonify(
|
12 |
+
email=user.email,
|
13 |
+
name=f"{user.first_name} {user.last_name}",
|
14 |
+
roles=[r.name for r in user.roles]
|
15 |
+
)
|
app/scripts/load_documents.py
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import pandas as pd
|
3 |
+
from langchain.docstore.document import Document
|
4 |
+
from langchain_community.vectorstores import Chroma
|
5 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
6 |
+
|
7 |
+
# Configurations
|
8 |
+
DATA_DIR = "data"
|
9 |
+
PERSIST_DIR = "chroma_storage"
|
10 |
+
EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
|
11 |
+
|
12 |
+
def parse_pages(pages_str):
|
13 |
+
if pd.isna(pages_str):
|
14 |
+
return []
|
15 |
+
try:
|
16 |
+
pages = eval(str(pages_str))
|
17 |
+
return pages if isinstance(pages, list) else [pages]
|
18 |
+
except:
|
19 |
+
try:
|
20 |
+
return [int(p.strip()) for p in str(pages_str).split(",") if p.strip().isdigit()]
|
21 |
+
except:
|
22 |
+
return []
|
23 |
+
|
24 |
+
def load_csv(filepath: str) -> list:
|
25 |
+
df = pd.read_csv(filepath)
|
26 |
+
df.columns = [c.strip().lower() for c in df.columns]
|
27 |
+
|
28 |
+
documents = []
|
29 |
+
for _, row in df.iterrows():
|
30 |
+
text = row.get('contenu', '')
|
31 |
+
if pd.isna(text) or not str(text).strip():
|
32 |
+
continue
|
33 |
+
|
34 |
+
meta = {
|
35 |
+
'doc': (
|
36 |
+
row.get('doc').strip()
|
37 |
+
if pd.notna(row.get('doc')) and str(row.get('doc')).strip()
|
38 |
+
else os.path.splitext(os.path.basename(filepath))[0]
|
39 |
+
),
|
40 |
+
'titre': row.get('titre', ''),
|
41 |
+
'chapitre': row.get('chapitre', ''),
|
42 |
+
'article': row.get('article', ''),
|
43 |
+
'sous_titre1': row.get('sous_titre1', ''),
|
44 |
+
'sous_titre2': row.get('sous_titre2', ''),
|
45 |
+
'sous_titre3': row.get('sous_titre3', ''),
|
46 |
+
'pages': ", ".join(map(str, parse_pages(row.get('pages', '')))) # <- stringify to avoid Chroma error
|
47 |
+
}
|
48 |
+
|
49 |
+
documents.append(Document(page_content=str(text).strip(), metadata=meta))
|
50 |
+
|
51 |
+
print(f"π Loaded: {os.path.basename(filepath)} β {len(documents)} documents")
|
52 |
+
return documents
|
53 |
+
|
54 |
+
def process_files():
|
55 |
+
all_docs = []
|
56 |
+
for file in os.listdir(DATA_DIR):
|
57 |
+
if file.endswith(".csv"):
|
58 |
+
full_path = os.path.join(DATA_DIR, file)
|
59 |
+
all_docs.extend(load_csv(full_path))
|
60 |
+
|
61 |
+
print(f"π Total: {len(all_docs)} full documents")
|
62 |
+
|
63 |
+
embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
|
64 |
+
db = Chroma.from_documents(all_docs, embedding=embeddings, persist_directory=PERSIST_DIR)
|
65 |
+
db.persist()
|
66 |
+
|
67 |
+
print(f"β
Done! Stored in {PERSIST_DIR}")
|
68 |
+
|
69 |
+
if __name__ == "__main__":
|
70 |
+
process_files()
|
app/services/__init__.py
ADDED
File without changes
|
app/services/__pycache__/__init__.cpython-313.pyc
ADDED
Binary file (170 Bytes). View file
|
|
app/services/__pycache__/auth_utils.cpython-313.pyc
ADDED
Binary file (662 Bytes). View file
|
|
app/services/__pycache__/chatbot.cpython-313.pyc
ADDED
Binary file (1.56 kB). View file
|
|
app/services/auth_utils.py
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from app import bcrypt
|
2 |
+
|
3 |
+
def hash_password(password):
|
4 |
+
return bcrypt.generate_password_hash(password).decode('utf-8')
|
5 |
+
|
6 |
+
def check_password(hashed, password):
|
7 |
+
return bcrypt.check_password_hash(hashed, password)
|
app/services/chatbot.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# app/services/chatbot.py
|
2 |
+
from langchain_chroma import Chroma
|
3 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
4 |
+
from app.config import Config
|
5 |
+
|
6 |
+
PERSIST_DIR = Config.PERSIST_DIR
|
7 |
+
EMBEDDING_MODEL = Config.EMBEDDING_MODEL
|
8 |
+
|
9 |
+
embeddings = HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
|
10 |
+
db = Chroma(persist_directory=PERSIST_DIR, embedding_function=embeddings)
|
11 |
+
|
12 |
+
def ask(query, k=6):
|
13 |
+
results = db.similarity_search_with_score(query, k=k)
|
14 |
+
response = []
|
15 |
+
for doc, score in results:
|
16 |
+
meta = doc.metadata
|
17 |
+
response.append({
|
18 |
+
"titre": meta.get("titre", ""),
|
19 |
+
"chapitre": meta.get("chapitre", ""),
|
20 |
+
"article": meta.get("article", ""),
|
21 |
+
"contenu": doc.page_content,
|
22 |
+
"doc": meta.get("doc", ""),
|
23 |
+
"pages": meta.get("pages", []) if isinstance(meta.get("pages"), list) else [meta.get("pages")] if meta.get("pages") else []
|
24 |
+
})
|
25 |
+
return response
|