Skip to content

Commit 089c9d1

Browse files
committed
use new indexer files structureclear
1 parent 869f848 commit 089c9d1

3 files changed

Lines changed: 303 additions & 3664 deletions

File tree

surfsense_backend/app/tasks/connector_indexers/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
- Confluence: Index pages from Confluence spaces
1515
- Discord: Index messages from Discord servers
1616
- ClickUp: Index tasks from ClickUp workspaces
17+
- Google Gmail: Index messages from Google Gmail
1718
- Google Calendar: Index events from Google Calendar
1819
"""
1920

@@ -27,6 +28,7 @@
2728

2829
# Calendar and scheduling
2930
from .google_calendar_indexer import index_google_calendar_events
31+
from .google_gmail_indexer import index_google_gmail_messages
3032
from .jira_indexer import index_jira_issues
3133

3234
# Issue tracking and project management
@@ -36,7 +38,7 @@
3638
from .notion_indexer import index_notion_pages
3739
from .slack_indexer import index_slack_messages
3840

39-
__all__ = [
41+
__all__ = [ # noqa: RUF022
4042
"index_clickup_tasks",
4143
"index_confluence_pages",
4244
"index_discord_messages",
@@ -51,4 +53,5 @@
5153
"index_notion_pages",
5254
# Communication platforms
5355
"index_slack_messages",
56+
"index_google_gmail_messages",
5457
]
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
"""
2+
Google Gmail connector indexer.
3+
"""
4+
5+
from datetime import datetime
6+
7+
from google.oauth2.credentials import Credentials
8+
from sqlalchemy.exc import SQLAlchemyError
9+
from sqlalchemy.ext.asyncio import AsyncSession
10+
11+
from app.config import config
12+
from app.connectors.google_gmail_connector import GoogleGmailConnector
13+
from app.db import (
14+
Document,
15+
DocumentType,
16+
SearchSourceConnectorType,
17+
)
18+
from app.services.task_logging_service import TaskLoggingService
19+
from app.utils.document_converters import generate_content_hash
20+
21+
from .base import (
22+
check_duplicate_document_by_hash,
23+
create_document_chunks,
24+
get_connector_by_id,
25+
logger,
26+
update_connector_last_indexed,
27+
)
28+
29+
30+
async def index_google_gmail_messages(
31+
session: AsyncSession,
32+
connector_id: int,
33+
search_space_id: int,
34+
user_id: str,
35+
start_date: str | None = None,
36+
end_date: str | None = None,
37+
update_last_indexed: bool = True,
38+
max_messages: int = 100,
39+
) -> tuple[int, str]:
40+
"""
41+
Index Gmail messages for a specific connector.
42+
43+
Args:
44+
session: Database session
45+
connector_id: ID of the Gmail connector
46+
search_space_id: ID of the search space
47+
user_id: ID of the user
48+
start_date: Start date for filtering messages (YYYY-MM-DD format)
49+
end_date: End date for filtering messages (YYYY-MM-DD format)
50+
update_last_indexed: Whether to update the last_indexed_at timestamp (default: True)
51+
max_messages: Maximum number of messages to fetch (default: 100)
52+
53+
Returns:
54+
Tuple of (number_of_indexed_messages, status_message)
55+
"""
56+
task_logger = TaskLoggingService(session, search_space_id)
57+
58+
# Calculate days back based on start_date
59+
if start_date:
60+
try:
61+
start_date_obj = datetime.strptime(start_date, "%Y-%m-%d")
62+
days_back = (datetime.now() - start_date_obj).days
63+
except ValueError:
64+
days_back = 30 # Default to 30 days if start_date is invalid
65+
66+
# Log task start
67+
log_entry = await task_logger.log_task_start(
68+
task_name="google_gmail_messages_indexing",
69+
source="connector_indexing_task",
70+
message=f"Starting Gmail messages indexing for connector {connector_id}",
71+
metadata={
72+
"connector_id": connector_id,
73+
"user_id": str(user_id),
74+
"max_messages": max_messages,
75+
"days_back": days_back,
76+
},
77+
)
78+
79+
try:
80+
# Get connector by id
81+
connector = await get_connector_by_id(
82+
session, connector_id, SearchSourceConnectorType.GOOGLE_GMAIL_CONNECTOR
83+
)
84+
85+
if not connector:
86+
error_msg = f"Gmail connector with ID {connector_id} not found"
87+
await task_logger.log_task_failure(
88+
log_entry, error_msg, {"error_type": "ConnectorNotFound"}
89+
)
90+
return 0, error_msg
91+
92+
# Create credentials from connector config
93+
config_data = connector.config
94+
credentials = Credentials(
95+
token=config_data.get("token"),
96+
refresh_token=config_data.get("refresh_token"),
97+
token_uri=config_data.get("token_uri"),
98+
client_id=config_data.get("client_id"),
99+
client_secret=config_data.get("client_secret"),
100+
scopes=config_data.get("scopes", []),
101+
)
102+
103+
if (
104+
not credentials.client_id
105+
or not credentials.client_secret
106+
or not credentials.refresh_token
107+
):
108+
await task_logger.log_task_failure(
109+
log_entry,
110+
f"Google gmail credentials not found in connector config for connector {connector_id}",
111+
"Missing Google gmail credentials",
112+
{"error_type": "MissingCredentials"},
113+
)
114+
return 0, "Google gmail credentials not found in connector config"
115+
116+
# Initialize Google gmail client
117+
await task_logger.log_task_progress(
118+
log_entry,
119+
f"Initializing Google gmail client for connector {connector_id}",
120+
{"stage": "client_initialization"},
121+
)
122+
123+
# Initialize Google gmail connector
124+
gmail_connector = GoogleGmailConnector(credentials)
125+
126+
# Fetch recent Google gmail messages
127+
logger.info(f"Fetching recent emails for connector {connector_id}")
128+
messages, error = gmail_connector.get_recent_messages(
129+
max_results=max_messages, days_back=days_back
130+
)
131+
132+
if error:
133+
await task_logger.log_task_failure(
134+
log_entry, f"Failed to fetch messages: {error}", {}
135+
)
136+
return 0, f"Failed to fetch Gmail messages: {error}"
137+
138+
if not messages:
139+
success_msg = "No Google gmail messages found in the specified date range"
140+
await task_logger.log_task_success(
141+
log_entry, success_msg, {"messages_count": 0}
142+
)
143+
return 0, success_msg
144+
145+
logger.info(f"Found {len(messages)} Google gmail messages to index")
146+
147+
documents_indexed = 0
148+
skipped_messages = []
149+
documents_skipped = 0
150+
for message in messages:
151+
try:
152+
# Extract message information
153+
message_id = message.get("id", "")
154+
thread_id = message.get("threadId", "")
155+
156+
# Extract headers for subject and sender
157+
payload = message.get("payload", {})
158+
headers = payload.get("headers", [])
159+
160+
subject = "No Subject"
161+
sender = "Unknown Sender"
162+
date_str = "Unknown Date"
163+
164+
for header in headers:
165+
name = header.get("name", "").lower()
166+
value = header.get("value", "")
167+
if name == "subject":
168+
subject = value
169+
elif name == "from":
170+
sender = value
171+
elif name == "date":
172+
date_str = value
173+
174+
if not message_id:
175+
logger.warning(f"Skipping message with missing ID: {subject}")
176+
skipped_messages.append(f"{subject} (missing ID)")
177+
documents_skipped += 1
178+
continue
179+
180+
# Format message to markdown
181+
markdown_content = gmail_connector.format_message_to_markdown(message)
182+
183+
if not markdown_content.strip():
184+
logger.warning(f"Skipping message with no content: {subject}")
185+
skipped_messages.append(f"{subject} (no content)")
186+
documents_skipped += 1
187+
continue
188+
189+
# Create a simple summary
190+
summary_content = f"Google Gmail Message: {subject}\n\n"
191+
summary_content += f"Sender: {sender}\n"
192+
summary_content += f"Date: {date_str}\n"
193+
194+
# Generate content hash
195+
content_hash = generate_content_hash(markdown_content, search_space_id)
196+
197+
# Check if document already exists
198+
existing_document_by_hash = await check_duplicate_document_by_hash(
199+
session, content_hash
200+
)
201+
202+
if existing_document_by_hash:
203+
logger.info(
204+
f"Document with content hash {content_hash} already exists for message {message_id}. Skipping processing."
205+
)
206+
documents_skipped += 1
207+
continue
208+
209+
# Generate embedding for the summary
210+
summary_embedding = config.embedding_model_instance.embed(
211+
summary_content
212+
)
213+
214+
# Process chunks
215+
chunks = await create_document_chunks(markdown_content)
216+
217+
# Create and store new document
218+
logger.info(f"Creating new document for Gmail message: {subject}")
219+
document = Document(
220+
search_space_id=search_space_id,
221+
title=f"Gmail: {subject}",
222+
document_type=DocumentType.GOOGLE_GMAIL_CONNECTOR,
223+
document_metadata={
224+
"message_id": message_id,
225+
"thread_id": thread_id,
226+
"subject": subject,
227+
"sender": sender,
228+
"date": date_str,
229+
"connector_id": connector_id,
230+
},
231+
content=markdown_content,
232+
content_hash=content_hash,
233+
embedding=summary_embedding,
234+
chunks=chunks,
235+
)
236+
session.add(document)
237+
documents_indexed += 1
238+
logger.info(f"Successfully indexed new email {summary_content}")
239+
240+
except Exception as e:
241+
logger.error(
242+
f"Error processing the email {message_id}: {e!s}",
243+
exc_info=True,
244+
)
245+
skipped_messages.append(f"{subject} (processing error)")
246+
documents_skipped += 1
247+
continue # Skip this message and continue with others
248+
249+
# Update the last_indexed_at timestamp for the connector only if requested
250+
total_processed = documents_indexed
251+
if total_processed > 0:
252+
await update_connector_last_indexed(session, connector, update_last_indexed)
253+
254+
# Commit all changes
255+
await session.commit()
256+
logger.info(
257+
"Successfully committed all Google gmail document changes to database"
258+
)
259+
260+
# Log success
261+
await task_logger.log_task_success(
262+
log_entry,
263+
f"Successfully completed Google gmail indexing for connector {connector_id}",
264+
{
265+
"events_processed": total_processed,
266+
"documents_indexed": documents_indexed,
267+
"documents_skipped": documents_skipped,
268+
"skipped_messages_count": len(skipped_messages),
269+
},
270+
)
271+
272+
logger.info(
273+
f"Google gmail indexing completed: {documents_indexed} new emails, {documents_skipped} skipped"
274+
)
275+
return (
276+
total_processed,
277+
None,
278+
) # Return None as the error message to indicate success
279+
280+
except SQLAlchemyError as db_error:
281+
await session.rollback()
282+
await task_logger.log_task_failure(
283+
log_entry,
284+
f"Database error during Google gmail indexing for connector {connector_id}",
285+
str(db_error),
286+
{"error_type": "SQLAlchemyError"},
287+
)
288+
logger.error(f"Database error: {db_error!s}", exc_info=True)
289+
return 0, f"Database error: {db_error!s}"
290+
except Exception as e:
291+
await session.rollback()
292+
await task_logger.log_task_failure(
293+
log_entry,
294+
f"Failed to index Google gmail emails for connector {connector_id}",
295+
str(e),
296+
{"error_type": type(e).__name__},
297+
)
298+
logger.error(f"Failed to index Google gmail emails: {e!s}", exc_info=True)
299+
return 0, f"Failed to index Google gmail emails: {e!s}"

0 commit comments

Comments
 (0)