Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ DJANGO_SUPERUSER_PASSWORD=adminpass
NEWSLETTER_API_BASE_URL=http://127.0.0.1:8080
NEWSLETTER_API_USERNAME=admin
NEWSLETTER_API_PASSWORD=adminpass

DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1,newslettermaker.tech
FRONTEND_URL=http://localhost:3000
4 changes: 4 additions & 0 deletions .gitguardian.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
version: 2
secret:
ignored_paths:
- "frontend/src/lib/__tests__/api.test.ts"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ __pycache__/
.ruff_cache/
.venv/
venv/
.coverage
.env
frontend/.env.local
celerybeat-schedule*
db.sqlite3
htmlcov/
staticfiles/
frontend/.next/
frontend/coverage/
frontend/node_modules/

docs/
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
"cSpell.words": [
"ASGI",
"buildx",
"cbranch",
"cfgv",
"cstat",
"djlint",
"FAVICONS",
"Feedly",
"Fraunces",
"gunicorn",
"healthz",
"HNSW",
Expand All @@ -28,6 +32,9 @@
"QDRANT",
"readyz",
"Referer",
"simplejwt",
"Unparseable",
"unstub",
"upserted",
"upvote",
"uritemplate",
Expand Down
7 changes: 0 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,6 @@ just changepassword your-username

For the default local bootstrap, `.env` also seeds an `admin` superuser in the container database using `DJANGO_SUPERUSER_USERNAME`, `DJANGO_SUPERUSER_EMAIL`, and `DJANGO_SUPERUSER_PASSWORD`.

## Documentation

- [PLANNING.md](docs/PLANNING.md) - Full architecture decisions, data model, and feedback loop design
- [VENDOR.md](docs/VENDOR.md) - Per-skill model selection, rationale, and API pricing
- [GENRES.md](docs/GENRES.md) - Newsletter format types and layout templates
- [IMPLEMENTATION.md](docs/IMPLEMENTATION.md) - Additional implementation notes

## License

This repository is licensed under the GNU Affero General Public License v3.0 or later. See [LICENSE](LICENSE).
Expand Down
27 changes: 15 additions & 12 deletions core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
UserFeedback,
)
from core.plugins import get_plugin_for_source_config, validate_plugin_config
from core.tasks import process_content


@admin.register(Project)
Expand Down Expand Up @@ -211,6 +210,8 @@ def changelist_view(self, request, extra_context=None):

@admin.action(description="Generate Ideas for Newsletter")
def generate_newsletter_ideas(self, request, queryset):
from core.tasks import process_content

content_ids = list(queryset.values_list("id", flat=True))
for content_id in content_ids:
process_content.delay(content_id)
Expand Down Expand Up @@ -252,7 +253,7 @@ class SkillResultAdmin(ModelAdmin):
@admin.action(description="Retry Selected Skills")
def retry_selected_skills(self, request, queryset):
"""Resets status to PENDING and clears errors for retry by the worker."""
updated = queryset.update(status="PENDING", error_message="")
updated = queryset.update(status="pending", error_message="")
self.message_user(
request,
f"Successfully reset {updated} skills to PENDING for retry.",
Expand All @@ -275,11 +276,12 @@ def get_content_link(self, obj):

@admin.display(description="Status")
def display_status(self, obj):
colors = {"COMPLETED": "green", "FAILED": "red", "PENDING": "orange"}
color = colors.get(obj.status, "gray")
status_value = str(obj.status).lower()
colors = {"completed": "green", "failed": "red", "pending": "orange"}
color = colors.get(status_value, "gray")
return format_html(
'<span style="color: {}; font-weight: bold;">● {}</span>',
color, obj.status
color, status_value.upper()
)

@admin.display(description="Perf / Conf")
Expand Down Expand Up @@ -308,7 +310,7 @@ def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
metrics = qs.aggregate(avg_lat=Avg('latency_ms'))
avg_latency = metrics['avg_lat'] or 0
failure_count = qs.filter(status='FAILED').count()
failure_count = qs.filter(status='failed').count()
total_count = qs.count() or 1

extra_context["dashboard_stats"] = [
Expand Down Expand Up @@ -343,7 +345,7 @@ class UserFeedbackAdmin(ModelAdmin):

@admin.display(description="Type")
def display_feedback(self, obj):
if obj.feedback_type == "UPVOTE":
if str(obj.feedback_type).lower() == "upvote":
return format_html('<span style="font-size: {}">{}</span>', "1.2rem", "👍")
return format_html('<span style="font-size: {}">{}</span>', "1.2rem", "👎")

Expand All @@ -363,7 +365,7 @@ def get_ai_score(self, obj):
def changelist_view(self, request, extra_context=None):
qs = self.get_queryset(request)
extra_context = extra_context or {}
upvotes = qs.filter(feedback_type="UPVOTE").count()
upvotes = qs.filter(feedback_type="upvote").count()
total = qs.count() or 1
approval_rate = (upvotes / total) * 100

Expand Down Expand Up @@ -414,11 +416,12 @@ class IngestionRunAdmin(ModelAdmin):

@admin.display(description="Status")
def display_status(self, obj):
colors = {"COMPLETED": "success", "FAILED": "danger", "RUNNING": "info"}
status_value = str(obj.status).lower()
colors = {"success": "success", "failed": "danger", "running": "info"}
return format_html(
'<span class="unfold-badge {}">{}</span>',
colors.get(obj.status, "warning"),
obj.status
colors.get(status_value, "warning"),
status_value.upper()
)

@admin.display(description="Efficiency (Ingested/Fetched)")
Expand All @@ -445,7 +448,7 @@ def changelist_view(self, request, extra_context=None):
qs = self.get_queryset(request)
extra_context = extra_context or {}
total_runs = qs.count()
failed_runs = qs.filter(status="FAILED").count()
failed_runs = qs.filter(status="failed").count()
total_ingested = sum(qs.values_list('items_ingested', flat=True))

extra_context["dashboard_stats"] = [
Expand Down
16 changes: 8 additions & 8 deletions core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,6 @@
SourceConfig,
UserFeedback,
)
from core.pipeline import (
CLASSIFICATION_SKILL_NAME,
RELATED_CONTENT_SKILL_NAME,
RELEVANCE_SKILL_NAME,
SUMMARIZATION_SKILL_NAME,
execute_ad_hoc_skill,
)
from core.serializers import (
ContentSerializer,
EntitySerializer,
Expand All @@ -42,7 +35,11 @@
SourceConfigSerializer,
UserFeedbackSerializer,
)
from core.tasks import queue_content_skill

CLASSIFICATION_SKILL_NAME = "content_classification"
RELEVANCE_SKILL_NAME = "relevance_scoring"
SUMMARIZATION_SKILL_NAME = "summarization"
RELATED_CONTENT_SKILL_NAME = "find_related"

PROJECT_ID_PARAMETER = OpenApiParameter(
name="project_id",
Expand Down Expand Up @@ -532,6 +529,9 @@ class ContentViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet):
)
@action(detail=True, methods=["post"], url_path=r"skills/(?P<skill_name>[^/.]+)")
def run_skill(self, request, *args, **kwargs):
from core.pipeline import execute_ad_hoc_skill
from core.tasks import queue_content_skill

skill_name = str(kwargs["skill_name"])
if skill_name not in {
CLASSIFICATION_SKILL_NAME,
Expand Down
16 changes: 16 additions & 0 deletions core/auth_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from dj_rest_auth.registration.views import SocialLoginView
from rest_framework.permissions import AllowAny


class BaseSocialLoginView(SocialLoginView):
permission_classes = [AllowAny]


class GitHubLoginView(BaseSocialLoginView):
adapter_class = GitHubOAuth2Adapter


class GoogleLoginView(BaseSocialLoginView):
adapter_class = GoogleOAuth2Adapter
23 changes: 19 additions & 4 deletions core/embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from uuid import uuid4

import httpx
from django.conf import settings
from django.conf import settings as django_settings
from django.utils.dateparse import parse_datetime
from qdrant_client import QdrantClient
from qdrant_client.models import (
Expand All @@ -17,9 +17,23 @@
PointStruct,
VectorParams,
)
from sentence_transformers import SentenceTransformer

from core.models import Content
from core.settings_types import CoreSettings

SentenceTransformer = None
settings = cast(CoreSettings, django_settings)


def get_sentence_transformer_class():
global SentenceTransformer

if SentenceTransformer is None:
from sentence_transformers import SentenceTransformer as sentence_transformer_class

SentenceTransformer = sentence_transformer_class

return SentenceTransformer


class EmbeddingProvider(ABC):
Expand All @@ -33,7 +47,8 @@ def get_embedding_dimension(self) -> int:

class SentenceTransformerEmbeddingProvider(EmbeddingProvider):
def __init__(self):
self.model = SentenceTransformer(
sentence_transformer_class = get_sentence_transformer_class()
self.model = sentence_transformer_class(
settings.EMBEDDING_MODEL,
trust_remote_code=settings.EMBEDDING_TRUST_REMOTE_CODE,
)
Expand Down Expand Up @@ -97,7 +112,7 @@ def collection_name_for_project(project_id: int) -> str:

@lru_cache(maxsize=1)
def get_qdrant_client() -> QdrantClient:
return QdrantClient(url=settings.QDRANT_URL, timeout=10)
return QdrantClient(url=settings.QDRANT_URL, timeout=10, check_compatibility=False)


@lru_cache(maxsize=1)
Expand Down
13 changes: 13 additions & 0 deletions core/settings_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Protocol


class CoreSettings(Protocol):
QDRANT_URL: str
EMBEDDING_MODEL: str
EMBEDDING_PROVIDER: str
EMBEDDING_TRUST_REMOTE_CODE: bool
OLLAMA_URL: str
OPENROUTER_API_KEY: str
OPENROUTER_API_BASE: str
OPENROUTER_APP_URL: str
OPENROUTER_APP_NAME: str
Loading
Loading