diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..c4ee82c
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,74 @@
+amqp==5.2.0
+annotated-types==0.7.0
+anyio==4.4.0
+asgiref==3.8.1
+attrs==24.1.0
+beautifulsoup4==4.12.3
+billiard==4.2.0
+bs4==0.0.2
+celery==5.4.0
+certifi==2024.7.4
+cffi==1.17.0
+charset-normalizer==3.3.2
+click==8.1.7
+click-didyoumean==0.3.1
+click-plugins==1.1.1
+click-repl==0.3.0
+cron-descriptor==1.4.3
+cryptography==43.0.0
+distro==1.9.0
+Django==5.0.8
+django-admin-interface==0.28.8
+django-celery-beat==2.6.0
+django-ckeditor==6.7.1
+django-ckeditor-5==0.2.13
+django-colorfield==0.11.0
+django-cors-headers==4.4.0
+django-filter==24.2
+django-jazzmin==3.0.0
+django-js-asset==2.2.0
+django-shortcuts==1.6
+django-sortedm2m==4.0.0
+django-timezone-field==7.0
+django-tinymce==4.1.0
+djangorestframework==3.15.2
+drf-spectacular==0.27.2
+h11==0.14.0
+httpcore==1.0.5
+httpx==0.27.0
+idna==3.7
+inflection==0.5.1
+jiter==0.8.2
+jsonschema==4.23.0
+jsonschema-specifications==2023.12.1
+kombu==5.4.0
+openai==1.57.2
+pillow==10.4.0
+prompt_toolkit==3.0.47
+pycparser==2.22
+pydantic==2.10.3
+pydantic_core==2.27.1
+pyOpenSSL==24.2.1
+python-crontab==3.2.0
+python-dateutil==2.9.0.post0
+python-decouple==3.8
+python-dotenv==1.0.1
+python-slugify==8.0.4
+PyYAML==6.0.1
+redis==5.0.8
+referencing==0.35.1
+requests==2.32.3
+rpds-py==0.19.1
+six==1.16.0
+sniffio==1.3.1
+soupsieve==2.6
+sqlparse==0.5.1
+text-unidecode==1.3
+tqdm==4.67.1
+typing_extensions==4.12.2
+tzdata==2024.1
+uritemplate==4.1.1
+urllib3==2.2.2
+vine==5.1.0
+wcwidth==0.2.13
+whitenoise==6.7.0
diff --git a/server/apps/research/admin/article_admin.py b/server/apps/research/admin/article_admin.py
index 7dc624c..1a29c07 100644
--- a/server/apps/research/admin/article_admin.py
+++ b/server/apps/research/admin/article_admin.py
@@ -4,6 +4,11 @@
from apps.research.models import Article, ArticleSlugHistory
from tinymce.widgets import TinyMCE
from .slug_history import current_slug_history
+from django.conf import settings
+from django.http import JsonResponse
+from django.urls import path
+from ..services.gpt_service import GPTService
+import asyncio
class ArticleForm(forms.ModelForm):
class Meta:
@@ -21,13 +26,64 @@ def __init__(self, *args, **kwargs):
Q(pk=self.instance.pk) | Q(status='draft')
).order_by('-scheduled_publish_time')
- self.fields['acknowledgement'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "acknowledgement_richtext_field", 'placeholder': "Enter Acknowledgement here"})
- self.fields['content'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "content_richtext_field", 'placeholder': "Enter Article Content here"})
+ # Configure TinyMCE widgets
+ self.fields['acknowledgement'].widget = TinyMCE(attrs={
+ 'cols': 80,
+ 'rows': 30,
+ 'id': "acknowledgement_richtext_field",
+ 'placeholder': "Enter Acknowledgement here"
+ })
+ self.fields['content'].widget = TinyMCE(attrs={
+ 'cols': 80,
+ 'rows': 30,
+ 'id': "content_richtext_field",
+ 'placeholder': "Enter Article Content here"
+ })
+ self.fields['gpt_summary'].widget = TinyMCE(attrs={
+ 'cols': 80,
+ 'rows': 15,
+ 'id': "gpt_summary_richtext_field",
+ 'placeholder': "GPT-generated summary will appear here"
+ })
class ArticleAdmin(admin.ModelAdmin):
"""Admin interface for the Article model."""
form = ArticleForm
+ def __init__(self, model, admin_site):
+ super().__init__(model, admin_site)
+ self.gpt_service = GPTService()
+
+ def get_urls(self):
+ urls = super().get_urls()
+ custom_urls = [
+ path('generate-summary/', self.generate_summary_view, name='generate-summary'),
+ ]
+ return custom_urls + urls
+
+ async def _generate_summary(self, content: str) -> str:
+ system_prompt = (
+ "You are a professional summarizer at 2077 Research. Below is an article on Ethereum technical aspects. "
+ "Your goal is to produce a summary that is shorter than the original content, yet detailed enough for readers "
+ "to fully understand the piece without needing to read the original. Your summary should:\n"
+ "- Provide enough depth and detail so the user gets a complete understanding of the core ideas.\n"
+ "- Be in HTML format, use
tags for headings if needed. Avoid other heading levels.\n"
+ "- Minimize the use of bullet points. If you need to list items, you can, but prefer concise paragraph formatting.\n\n"
+ )
+ return await self.gpt_service.prompt(system_prompt, content)
+
+ def generate_summary_view(self, request):
+ if request.method == 'POST':
+ content = request.POST.get('content')
+ try:
+ gpt_summary = asyncio.run(self._generate_summary(content))
+ return JsonResponse({'summary': gpt_summary})
+ except Exception as e:
+ import logging
+ logging.error("An error occurred while generating the summary", exc_info=True)
+ return JsonResponse({'error': 'An internal error has occurred!'}, status=500)
+ return JsonResponse({'error': 'Invalid request method'}, status=400)
+
def current_slug_history(self, obj):
return current_slug_history(obj)
current_slug_history.short_description = 'Slug Change History'
@@ -36,7 +92,7 @@ def current_slug_history(self, obj):
('Article Details', {
'fields': [
'title', 'slug', 'authors', 'acknowledgement', 'categories',
- 'thumb', 'content', 'summary', 'status', 'scheduled_publish_time'
+ 'thumb', 'content', 'summary', 'gpt_summary', 'status', 'scheduled_publish_time'
]
}),
('Related Content', {
@@ -60,6 +116,12 @@ def current_slug_history(self, obj):
readonly_fields = ('views', 'current_slug_history',)
list_editable = ('status',)
+ class Media:
+ css = {
+ 'all': ('css/article_admin.css',)
+ }
+ js = ('js/article_admin.js',)
+
def display_authors(self, obj):
"""Return a comma-separated list of authors for the article."""
return ", ".join(author.user.username for author in obj.authors.all())
diff --git a/server/apps/research/migrations/0017_article_gpt_summary_alter_article_summary.py b/server/apps/research/migrations/0017_article_gpt_summary_alter_article_summary.py
new file mode 100644
index 0000000..939a188
--- /dev/null
+++ b/server/apps/research/migrations/0017_article_gpt_summary_alter_article_summary.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.8 on 2024-12-10 18:18
+
+import tinymce.models
+from django.db import migrations
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('research', '0016_article_related_articles'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='article',
+ name='gpt_summary',
+ field=tinymce.models.HTMLField(blank=True, null=True),
+ ),
+ ]
diff --git a/server/apps/research/models/article.py b/server/apps/research/models/article.py
index 4b607e6..6b1c3b3 100644
--- a/server/apps/research/models/article.py
+++ b/server/apps/research/models/article.py
@@ -28,6 +28,7 @@ class Article(BaseModel):
title = models.TextField()
content = HTMLField(blank=True, null=True)
summary = models.TextField(blank=True)
+ gpt_summary = HTMLField(blank=True, null=True)
acknowledgement = HTMLField(blank=True, null=True)
authors = models.ManyToManyField(Author, blank=True, related_name='articles')
slug = models.SlugField(max_length=255, blank=True, db_index=True)
diff --git a/server/apps/research/serializers/article_serializer.py b/server/apps/research/serializers/article_serializer.py
index 0832cfc..f824563 100644
--- a/server/apps/research/serializers/article_serializer.py
+++ b/server/apps/research/serializers/article_serializer.py
@@ -55,6 +55,7 @@ class Meta:
"created_at",
"updated_at",
"table_of_contents",
+ "gpt_summary",
]
@@ -90,6 +91,7 @@ class Meta:
"min_read",
"status",
"views",
+ "gpt_summary",
"created_at",
"updated_at",
"scheduled_publish_time",
@@ -123,6 +125,7 @@ class Meta:
"thumb",
"content",
"summary",
+ "gpt_summary",
"acknowledgement",
"status",
"authors",
@@ -185,4 +188,4 @@ def update(self, instance: Article, validated_data: dict) -> Article:
return instance
except Exception as e:
- raise serializers.ValidationError(f"Error updating article: {str(e)}")
+ raise serializers.ValidationError(f"Error updating article: {str(e)}")
\ No newline at end of file
diff --git a/server/apps/research/services/__init__.py b/server/apps/research/services/__init__.py
new file mode 100644
index 0000000..5aaea07
--- /dev/null
+++ b/server/apps/research/services/__init__.py
@@ -0,0 +1,4 @@
+"""
+Services package for the research app.
+This package contains service classes that handle business logic and external API interactions.
+"""
\ No newline at end of file
diff --git a/server/apps/research/services/gpt_service.py b/server/apps/research/services/gpt_service.py
new file mode 100644
index 0000000..4ea1010
--- /dev/null
+++ b/server/apps/research/services/gpt_service.py
@@ -0,0 +1,42 @@
+from django.conf import settings
+from openai import AsyncOpenAI
+
+class GPTService:
+ """Service for handling OpenAI GPT API interactions."""
+
+ def __init__(self):
+ self.client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
+ self.model = "gpt-3.5-turbo"
+ self.max_tokens = 500
+
+ async def prompt(self, system: str, user: str) -> str:
+ """
+ Send a prompt to GPT and get the response.
+
+ Args:
+ system (str): The system message that sets the behavior of the assistant
+ user (str): The user's input/question
+
+ Returns:
+ str: The generated response from GPT
+
+ Raises:
+ Exception: If there's an error in the API call or if the API key is not set
+ """
+ if not settings.OPENAI_API_KEY:
+ raise Exception("OpenAI API key is not configured")
+
+ try:
+ completion = await self.client.chat.completions.create(
+ model=self.model,
+ messages=[
+ {"role": "system", "content": system},
+ {"role": "user", "content": user}
+ ],
+ max_tokens=self.max_tokens
+ )
+ # Access the response content directly from the completion object
+ return completion.choices[0].message.content
+ except Exception as e:
+ print(e)
+ raise Exception(f"Error calling OpenAI API: {str(e)}")
\ No newline at end of file
diff --git a/server/core/config/base.py b/server/core/config/base.py
index 1d6d46e..d043e3b 100644
--- a/server/core/config/base.py
+++ b/server/core/config/base.py
@@ -223,4 +223,7 @@
SILENCED_SYSTEM_CHECKS = ["security.W019"]
# Tinymce API Config
-TINYMCE_API_KEY = config('TINYMCE_API_KEY')
\ No newline at end of file
+TINYMCE_API_KEY = config('TINYMCE_API_KEY')
+
+# OpenAI API Config
+OPENAI_API_KEY = config('OPENAI_API_KEY', default=None)
\ No newline at end of file
diff --git a/server/requirements.txt b/server/requirements.txt
index a9ee425..3b32972 100644
--- a/server/requirements.txt
+++ b/server/requirements.txt
@@ -1,4 +1,5 @@
amqp==5.2.0
+annotated-types==0.7.0
anyio==4.4.0
asgiref==3.8.1
attrs==24.1.0
@@ -15,6 +16,7 @@ click-plugins==1.1.1
click-repl==0.3.0
cron-descriptor==1.4.3
cryptography==43.0.0
+distro==1.9.0
Django==5.0.8
django-admin-interface==0.28.8
django-celery-beat==2.6.0
@@ -33,12 +35,16 @@ httpcore==1.0.5
httpx==0.27.0
idna==3.7
inflection==0.5.1
+jiter==0.8.2
jsonschema==4.23.0
jsonschema-specifications==2023.12.1
kombu==5.4.0
+openai==1.57.4
pillow==10.4.0
prompt_toolkit==3.0.47
pycparser==2.22
+pydantic==2.10.3
+pydantic_core==2.27.1
pyOpenSSL==24.2.1
python-crontab==3.2.0
python-dateutil==2.9.0.post0
@@ -55,6 +61,8 @@ sniffio==1.3.1
soupsieve==2.6
sqlparse==0.5.1
text-unidecode==1.3
+tqdm==4.67.1
+typing_extensions==4.12.2
tzdata==2024.1
uritemplate==4.1.1
urllib3==2.2.2
diff --git a/server/static/css/article_admin.css b/server/static/css/article_admin.css
new file mode 100644
index 0000000..fef5266
--- /dev/null
+++ b/server/static/css/article_admin.css
@@ -0,0 +1,28 @@
+.generate-summary-btn {
+ background-color: #0C4B33;
+ color: white;
+ padding: 10px 15px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: bold;
+ margin-left: 10px;
+ align-self: baseline;
+}
+
+.generate-summary-btn:disabled {
+ background-color: #cccccc;
+ cursor: not-allowed;
+}
+
+.summary-status {
+ margin-left: 10px;
+ font-style: italic;
+ color: #666;
+}
+
+.generate-summary-container-btn {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
\ No newline at end of file
diff --git a/server/static/js/article_admin.js b/server/static/js/article_admin.js
new file mode 100644
index 0000000..4e28844
--- /dev/null
+++ b/server/static/js/article_admin.js
@@ -0,0 +1,101 @@
+document.addEventListener('DOMContentLoaded', function() {
+ // Wait for TinyMCE to initialize
+ if (typeof tinymce !== 'undefined') {
+ const gptSummaryContainer = document.getElementById('gpt_summary_richtext_field');
+ if (gptSummaryContainer) {
+ const buttonContainer = document.createElement('div');
+ buttonContainer.id = 'generate-summary-container-btn';
+
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.id = 'generate-summary-btn';
+ button.className = 'generate-summary-btn';
+ button.textContent = 'Generate Summary';
+
+ const statusSpan = document.createElement('p');
+ statusSpan.className = 'summary-status';
+ statusSpan.id = 'summary-status';
+
+ buttonContainer.appendChild(button);
+ buttonContainer.appendChild(statusSpan);
+
+ gptSummaryContainer.parentNode.insertBefore(buttonContainer, gptSummaryContainer.nextSibling);
+ button.parentNode.insertBefore(statusSpan, button.nextSibling);
+
+ button.addEventListener('click', function() {
+ const contentEditor = tinymce.get('content_richtext_field');
+ const gptSummaryContainer = document.getElementById('gpt_summary_richtext_field');
+ statusSpan.textContent = ' Generating summary...';
+
+ if (contentEditor && gptSummaryContainer) {
+ const content = contentEditor.getContent();
+ if (!content.trim()) {
+ alert('Please enter some content before generating a summary.');
+ return;
+ }
+
+ // Call the GPT API
+ generateSummary(content)
+ .then(() => {
+ statusSpan.textContent = ' Summary generated successfully!';
+ })
+ .catch(error => {
+ statusSpan.textContent = ' Error generating summary. Please try again.';
+ })
+ .finally(() => {
+ // Re-enable editors and button
+ contentEditor.setMode('design');
+ gptSummaryEditor.setMode('design');
+ button.disabled = false;
+ });
+ }
+ });
+ }
+ }
+});
+
+async function generateSummary(content) {
+ try {
+ const response = await fetch('/admin/research/article/generate-summary/', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ 'X-CSRFToken': getCookie('csrftoken')
+ },
+ body: 'content=' + encodeURIComponent(content)
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const data = await response.json();
+
+ // Update the summary field
+ if (typeof tinymce !== 'undefined') {
+ const summaryEditor = tinymce.get('gpt_summary_richtext_field');
+ if (summaryEditor) {
+ summaryEditor.setContent(data.summary);
+ }
+ }
+ } catch (error) {
+ console.error('Error:', error);
+ throw error;
+ }
+}
+
+// Helper function to get CSRF token
+function getCookie(name) {
+ let cookieValue = null;
+ if (document.cookie && document.cookie !== '') {
+ const cookies = document.cookie.split(';');
+ for (let i = 0; i < cookies.length; i++) {
+ const cookie = cookies[i].trim();
+ if (cookie.substring(0, name.length + 1) === (name + '=')) {
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+ break;
+ }
+ }
+ }
+ return cookieValue;
+}
\ No newline at end of file