From dad1e79bc6f898fd10818137ee1c3b08cbab75f9 Mon Sep 17 00:00:00 2001 From: "Ian K. Guimaraes" Date: Wed, 11 Dec 2024 17:17:06 -0300 Subject: [PATCH 1/7] feat: generate article summaries with chat gpt --- requirements.txt | 74 +++++++++++++ server/apps/research/admin/article_admin.py | 49 ++++++++- ...ticle_gpt_summary_alter_article_summary.py | 24 +++++ server/apps/research/models/article.py | 3 +- server/apps/research/services/__init__.py | 4 + server/apps/research/services/gpt_service.py | 42 ++++++++ server/core/config/base.py | 5 +- server/static/css/article_admin.css | 25 +++++ server/static/js/article_admin.js | 102 ++++++++++++++++++ 9 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 requirements.txt create mode 100644 server/apps/research/migrations/0017_article_gpt_summary_alter_article_summary.py create mode 100644 server/apps/research/services/__init__.py create mode 100644 server/apps/research/services/gpt_service.py create mode 100644 server/static/css/article_admin.css create mode 100644 server/static/js/article_admin.js 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 352ae59..91b556b 100644 --- a/server/apps/research/admin/article_admin.py +++ b/server/apps/research/admin/article_admin.py @@ -3,9 +3,12 @@ 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: model = Article @@ -15,10 +18,45 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['acknowledgement'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "acknowledgement_richtext_field", 'placeholder': f"Enter Acknowledgement here"}) self.fields['content'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "content_richtext_field", 'placeholder': f"Enter Article Content here"}) + self.fields['summary'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "summary_richtext_field", 'placeholder': f"Article summary will be generated 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" + ) + 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: + # Run the async function in the sync view + summary = asyncio.run(self._generate_summary(content)) + return JsonResponse({'summary': summary}) + except Exception as e: + return JsonResponse({'error': str(e)}, 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' @@ -38,7 +76,12 @@ def current_slug_history(self, obj): list_filter = ('authors', 'status', 'categories', 'created_at', 'is_sponsored') 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.""" 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..af3fb92 --- /dev/null +++ b/server/apps/research/migrations/0017_article_gpt_summary_alter_article_summary.py @@ -0,0 +1,24 @@ +# Generated by Django 5.0.8 on 2024-12-10 18:18 + +import tinymce.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('research', '0016_alter_category_slug'), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='gpt_summary', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='article', + name='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 72cca9d..a4d3c1b 100644 --- a/server/apps/research/models/article.py +++ b/server/apps/research/models/article.py @@ -25,7 +25,8 @@ class Article(BaseModel): title = models.TextField() content = HTMLField(blank=True, null=True) - summary = models.TextField(blank=True) + summary = HTMLField(blank=True, null=True) + gpt_summary = models.TextField(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/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 ae1ed98..a618bf0 100644 --- a/server/core/config/base.py +++ b/server/core/config/base.py @@ -196,4 +196,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/static/css/article_admin.css b/server/static/css/article_admin.css new file mode 100644 index 0000000..c0a67d4 --- /dev/null +++ b/server/static/css/article_admin.css @@ -0,0 +1,25 @@ +.generate-summary-btn { + background-color: #417690; + color: white; + padding: 10px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; + margin-left: 10px; +} + +.generate-summary-btn:hover { + background-color: #2b5172; +} + +.generate-summary-btn:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +.summary-status { + margin-left: 10px; + font-style: italic; + color: #666; +} \ 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..ef9aee9 --- /dev/null +++ b/server/static/js/article_admin.js @@ -0,0 +1,102 @@ +document.addEventListener('DOMContentLoaded', function() { + // Add generate summary button after the content field + const contentField = document.getElementById('content_richtext_field'); + const summaryField = document.getElementById('summary_richtext_field'); + if (contentField) { + const button = document.createElement('button'); + button.type = 'button'; + button.id = 'generate-summary-btn'; + button.className = 'generate-summary-btn'; + button.textContent = 'Generate Summary'; + button.style.height = 'min-content'; + + const statusSpan = document.createElement('span'); + statusSpan.className = 'summary-status'; + statusSpan.id = 'summary-status'; + + summaryField.parentNode.insertBefore(button, summaryField.nextSibling); + button.parentNode.insertBefore(statusSpan, button.nextSibling); + + // Add click event listener to the button + button.addEventListener('click', function() { + const editor = tinymce.get('content_richtext_field'); + const summaryEditor = tinymce.get('summary_richtext_field'); + + if (editor && summaryEditor) { + const content = editor.getContent(); + if (!content.trim()) { + alert('Please enter some content before generating a summary.'); + return; + } + + // Disable both editors and the button + editor.setMode('readonly'); + summaryEditor.setMode('readonly'); + button.disabled = true; + statusSpan.textContent = ' Generating summary...'; + + // Call the GPT API + generateSummary(content) + .then(() => { + statusSpan.textContent = ' Summary generated successfully!'; + }) + .catch(error => { + console.error('Error generating summary:', error); + statusSpan.textContent = ' Error generating summary. Please try again.'; + }) + .finally(() => { + // Re-enable both editors and the button + editor.setMode('design'); + summaryEditor.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('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 From 13bda4033ead4d01c147a979cd2091b60e1e730a Mon Sep 17 00:00:00 2001 From: "Ian K. Guimaraes" Date: Wed, 11 Dec 2024 20:09:22 -0300 Subject: [PATCH 2/7] refactor: button position --- server/apps/research/admin/article_admin.py | 10 +- ...ticle_gpt_summary_alter_article_summary.py | 8 +- server/apps/research/models/article.py | 2 +- server/static/css/article_admin.css | 13 ++- server/static/js/article_admin.js | 95 +++++++++---------- 5 files changed, 62 insertions(+), 66 deletions(-) diff --git a/server/apps/research/admin/article_admin.py b/server/apps/research/admin/article_admin.py index 91b556b..ef090fb 100644 --- a/server/apps/research/admin/article_admin.py +++ b/server/apps/research/admin/article_admin.py @@ -18,7 +18,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['acknowledgement'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "acknowledgement_richtext_field", 'placeholder': f"Enter Acknowledgement here"}) self.fields['content'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "content_richtext_field", 'placeholder': f"Enter Article Content here"}) - self.fields['summary'].widget = TinyMCE(attrs={'cols': 80, 'rows': 30, 'id': "summary_richtext_field", 'placeholder': f"Article summary will be generated here"}) + self.fields['gpt_summary'].widget = TinyMCE(attrs={'cols': 80, 'rows': 15, 'id': "gpt_summary_richtext_field", 'placeholder': f"GPT-generated summary will appear here"}) class ArticleAdmin(admin.ModelAdmin): """Admin interface for the Article model.""" @@ -42,7 +42,7 @@ async def _generate_summary(self, content: str) -> str: "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" + "- 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) @@ -51,8 +51,8 @@ def generate_summary_view(self, request): content = request.POST.get('content') try: # Run the async function in the sync view - summary = asyncio.run(self._generate_summary(content)) - return JsonResponse({'summary': summary}) + gpt_summary = asyncio.run(self._generate_summary(content)) + return JsonResponse({'summary': gpt_summary}) except Exception as e: return JsonResponse({'error': str(e)}, status=500) return JsonResponse({'error': 'Invalid request method'}, status=400) @@ -62,7 +62,7 @@ def current_slug_history(self, obj): current_slug_history.short_description = 'Slug Change History' fieldsets = [ - ('Article Details', {'fields': ['title', 'slug', 'authors', 'acknowledgement', 'categories', 'thumb', 'content', 'summary', 'status', 'scheduled_publish_time']}), + ('Article Details', {'fields': ['title', 'slug', 'authors', 'acknowledgement', 'categories', 'thumb', 'content', 'summary', 'gpt_summary', 'status', 'scheduled_publish_time']}), ('Sponsorship Details', {'fields': ['is_sponsored', 'sponsor_color', 'sponsor_text_color']}), ('URL Management', { 'fields': ('current_slug_history',), 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 index af3fb92..9d2b209 100644 --- 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 @@ -1,8 +1,7 @@ # Generated by Django 5.0.8 on 2024-12-10 18:18 import tinymce.models -from django.db import migrations, models - +from django.db import migrations class Migration(migrations.Migration): @@ -14,11 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='article', name='gpt_summary', - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name='article', - name='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 a4d3c1b..5d8883e 100644 --- a/server/apps/research/models/article.py +++ b/server/apps/research/models/article.py @@ -25,7 +25,7 @@ class Article(BaseModel): title = models.TextField() content = HTMLField(blank=True, null=True) - summary = HTMLField(blank=True, null=True) + summary = models.TextField(blank=True) gpt_summary = models.TextField(blank=True, null=True) acknowledgement = HTMLField(blank=True, null=True) authors = models.ManyToManyField(Author, blank=True, related_name='articles') diff --git a/server/static/css/article_admin.css b/server/static/css/article_admin.css index c0a67d4..fef5266 100644 --- a/server/static/css/article_admin.css +++ b/server/static/css/article_admin.css @@ -1,5 +1,5 @@ .generate-summary-btn { - background-color: #417690; + background-color: #0C4B33; color: white; padding: 10px 15px; border: none; @@ -7,10 +7,7 @@ cursor: pointer; font-weight: bold; margin-left: 10px; -} - -.generate-summary-btn:hover { - background-color: #2b5172; + align-self: baseline; } .generate-summary-btn:disabled { @@ -22,4 +19,10 @@ 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 index ef9aee9..4e28844 100644 --- a/server/static/js/article_admin.js +++ b/server/static/js/article_admin.js @@ -1,57 +1,56 @@ document.addEventListener('DOMContentLoaded', function() { - // Add generate summary button after the content field - const contentField = document.getElementById('content_richtext_field'); - const summaryField = document.getElementById('summary_richtext_field'); - if (contentField) { - const button = document.createElement('button'); - button.type = 'button'; - button.id = 'generate-summary-btn'; - button.className = 'generate-summary-btn'; - button.textContent = 'Generate Summary'; - button.style.height = 'min-content'; + // 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 statusSpan = document.createElement('span'); - statusSpan.className = 'summary-status'; - statusSpan.id = 'summary-status'; + const button = document.createElement('button'); + button.type = 'button'; + button.id = 'generate-summary-btn'; + button.className = 'generate-summary-btn'; + button.textContent = 'Generate Summary'; - summaryField.parentNode.insertBefore(button, summaryField.nextSibling); - button.parentNode.insertBefore(statusSpan, button.nextSibling); + const statusSpan = document.createElement('p'); + statusSpan.className = 'summary-status'; + statusSpan.id = 'summary-status'; - // Add click event listener to the button - button.addEventListener('click', function() { - const editor = tinymce.get('content_richtext_field'); - const summaryEditor = tinymce.get('summary_richtext_field'); - - if (editor && summaryEditor) { - const content = editor.getContent(); - if (!content.trim()) { - alert('Please enter some content before generating a summary.'); - return; - } + buttonContainer.appendChild(button); + buttonContainer.appendChild(statusSpan); + + gptSummaryContainer.parentNode.insertBefore(buttonContainer, gptSummaryContainer.nextSibling); + button.parentNode.insertBefore(statusSpan, button.nextSibling); - // Disable both editors and the button - editor.setMode('readonly'); - summaryEditor.setMode('readonly'); - button.disabled = true; + 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 => { - console.error('Error generating summary:', error); - statusSpan.textContent = ' Error generating summary. Please try again.'; - }) - .finally(() => { - // Re-enable both editors and the button - editor.setMode('design'); - summaryEditor.setMode('design'); - button.disabled = false; - }); - } - }); + // 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; + }); + } + }); + } } }); @@ -74,7 +73,7 @@ async function generateSummary(content) { // Update the summary field if (typeof tinymce !== 'undefined') { - const summaryEditor = tinymce.get('summary_richtext_field'); + const summaryEditor = tinymce.get('gpt_summary_richtext_field'); if (summaryEditor) { summaryEditor.setContent(data.summary); } From 90e6547e1c3894c21745f11b3a122b79b55e773e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ian=20K=2E=20Guimar=C3=A3es?= Date: Wed, 11 Dec 2024 20:12:08 -0300 Subject: [PATCH 3/7] Fix code scanning alert no. 9: Information exposure through an exception Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- server/apps/research/admin/article_admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/apps/research/admin/article_admin.py b/server/apps/research/admin/article_admin.py index ef090fb..1a8fa17 100644 --- a/server/apps/research/admin/article_admin.py +++ b/server/apps/research/admin/article_admin.py @@ -54,7 +54,9 @@ def generate_summary_view(self, request): gpt_summary = asyncio.run(self._generate_summary(content)) return JsonResponse({'summary': gpt_summary}) except Exception as e: - return JsonResponse({'error': str(e)}, status=500) + 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): From b015690af4cf99b84608a97e2de293591e9b3fad Mon Sep 17 00:00:00 2001 From: "Ian K. Guimaraes" Date: Wed, 11 Dec 2024 21:10:15 -0300 Subject: [PATCH 4/7] chore: add gpt_summary to article serializer list --- server/apps/research/serializers/article_serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/apps/research/serializers/article_serializer.py b/server/apps/research/serializers/article_serializer.py index 2bf8989..16dc7a1 100644 --- a/server/apps/research/serializers/article_serializer.py +++ b/server/apps/research/serializers/article_serializer.py @@ -26,7 +26,7 @@ class Meta: model = Article fields = [ 'id', 'slug', 'title', 'authors', 'thumb', 'categories', 'summary', - 'acknowledgement', 'content', 'min_read', 'status', 'views', + 'acknowledgement', 'content', 'min_read', 'status', 'views', 'gpt_summary', 'created_at', 'updated_at', 'scheduled_publish_time', 'table_of_contents', 'is_sponsored', 'sponsor_color', 'sponsor_text_color' ] @@ -38,7 +38,7 @@ class ArticleCreateUpdateSerializer(serializers.ModelSerializer): class Meta: model = Article - fields = ['title', 'slug', 'categories', 'thumb', 'content', 'summary', 'acknowledgement', 'status', 'authors', 'scheduled_publish_time', 'is_sponsored', 'sponsor_color', 'sponsor_text_color'] + fields = ['title', 'slug', 'categories', 'thumb', 'content', 'summary', 'gpt_summary', 'acknowledgement', 'status', 'authors', 'scheduled_publish_time', 'is_sponsored', 'sponsor_color', 'sponsor_text_color'] def create(self, validated_data: dict) -> Article: """Create a new article instance.""" From 4b789e890ddfc06ae29faeea153e46736bc87051 Mon Sep 17 00:00:00 2001 From: iankressin Date: Fri, 13 Dec 2024 16:41:50 -0300 Subject: [PATCH 5/7] chore: update requirements --- server/requirements.txt | 8 ++++++++ 1 file changed, 8 insertions(+) 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 From 3c73e8d8eaca242f171697833ebf5976c96230de Mon Sep 17 00:00:00 2001 From: iankressin Date: Mon, 16 Dec 2024 17:19:44 -0300 Subject: [PATCH 6/7] chore: return updated_at on /articles and /articles/:id --- server/apps/research/serializers/article_serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/apps/research/serializers/article_serializer.py b/server/apps/research/serializers/article_serializer.py index f8b02b2..71ad437 100644 --- a/server/apps/research/serializers/article_serializer.py +++ b/server/apps/research/serializers/article_serializer.py @@ -28,7 +28,7 @@ class Meta: model = Article exclude = [ 'content', 'scheduled_publish_time', 'acknowledgement', - 'status', 'views', 'created_at', 'updated_at', 'table_of_contents' + 'status', 'views', 'created_at', 'table_of_contents' ] class ArticleSerializer(serializers.ModelSerializer): @@ -49,7 +49,7 @@ class Meta: 'id', 'slug', 'title', 'authors', 'thumb', 'categories', 'summary', 'acknowledgement', 'content', 'min_read', 'status', 'views', 'created_at', 'updated_at', 'scheduled_publish_time', 'table_of_contents', - 'is_sponsored', 'sponsor_color', 'sponsor_text_color', 'related_articles' + 'is_sponsored', 'sponsor_color', 'sponsor_text_color', 'related_articles', 'updated_at' ] class ArticleCreateUpdateSerializer(serializers.ModelSerializer): From e39397229a7dd675730a359717d7ed210284c7f3 Mon Sep 17 00:00:00 2001 From: iankressin Date: Tue, 17 Dec 2024 16:29:12 -0300 Subject: [PATCH 7/7] fix: correct type of gpt_summary in article model --- server/apps/research/models/article.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/apps/research/models/article.py b/server/apps/research/models/article.py index f490205..d46e504 100644 --- a/server/apps/research/models/article.py +++ b/server/apps/research/models/article.py @@ -27,7 +27,7 @@ class Article(BaseModel): title = models.TextField() content = HTMLField(blank=True, null=True) summary = models.TextField(blank=True) - gpt_summary = models.TextField(blank=True, null=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)