commit e1a68eb65ca3a0adb115ca38b1b4e99028ded9df Author: scayac Date: Fri Jun 13 15:40:54 2025 +0200 Version 1.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1caf493 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv +db.sqlite3 +__pycache__ +migrations diff --git a/README.md b/README.md new file mode 100644 index 0000000..79eaa29 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# IAProf + +IAProf est une application web Django intégrant le module Python d’OpenAI et utilisant les WebSockets pour la communication en temps réel. Ce projet propose un système de connexion simple qui redirige l’utilisateur vers une vue « Hello World » après authentification. + +## Fonctionnalités + +- **Framework Django** : Application basée sur Django, un framework web Python rapide et structurant. +- **Intégration OpenAI** : Utilisation du module Python OpenAI pour des fonctionnalités d’IA. +- **Base SQLite** : Base de données légère et facile à configurer. +- **Authentification** : Page de connexion redirigeant vers une vue « Hello World » si l’utilisateur est authentifié. + +## Structure du projet + +``` +IAProf +├── ia_prof +│ ├── __init__.py +│ ├── asgi.py +│ ├── settings.py +│ ├── urls.py +│ └── wsgi.py +├── main +│ ├── __init__.py +│ ├── admin.py +│ ├── apps.py +│ ├── consumers.py +│ ├── migrations +│ │ └── __init__.py +│ ├── models.py +│ ├── templates +│ │ ├── hello_world.html +│ │ └── login.html +│ ├── tests.py +│ ├── urls.py +│ └── views.py +├── manage.py +├── requirements.txt +├── README.md +└── venv +``` + +## Installation + +1. Clonez le dépôt : + ``` + git clone + cd IAProf + ``` + +2. Créez un environnement virtuel : + ``` + python -m venv venv + ``` + +3. Activez l’environnement virtuel : + - Sous Windows : + ``` + venv\Scripts\activate + ``` + - Sous macOS/Linux : + ``` + source venv/bin/activate + ``` + +4. Installez les dépendances : + ``` + pip install -r requirements.txt + ``` + +5. Appliquez les migrations : + ``` + python manage.py migrate + ``` + +6. Lancez le serveur de développement : + ``` + python manage.py runserver + ``` + +## Configuration de la clé OpenAI + +La clé API OpenAI doit être définie dans le fichier `ia_prof/settings.py` : + +```python +OPENAI_API_KEY = "votre_clé_openai" +``` + +## Utilisation + +- Rendez-vous sur `http://127.0.0.1:8000/login` pour accéder à la page de connexion. +- Après connexion, vous serez redirigé vers la vue « Hello World ». + +Dans le code, utilisez-la via `settings.OPENAI_API_KEY` pour sécuriser et centraliser la configuration. \ No newline at end of file diff --git a/ia_prof/__init__.py b/ia_prof/__init__.py new file mode 100644 index 0000000..82789f2 --- /dev/null +++ b/ia_prof/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left blank. \ No newline at end of file diff --git a/ia_prof/asgi.py b/ia_prof/asgi.py new file mode 100644 index 0000000..f149089 --- /dev/null +++ b/ia_prof/asgi.py @@ -0,0 +1,13 @@ +from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack +import main.routing + +application = ProtocolTypeRouter({ + "http": get_asgi_application(), + "websocket": AuthMiddlewareStack( + URLRouter( + main.routing.websocket_urlpatterns + ) + ), +}) \ No newline at end of file diff --git a/ia_prof/settings.py b/ia_prof/settings.py new file mode 100644 index 0000000..c84cce9 --- /dev/null +++ b/ia_prof/settings.py @@ -0,0 +1,116 @@ +""" +Django settings for IAProf project. + +Generated by 'django-admin startproject' using Django 4.x. +""" + +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.x/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'your-secret-key' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'main', # Your main application +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'ia_prof.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [os.path.join(BASE_DIR, 'main/templates')], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'ia_prof.wsgi.application' + +# Database +# https://docs.djangoproject.com/en/4.x/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + +# Password validation +# https://docs.djangoproject.com/en/4.x/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/4.x/topics/i18n/ + +LANGUAGE_CODE = 'fr' + +TIME_ZONE = 'Europe/Paris' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.x/howto/static-files/ + +STATIC_URL = '/static/' + +# Default primary key field type +# https://docs.djangoproject.com/en/4.x/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +OPENAI_API_KEY = "sk-proj-hrV9Se3D3Vn6ro66AoMFT3BlbkFJ3kgB6P9xQFpcaymQQHFI" \ No newline at end of file diff --git a/ia_prof/urls.py b/ia_prof/urls.py new file mode 100644 index 0000000..7e33409 --- /dev/null +++ b/ia_prof/urls.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from django.urls import path, include, re_path +from django.views.generic.base import RedirectView + +urlpatterns = [ + path('admin/', admin.site.urls), + re_path(r'^$', RedirectView.as_view(url='/login/', permanent=False)), + path('', include('main.urls')), +] \ No newline at end of file diff --git a/ia_prof/wsgi.py b/ia_prof/wsgi.py new file mode 100644 index 0000000..58bdde2 --- /dev/null +++ b/ia_prof/wsgi.py @@ -0,0 +1,15 @@ +""" +WSGI config for IAProf project. + +It exposes the WSGI callable as a module-level variable named `application`. + +For more information on this file, see +https://docs.djangoproject.com/en/stable/howto/deployment/wsgi/ +""" + +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ia_prof.settings') + +application = get_wsgi_application() \ No newline at end of file diff --git a/main/__init__.py b/main/__init__.py new file mode 100644 index 0000000..82789f2 --- /dev/null +++ b/main/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left blank. \ No newline at end of file diff --git a/main/admin.py b/main/admin.py new file mode 100644 index 0000000..005ebc9 --- /dev/null +++ b/main/admin.py @@ -0,0 +1,4 @@ +from django.contrib import admin +from .models import UserProfile + +admin.site.register(UserProfile) \ No newline at end of file diff --git a/main/apps.py b/main/apps.py new file mode 100644 index 0000000..1f6e9cb --- /dev/null +++ b/main/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + +class MainConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'main' \ No newline at end of file diff --git a/main/consumers.py b/main/consumers.py new file mode 100644 index 0000000..10116d6 --- /dev/null +++ b/main/consumers.py @@ -0,0 +1,17 @@ +from channels.generic.websocket import AsyncWebsocketConsumer +import json + +class ChatConsumer(AsyncWebsocketConsumer): + async def connect(self): + await self.accept() + + async def disconnect(self, close_code): + pass + + async def receive(self, text_data): + text_data_json = json.loads(text_data) + message = text_data_json['message'] + + await self.send(text_data=json.dumps({ + 'message': message + })) \ No newline at end of file diff --git a/main/models.py b/main/models.py new file mode 100644 index 0000000..1ee0980 --- /dev/null +++ b/main/models.py @@ -0,0 +1,9 @@ +from django.db import models +from django.contrib.auth.models import User + +class UserProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + # Ajoutez d'autres champs personnalisés ici si besoin + + def __str__(self): + return self.user.username \ No newline at end of file diff --git a/main/openAIAppreciations.py b/main/openAIAppreciations.py new file mode 100644 index 0000000..83773b5 --- /dev/null +++ b/main/openAIAppreciations.py @@ -0,0 +1,90 @@ +from json import dumps +import pdfplumber +import openai +from PyPDF2 import PdfReader +from django.conf import settings + +openai.api_key = settings.OPENAI_API_KEY + +def convertPdfToJSON(file): + eleve_id = 1 + eleves = [] + + pdf = pdfplumber.open(file) + for page in pdf.pages: + + curent_page = [] + write = False + eleve_found = False + app_gen = "" + + current_eleve = {"eleve_id": "", + "eleve": "", + "app_generale": "", + "appreciations": []} + + # lecture ligne par ligne + lines = page.extract_text().split('\n') + for i, line in enumerate(lines): + + # Attendre la ligne qui se termine par " Trimestre" + if not write: + if line.strip().endswith("Trimestre"): + write = True + continue + + # Enregistrer la ligne suivante (nom de l'élève) + if write and not eleve_found: + eleve_nom = line.strip() + # Vérifier si l'élève n'est pas déjà dans la liste et que le bulletin ne tient pas sur plusieurs pages + if not any(e["eleve"] == eleve_nom for e in eleves) and not line.strip().endswith("élèves)"): + current_eleve["eleve_id"] = f"ELEVE{eleve_id}" + current_eleve["eleve"] = eleve_nom + eleve_id += 1 + eleve_found = True + else: + # Si l'élève est déjà trouvé, on prend le dernier élève de la liste auquel on va ajouter les données de la nouvelle page + current_eleve = eleves.pop() + eleve_found = True + continue + + # Si on a trouvé l'élève, on cherche l'appréciation générale + if eleve_found and line.strip().startswith("Appréciation globale :"): + app_gen = line.strip().split(":", 1)[-1].strip() + continue + + # Si on a trouvé l'appréciation générale, on continue à la lire jusqu'à la ligne "Le Chef d'établissement" + if app_gen: + if not line.strip().startswith("Le Chef d'établissement"): + app_gen += line.strip()+" " + else: + current_eleve["app_generale"] = app_gen + continue + + # récupération des tableaux + tables = page.extract_tables() + tables = tables[0][2:] + for table in tables: + if table[1] != None and table[-1] != None: + current_eleve["appreciations"].append({"matiere": table[1].split('\n')[0], "appreciation": table[-1]}) + + eleves.append(current_eleve) + return eleves + +def generer_appreciation_pour_eleve(eleve, eleves_json): + eleve_data = next((e for e in eleves_json if e["eleve"] == eleve), None) + if not eleve_data: + return f"Aucun élève trouvé avec le nom {eleve}" + + completion = openai.ChatCompletion.create( + model="ft:gpt-4o-2024-08-06:personal:app-gen-gangneux2:AYJecsON", + messages=[ + {"role": "system", "content": "Rédige une appréciation générale (500 caractères max) pour cet élève"}, + {"role": "user", "content": dumps(eleve_data["appreciations"])} + ], + temperature=0.7, + presence_penalty=0.6, + frequency_penalty=0.6, + top_p=0.5) + + return completion.choices[0].message.content diff --git a/main/openaiApprecioations copy.py b/main/openaiApprecioations copy.py new file mode 100644 index 0000000..2d4ab06 --- /dev/null +++ b/main/openaiApprecioations copy.py @@ -0,0 +1,129 @@ +from openai import OpenAI +from PyPDF2 import PdfReader +from django.conf import settings + +liste_eleves = {} +client = OpenAI(api_key=settings.OPENAI_API_KEY) + +def split_by_eleve(text): + """ + Découpe le texte en une liste de chaînes, chaque élément commençant par 'ELEVE'. + """ + import re + # On split sur chaque occurrence de 'ELEVE' suivie d'un ou plusieurs chiffres + parts = re.split(r'(ELEVE\d+)', text) + result = [] + for i in range(1, len(parts), 2): + # parts[i] est 'ELEVEid', parts[i+1] est le texte associé + bloc = parts[i] + parts[i+1] if i+1 < len(parts) else parts[i] + result.append(bloc.strip()) + return result + +def anonymiserPdf(file, eleves): + eleve_id = 1 + pages_content = "" + + for page in file.pages: + lines = page.extract_text().split('\n') + curent_page = [] + write = False + eleve_found = False + + for i, line in enumerate(lines): + # Attendre la ligne qui se termine par " Trimestre" + if not write: + if line.strip().endswith("Trimestre"): + write = True + continue + + # Enregistrer la ligne suivante (nom de l'élève) + if write and not eleve_found: + eleve_nom = line.strip() + if eleve_nom not in eleves: + eleves[eleve_nom] = f"ELEVE{eleve_id}" + eleve_id += 1 + curent_page.append(eleves[eleve_nom] + "\n") + eleve_found = True + continue + + # Attendre la ligne qui commence par "Appréciations" + if eleve_found: + if line.strip().startswith("Appréciations"): + # Commencer à écrire les lignes suivantes + for l in lines[i+1:]: + if l.startswith("M.") or l.startswith("Mme"): + continue + curent_page.append(l + "\n") + break # Fin du traitement de cette page + + pages_content += "".join(curent_page) + return pages_content + +def get_eleve_name(text, eleves): + """ + Extrait le nom de l'élève à partir du texte. + """ + for line in text.split('\n'): + line = line.strip() + if line in eleves: + return eleves[line] + return None + +def replace_eleve_ids_with_names(text, eleves): + # Inverse the eleves dict to map ELEVEid -> nom prénom + id_to_name = {v: k for k, v in eleves.items()} + # Trier les IDs par longueur décroissante pour éviter les remplacements partiels + for eleve_id in sorted(id_to_name.keys(), key=len, reverse=True): + nom = id_to_name[eleve_id] + if eleve_id in text: + text = text.replace(eleve_id, nom) + return text + +def generatationAppreciations(appreciations_path, modele_path=None): + + if (modele_path): + modele = PdfReader(modele_path) + modele_anonyme = anonymiserPdf(modele, liste_eleves) + + appreciations = PdfReader(appreciations_path) + appreciation_anonyme = anonymiserPdf(appreciations, liste_eleves) + tableau_appreciations = split_by_eleve(appreciation_anonyme) + + responses = [] + #debug + tableau_appreciations = tableau_appreciations[:3] + for eleve in tableau_appreciations: + if modele_path: + # Si un modèle est fourni, on utilise le modèle pour chaque élève + data = [ + { + "role": "developer", + "content": "Tu dois générer des appréciations générales de bulletins avec le style de l'exemple suivant (500 caractères max)" + },{ + "role": "assistant", + "content": modele_anonyme + }, + { + "role": "user", + "content": eleve + }] + else: + data = [ + { + "role": "developer", + "content": "Tu dois générer des appréciations générales de bulletins (500 caractères max)" + }, + { + "role": "user", + "content": eleve + }] + completion = client.chat.completions.create( + model="gpt-4.1-mini", + messages=data, + temperature=1, + top_p=1) + resultat = "" + get_eleve_name(completion.choices[0].message.content, liste_eleves) + "\n" + resultat += replace_eleve_ids_with_names(completion.choices[0].message.content, liste_eleves) + responses.append(resultat) + + return responses diff --git a/main/templates/generation.html b/main/templates/generation.html new file mode 100644 index 0000000..43a20ce --- /dev/null +++ b/main/templates/generation.html @@ -0,0 +1,136 @@ + + + + + + + IAProf + + + + + + + + +
+
+
+
+
+
+

Choix du fichier de bulletins à traiter :

+
+
+ {% if success %} +
Fichiers reçus avec succès !
+ Appréciations : {{ appreciations_filename }}
+ {% if modele_filename %}Modèle : {{ modele_filename }}
{% endif %} + Résultat du traitement : {{ resultat }} +
+ {% endif %} + +
+ {% csrf_token %} +
+ + +
+ +
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/main/templates/login.html b/main/templates/login.html new file mode 100644 index 0000000..f9bf88c --- /dev/null +++ b/main/templates/login.html @@ -0,0 +1,140 @@ + + + + + + + Connexion - IAProf + + + + + + + +
+
+
+
+
+
+
+
+

Connexion

+
+
+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/main/templates/resultat_appreciations.html b/main/templates/resultat_appreciations.html new file mode 100644 index 0000000..7c64419 --- /dev/null +++ b/main/templates/resultat_appreciations.html @@ -0,0 +1,199 @@ + + + + + + IAProf + + + + + + +
+
+
+
+
+
+

Génération des appréciations

+
+
+
+ + +
+ + + + + + + + + + {% if appreciations_json and appreciations_json|length > 0 %} + {% for appreciation in appreciations_json %} + + + + + {% endfor %} + {% else %} + + + + {% endif %} + +
ÉlèveAppréciation
{{ appreciation.eleve }}
Aucune appréciation générée.
+ +
+
+
+
+
+
+ + + diff --git a/main/tests.py b/main/tests.py new file mode 100644 index 0000000..3a0896c --- /dev/null +++ b/main/tests.py @@ -0,0 +1,26 @@ +from django.test import TestCase +from django.urls import reverse +from django.contrib.auth import get_user_model + +User = get_user_model() + +class LoginViewTests(TestCase): + def setUp(self): + self.username = 'testuser' + self.password = 'testpassword' + self.user = User.objects.create_user(username=self.username, password=self.password) + + def test_login_view_redirects_authenticated_user(self): + self.client.login(username=self.username, password=self.password) + response = self.client.get(reverse('hello_world')) + self.assertEqual(response.status_code, 200) + + def test_login_view_renders_login_template_for_anonymous_user(self): + response = self.client.get(reverse('login')) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'login.html') + + def test_logout_redirects_to_login(self): + self.client.login(username=self.username, password=self.password) + response = self.client.post(reverse('logout')) + self.assertRedirects(response, reverse('login')) \ No newline at end of file diff --git a/main/urls.py b/main/urls.py new file mode 100644 index 0000000..a838c73 --- /dev/null +++ b/main/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from .views import login_view, generation, resultat_appreciations, logout_view, generer_appreciation_ajax + +urlpatterns = [ + path('login/', login_view, name='login'), + path('generation/', generation, name='generation'), + path('resultat/', resultat_appreciations, name='resultat_appreciations'), + path('logout/', logout_view, name='logout'), + path('generer_appreciation_ajax/', generer_appreciation_ajax, name='generer_appreciation_ajax'), +] \ No newline at end of file diff --git a/main/views.py b/main/views.py new file mode 100644 index 0000000..b966da7 --- /dev/null +++ b/main/views.py @@ -0,0 +1,79 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required +from django.conf import settings +from main.openAIAppreciations import * +from django.urls import reverse +from django.http import JsonResponse +import os +import uuid +import json + +def login_view(request): + if request.method == 'POST': + username = request.POST['username'] + password = request.POST['password'] + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + return redirect('generation') + return render(request, 'login.html') + +@login_required(login_url='/login/') +def generation(request): + if request.method == 'POST': + appreciations_pdf = request.FILES.get('appreciations_pdf') + appreciations_path = None + tmp_dir = os.path.join(settings.BASE_DIR, 'main', 'tmp') + os.makedirs(tmp_dir, exist_ok=True) + if appreciations_pdf: + ext = os.path.splitext(appreciations_pdf.name)[1] + random_name = f"appreciations_{uuid.uuid4().hex}{ext}" + appreciations_path = os.path.join(tmp_dir, random_name) + with open(appreciations_path, 'wb+') as destination: + for chunk in appreciations_pdf.chunks(): + destination.write(chunk) + # Appel du traitement après upload + appreciations_json = convertPdfToJSON(appreciations_path) + if appreciations_path and os.path.exists(appreciations_path): + os.remove(appreciations_path) + request.session['appreciations_json'] = appreciations_json + return redirect('resultat_appreciations') + return render(request, 'generation.html') + +@login_required(login_url='/login/') +def resultat_appreciations(request): + appreciations_result = request.session.get('appreciations_json', []) + return render(request, 'resultat_appreciations.html', {'appreciations_json': appreciations_result}) + +@login_required(login_url='/login/') +def generer_appreciation_ajax(request): + if request.method == 'POST': + data = json.loads(request.body) + eleve = data.get('eleve') + modele = data.get('modele') + appreciation = generer_appreciation_pour_eleve(eleve, request.session.get('appreciations_json', []), modele) + return JsonResponse({'appreciation': appreciation}) + return JsonResponse({'error': 'Méthode non autorisée'}, status=405) + +def logout_view(request): + logout(request) + return redirect('login') + +def generer_appreciation_pour_eleve(eleve, eleves_json, modele=None): + eleve_data = next((e for e in eleves_json if e["eleve"] == eleve), None) + if not eleve_data: + return f"Aucun élève trouvé avec le nom {eleve}" + if modele is None: + modele = "ft:gpt-4o-2024-08-06:personal:app-gen-gangneux2:AYJecsON" + completion = openai.ChatCompletion.create( + model=modele, + messages=[ + {"role": "system", "content": "Rédige une appréciation générale (500 caractères max) pour cet élève"}, + {"role": "user", "content": str(eleve_data["appreciations"])} + ], + temperature=0.7, + presence_penalty=0.6, + frequency_penalty=0.6, + top_p=0.5) + return completion.choices[0].message.content \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..d5f2e9b --- /dev/null +++ b/manage.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +import os +import sys + +def main(): + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ia_prof.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..acb8323 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Django==4.2 +djangorestframework==3.14.0 +openai==0.27.0 +PyPDF2 +openai +pdfplumber \ No newline at end of file