Skip to content
Draft
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
6 changes: 3 additions & 3 deletions env.d/development/backend.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ MDA_API_SECRET=my-shared-secret-mda
SALT_KEY=ThisIsAnExampleSaltForDevPurposeOnly

# AI
AI_BASE_URL=
AI_API_KEY=
AI_MODEL=
AI_BASE_URL=https://albert.api.etalab.gouv.fr/v1
AI_API_KEY=<my-ai-api-key>
AI_MODEL=albert-large

AI_FEATURE_SUMMARY_ENABLED=False
117 changes: 116 additions & 1 deletion src/backend/core/api/viewsets/thread.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""API ViewSet for Thread model."""

import logging

from django.conf import settings
from django.db.models import Count, Exists, OuterRef, Q

Expand All @@ -10,14 +12,17 @@
OpenApiResponse,
extend_schema,
)
from rest_framework import mixins, status, viewsets
from rest_framework import mixins, viewsets, status
from rest_framework.response import Response

from core import enums, models
from core.ai.thread_summarizer import summarize_thread
from core.search import search_threads

from .. import permissions, serializers

logger = logging.getLogger(__name__)


class ThreadViewSet(
viewsets.GenericViewSet,
Expand Down Expand Up @@ -350,16 +355,125 @@ def stats(self, request):
location=OpenApiParameter.QUERY,
description="Filter threads that are spam (1=true, 0=false).",
),
OpenApiParameter(
name="message_ids",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
description="Comma-separated list of message IDs to filter threads by specific messages (used by AI search).",
),
],
)
def list(self, request, *args, **kwargs):
"""List threads with optional search functionality."""
search_query = request.query_params.get("search", "").strip()
message_ids = request.query_params.get("message_ids", "").strip()

# Debug: Log what path we're taking
print(f"DEBUG THREAD API START: search_query='{search_query}', message_ids='{message_ids}'")

# Check if we have specific message IDs from deep search (without search query)
if message_ids and not search_query:
print(f"DEBUG THREAD API: Taking message_ids ONLY path (no search query)")
mailbox_id = request.query_params.get("mailbox_id")

# Parse message IDs from comma-separated string
try:
message_id_list = [mid.strip() for mid in message_ids.split(",") if mid.strip()]
print(f"DEBUG: Parsed message_id_list: {message_id_list}")
print(f"DEBUG: Number of message IDs: {len(message_id_list)}")

# Check if this is the special "no results" UUID from contextual search
if len(message_id_list) == 1 and message_id_list[0] == "00000000-0000-0000-0000-000000000000":
print("DEBUG: Detected contextual search empty results UUID - showing no emails as intended")
# Return empty result
page = self.paginate_queryset([])
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
return drf.response.Response([])

# Get threads that contain these messages
threads_with_messages = models.Thread.objects.filter(
messages__id__in=message_id_list
).distinct()

print(f"DEBUG: Found {threads_with_messages.count()} threads with these messages")

# Apply additional filters from query parameters (mailbox, etc.)
queryset = self.get_queryset().filter(id__in=threads_with_messages)

print(f"DEBUG: After applying additional filters: {queryset.count()} threads")

# Use the paginator to create a paginated response
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
print(f"DEBUG: Returning paginated response with {len(page)} threads")
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
print(f"DEBUG: Returning non-paginated response with {queryset.count()} threads")
return drf.response.Response(serializer.data)

except Exception as e:
logger.error(f"Error processing message_ids: {e}")
print(f"DEBUG: Exception in message_ids processing: {e}")
# Fall back to regular search if message_ids processing fails

# If search is provided and OpenSearch is available, use it
if search_query and len(settings.OPENSEARCH_HOSTS[0]) > 0:
# Get the mailbox_id for filtering
mailbox_id = request.query_params.get("mailbox_id")
message_ids = request.query_params.get("message_ids", "").strip()

# Debug: Print all received parameters
print(f"DEBUG THREAD API: search_query='{search_query}', message_ids='{message_ids}'")
print(f"DEBUG THREAD API: All query params: {dict(request.query_params)}")
print(f"DEBUG THREAD API: Taking search+message_ids path")

# Debug: Print the received message_ids parameter
if message_ids:
print(f"DEBUG: Received message_ids parameter: '{message_ids}'")

# Check if we have specific message IDs from deep search (RAG)
if message_ids:
# Parse message IDs from comma-separated string
try:
message_id_list = [mid.strip() for mid in message_ids.split(",") if mid.strip()]
print(f"DEBUG: Parsed message_id_list: {message_id_list}")
print(f"DEBUG: Number of message IDs: {len(message_id_list)}")

# Check if this is the special "no results" UUID from contextual search
if len(message_id_list) == 1 and message_id_list[0] == "00000000-0000-0000-0000-000000000000":
print("DEBUG: Detected contextual search empty results UUID - showing no emails as intended")

# Get threads that contain these messages
threads_with_messages = models.Thread.objects.filter(
messages__id__in=message_id_list
).distinct()

print(f"DEBUG: Found {threads_with_messages.count()} threads with these messages")

# Apply additional filters from query parameters (mailbox, etc.)
queryset = self.get_queryset().filter(id__in=threads_with_messages)

print(f"DEBUG: After applying additional filters: {queryset.count()} threads")

# Use the paginator to create a paginated response
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
print(f"DEBUG: Returning paginated response with {len(page)} threads")
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
print(f"DEBUG: Returning non-paginated response with {queryset.count()} threads")
return drf.response.Response(serializer.data)

except Exception as e:
logger.error(f"Error processing message_ids: {e}")
print(f"DEBUG: Exception in message_ids processing: {e}")
# Fall back to regular search if message_ids processing fails

# Build filters from query parameters
# TODO: refactor as thread filters are not the same as message filters (has_messages, has_active)
Expand Down Expand Up @@ -410,6 +524,7 @@ def list(self, request, *args, **kwargs):
return drf.response.Response(serializer.data)

# Fall back to regular DB query if no search query or OpenSearch not available
print(f"DEBUG THREAD API: Taking regular DB query path (no search or no OpenSearch)")
return super().list(request, *args, **kwargs)

@extend_schema(
Expand Down
1 change: 1 addition & 0 deletions src/backend/core/management/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Django management commands package
1 change: 1 addition & 0 deletions src/backend/core/management/commands/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Django management commands
Loading
Loading