Skip to content

Commit d73c98c

Browse files
committed
✨️(ai) add deep search
1 parent 03cd72f commit d73c98c

File tree

32 files changed

+4449
-30
lines changed

32 files changed

+4449
-30
lines changed

env.d/development/backend.defaults

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ MDA_API_SECRET=my-shared-secret-mda
7474
SALT_KEY=ThisIsAnExampleSaltForDevPurposeOnly
7575

7676
# AI
77-
AI_BASE_URL=
78-
AI_API_KEY=
79-
AI_MODEL=
77+
AI_BASE_URL=https://albert.api.etalab.gouv.fr/v1
78+
AI_API_KEY=<my-ai-api-key>
79+
AI_MODEL=albert-large
8080

8181
AI_FEATURE_SUMMARY_ENABLED=False

src/backend/core/api/viewsets/thread.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""API ViewSet for Thread model."""
22

3+
import logging
4+
35
from django.conf import settings
46
from django.db.models import Count, Exists, OuterRef, Q
57

@@ -10,14 +12,17 @@
1012
OpenApiResponse,
1113
extend_schema,
1214
)
13-
from rest_framework import mixins, status, viewsets
15+
from rest_framework import mixins, viewsets, status
16+
from rest_framework.response import Response
1417

1518
from core import enums, models
1619
from core.ai.thread_summarizer import summarize_thread
1720
from core.search import search_threads
1821

1922
from .. import permissions, serializers
2023

24+
logger = logging.getLogger(__name__)
25+
2126

2227
class ThreadViewSet(
2328
viewsets.GenericViewSet,
@@ -350,16 +355,125 @@ def stats(self, request):
350355
location=OpenApiParameter.QUERY,
351356
description="Filter threads that are spam (1=true, 0=false).",
352357
),
358+
OpenApiParameter(
359+
name="message_ids",
360+
type=OpenApiTypes.STR,
361+
location=OpenApiParameter.QUERY,
362+
description="Comma-separated list of message IDs to filter threads by specific messages (used by AI search).",
363+
),
353364
],
354365
)
355366
def list(self, request, *args, **kwargs):
356367
"""List threads with optional search functionality."""
357368
search_query = request.query_params.get("search", "").strip()
369+
message_ids = request.query_params.get("message_ids", "").strip()
370+
371+
# Debug: Log what path we're taking
372+
print(f"DEBUG THREAD API START: search_query='{search_query}', message_ids='{message_ids}'")
373+
374+
# Check if we have specific message IDs from deep search (without search query)
375+
if message_ids and not search_query:
376+
print(f"DEBUG THREAD API: Taking message_ids ONLY path (no search query)")
377+
mailbox_id = request.query_params.get("mailbox_id")
378+
379+
# Parse message IDs from comma-separated string
380+
try:
381+
message_id_list = [mid.strip() for mid in message_ids.split(",") if mid.strip()]
382+
print(f"DEBUG: Parsed message_id_list: {message_id_list}")
383+
print(f"DEBUG: Number of message IDs: {len(message_id_list)}")
384+
385+
# Check if this is the special "no results" UUID from contextual search
386+
if len(message_id_list) == 1 and message_id_list[0] == "00000000-0000-0000-0000-000000000000":
387+
print("DEBUG: Detected contextual search empty results UUID - showing no emails as intended")
388+
# Return empty result
389+
page = self.paginate_queryset([])
390+
if page is not None:
391+
serializer = self.get_serializer(page, many=True)
392+
return self.get_paginated_response(serializer.data)
393+
return drf.response.Response([])
394+
395+
# Get threads that contain these messages
396+
threads_with_messages = models.Thread.objects.filter(
397+
messages__id__in=message_id_list
398+
).distinct()
399+
400+
print(f"DEBUG: Found {threads_with_messages.count()} threads with these messages")
401+
402+
# Apply additional filters from query parameters (mailbox, etc.)
403+
queryset = self.get_queryset().filter(id__in=threads_with_messages)
404+
405+
print(f"DEBUG: After applying additional filters: {queryset.count()} threads")
406+
407+
# Use the paginator to create a paginated response
408+
page = self.paginate_queryset(queryset)
409+
if page is not None:
410+
serializer = self.get_serializer(page, many=True)
411+
print(f"DEBUG: Returning paginated response with {len(page)} threads")
412+
return self.get_paginated_response(serializer.data)
413+
414+
serializer = self.get_serializer(queryset, many=True)
415+
print(f"DEBUG: Returning non-paginated response with {queryset.count()} threads")
416+
return drf.response.Response(serializer.data)
417+
418+
except Exception as e:
419+
logger.error(f"Error processing message_ids: {e}")
420+
print(f"DEBUG: Exception in message_ids processing: {e}")
421+
# Fall back to regular search if message_ids processing fails
358422

359423
# If search is provided and OpenSearch is available, use it
360424
if search_query and len(settings.OPENSEARCH_HOSTS[0]) > 0:
361425
# Get the mailbox_id for filtering
362426
mailbox_id = request.query_params.get("mailbox_id")
427+
message_ids = request.query_params.get("message_ids", "").strip()
428+
429+
# Debug: Print all received parameters
430+
print(f"DEBUG THREAD API: search_query='{search_query}', message_ids='{message_ids}'")
431+
print(f"DEBUG THREAD API: All query params: {dict(request.query_params)}")
432+
print(f"DEBUG THREAD API: Taking search+message_ids path")
433+
434+
# Debug: Print the received message_ids parameter
435+
if message_ids:
436+
print(f"DEBUG: Received message_ids parameter: '{message_ids}'")
437+
438+
# Check if we have specific message IDs from deep search (RAG)
439+
if message_ids:
440+
# Parse message IDs from comma-separated string
441+
try:
442+
message_id_list = [mid.strip() for mid in message_ids.split(",") if mid.strip()]
443+
print(f"DEBUG: Parsed message_id_list: {message_id_list}")
444+
print(f"DEBUG: Number of message IDs: {len(message_id_list)}")
445+
446+
# Check if this is the special "no results" UUID from contextual search
447+
if len(message_id_list) == 1 and message_id_list[0] == "00000000-0000-0000-0000-000000000000":
448+
print("DEBUG: Detected contextual search empty results UUID - showing no emails as intended")
449+
450+
# Get threads that contain these messages
451+
threads_with_messages = models.Thread.objects.filter(
452+
messages__id__in=message_id_list
453+
).distinct()
454+
455+
print(f"DEBUG: Found {threads_with_messages.count()} threads with these messages")
456+
457+
# Apply additional filters from query parameters (mailbox, etc.)
458+
queryset = self.get_queryset().filter(id__in=threads_with_messages)
459+
460+
print(f"DEBUG: After applying additional filters: {queryset.count()} threads")
461+
462+
# Use the paginator to create a paginated response
463+
page = self.paginate_queryset(queryset)
464+
if page is not None:
465+
serializer = self.get_serializer(page, many=True)
466+
print(f"DEBUG: Returning paginated response with {len(page)} threads")
467+
return self.get_paginated_response(serializer.data)
468+
469+
serializer = self.get_serializer(queryset, many=True)
470+
print(f"DEBUG: Returning non-paginated response with {queryset.count()} threads")
471+
return drf.response.Response(serializer.data)
472+
473+
except Exception as e:
474+
logger.error(f"Error processing message_ids: {e}")
475+
print(f"DEBUG: Exception in message_ids processing: {e}")
476+
# Fall back to regular search if message_ids processing fails
363477

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

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

415530
@extend_schema(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Django management commands package
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Django management commands

0 commit comments

Comments
 (0)