Ultronprime commited on
Commit
2073328
·
verified ·
1 Parent(s): bcd0fb1

Create app. py

Browse files
Files changed (1) hide show
  1. app. py +655 -0
app. py ADDED
@@ -0,0 +1,655 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import json
4
+ import pandas as pd
5
+ import numpy as np
6
+ import datetime
7
+ import plotly.express as px
8
+ import plotly.graph_objects as go
9
+ import msal
10
+ import requests
11
+ from sentence_transformers import SentenceTransformer
12
+ from sklearn.metrics.pairwise import cosine_similarity
13
+ import threading
14
+ import time
15
+ from transformers import pipeline
16
+ import tempfile
17
+
18
+ # Configuration
19
+ MS_CLIENT_ID = os.getenv("MS_CLIENT_ID", "ff0d5b77-56a9-4fa0-bd59-5c7b4889186e")
20
+ MS_TENANT_ID = os.getenv("MS_TENANT_ID", "677c00b7-cf19-4fef-9962-132a076ae325")
21
+ MS_AUTHORITY = f"https://login.microsoftonline.com/{MS_TENANT_ID}"
22
+ MS_REDIRECT_URI = os.getenv("MS_REDIRECT_URI", "https://huggingface.co/spaces/YOUR-USERNAME/email-thread-analyzer/")
23
+
24
+ # Microsoft Graph API scopes
25
+ SCOPES = [
26
+ "User.Read",
27
+ "Mail.Read",
28
+ "Mail.ReadBasic",
29
+ ]
30
+
31
+ # Global variables
32
+ auth_app = None
33
+ current_user = None
34
+ user_token = None
35
+ emails = []
36
+ email_threads = {}
37
+ embeddings = {}
38
+ qa_data = {}
39
+ qa_model = None
40
+ embedding_model = None
41
+ search_results = []
42
+
43
+ # Initialize MSAL app
44
+ def init_auth_app():
45
+ global auth_app
46
+ auth_app = msal.PublicClientApplication(
47
+ client_id=MS_CLIENT_ID,
48
+ authority=MS_AUTHORITY
49
+ )
50
+
51
+ # Initialize models
52
+ def init_models():
53
+ global embedding_model, qa_model
54
+ try:
55
+ embedding_model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
56
+ qa_model = pipeline("question-answering", model="deepset/roberta-base-squad2")
57
+ return "Models initialized successfully"
58
+ except Exception as e:
59
+ print(f"Error initializing models: {e}")
60
+ embedding_model = None
61
+ qa_model = None
62
+ return f"Error initializing models: {e}"
63
+
64
+ # Get authorization URL
65
+ def get_auth_url():
66
+ auth_url = auth_app.get_authorization_request_url(
67
+ scopes=SCOPES,
68
+ redirect_uri=MS_REDIRECT_URI,
69
+ state="state"
70
+ )
71
+ return auth_url
72
+
73
+ # Process auth code
74
+ def process_auth_code(auth_code):
75
+ global current_user, user_token
76
+
77
+ try:
78
+ # Acquire token
79
+ token_response = auth_app.acquire_token_by_authorization_code(
80
+ code=auth_code,
81
+ scopes=SCOPES,
82
+ redirect_uri=MS_REDIRECT_URI
83
+ )
84
+
85
+ if "error" in token_response:
86
+ return f"Error: {token_response['error_description']}"
87
+
88
+ # Store token
89
+ user_token = token_response
90
+
91
+ # Get user info
92
+ user_response = requests.get(
93
+ "https://graph.microsoft.com/v1.0/me",
94
+ headers={"Authorization": f"Bearer {user_token['access_token']}"}
95
+ )
96
+
97
+ if user_response.status_code == 200:
98
+ current_user = user_response.json()
99
+ return f"Successfully authenticated as {current_user['displayName']}"
100
+ else:
101
+ return f"Error getting user info: {user_response.text}"
102
+
103
+ except Exception as e:
104
+ return f"Error during authentication: {str(e)}"
105
+
106
+ # Get mail folders
107
+ def get_mail_folders():
108
+ if not user_token:
109
+ return [], "Not authenticated"
110
+
111
+ try:
112
+ response = requests.get(
113
+ "https://graph.microsoft.com/v1.0/me/mailFolders",
114
+ headers={"Authorization": f"Bearer {user_token['access_token']}"}
115
+ )
116
+
117
+ if response.status_code == 200:
118
+ folders = response.json()["value"]
119
+ return [(folder["displayName"], folder["id"]) for folder in folders], None
120
+ else:
121
+ return [], f"Error: {response.text}"
122
+
123
+ except Exception as e:
124
+ return [], f"Error: {str(e)}"
125
+
126
+ # Extract emails from folder
127
+ def extract_emails(folder_id, max_emails=100, batch_size=25, start_date=None, end_date=None):
128
+ global emails, email_threads, embeddings
129
+
130
+ if not user_token:
131
+ return "Not authenticated"
132
+
133
+ try:
134
+ # Reset data
135
+ emails = []
136
+ email_threads = {}
137
+ embeddings = {}
138
+
139
+ # Prepare filter
140
+ filter_query = ""
141
+ if start_date and end_date:
142
+ start_date_iso = datetime.datetime.strptime(start_date, "%Y-%m-%d").isoformat() + "Z"
143
+ end_date_iso = datetime.datetime.strptime(end_date, "%Y-%m-%d").isoformat() + "Z"
144
+ filter_query = f"receivedDateTime ge {start_date_iso} and receivedDateTime le {end_date_iso}"
145
+
146
+ # Extract emails in batches
147
+ for i in range(0, max_emails, batch_size):
148
+ # Prepare request
149
+ url = f"https://graph.microsoft.com/v1.0/me/mailFolders/{folder_id}/messages"
150
+ headers = {"Authorization": f"Bearer {user_token['access_token']}"}
151
+ params = {
152
+ "$select": "id,subject,sender,from,toRecipients,ccRecipients,receivedDateTime,conversationId,bodyPreview,uniqueBody",
153
+ "$top": batch_size,
154
+ "$skip": i
155
+ }
156
+
157
+ if filter_query:
158
+ params["$filter"] = filter_query
159
+
160
+ # Make request
161
+ response = requests.get(url, headers=headers, params=params)
162
+
163
+ if response.status_code != 200:
164
+ return f"Error: {response.text}"
165
+
166
+ batch_emails = response.json()["value"]
167
+
168
+ if not batch_emails:
169
+ break
170
+
171
+ emails.extend(batch_emails)
172
+
173
+ if len(emails) >= max_emails:
174
+ emails = emails[:max_emails]
175
+ break
176
+
177
+ # Organize emails into threads
178
+ organize_email_threads()
179
+
180
+ # Generate embeddings in background
181
+ threading.Thread(target=generate_embeddings).start()
182
+
183
+ return f"Successfully extracted {len(emails)} emails organized into {len(email_threads)} threads"
184
+
185
+ except Exception as e:
186
+ return f"Error: {str(e)}"
187
+
188
+ # Organize emails into threads
189
+ def organize_email_threads():
190
+ global email_threads
191
+
192
+ threads = {}
193
+
194
+ for email in emails:
195
+ conversation_id = email["conversationId"]
196
+
197
+ if conversation_id not in threads:
198
+ threads[conversation_id] = []
199
+
200
+ threads[conversation_id].append(email)
201
+
202
+ # Sort emails within each thread by date
203
+ for thread_id, thread_emails in threads.items():
204
+ thread_emails.sort(key=lambda x: x["receivedDateTime"])
205
+
206
+ # Extract thread metadata
207
+ threads[thread_id] = {
208
+ "emails": thread_emails,
209
+ "subject": thread_emails[0]["subject"],
210
+ "start_date": thread_emails[0]["receivedDateTime"],
211
+ "end_date": thread_emails[-1]["receivedDateTime"],
212
+ "message_count": len(thread_emails),
213
+ "participants": get_unique_participants(thread_emails)
214
+ }
215
+
216
+ email_threads = threads
217
+
218
+ # Get unique participants
219
+ def get_unique_participants(thread_emails):
220
+ participants = set()
221
+
222
+ for email in thread_emails:
223
+ # Add sender
224
+ if "sender" in email and "emailAddress" in email["sender"]:
225
+ participants.add(email["sender"]["emailAddress"]["address"])
226
+
227
+ # Add recipients
228
+ if "toRecipients" in email:
229
+ for recipient in email["toRecipients"]:
230
+ participants.add(recipient["emailAddress"]["address"])
231
+
232
+ # Add CC recipients
233
+ if "ccRecipients" in email:
234
+ for recipient in email["ccRecipients"]:
235
+ participants.add(recipient["emailAddress"]["address"])
236
+
237
+ return list(participants)
238
+
239
+ # Generate embeddings for search
240
+ def generate_embeddings():
241
+ global embeddings
242
+
243
+ if not embedding_model or not email_threads:
244
+ return
245
+
246
+ for thread_id, thread in email_threads.items():
247
+ # Create text representation of thread
248
+ text = thread["subject"] + " " + " ".join([email["bodyPreview"] for email in thread["emails"]])
249
+
250
+ # Generate embedding
251
+ embedding = embedding_model.encode(text)
252
+
253
+ # Store embedding
254
+ embeddings[thread_id] = embedding
255
+
256
+ # Search threads
257
+ def search_threads(query):
258
+ global search_results
259
+
260
+ if not query or not embedding_model or not embeddings:
261
+ search_results = []
262
+ return "Please enter a search query and ensure emails have been extracted"
263
+
264
+ try:
265
+ # Generate query embedding
266
+ query_embedding = embedding_model.encode(query)
267
+
268
+ # Calculate similarity scores
269
+ scores = []
270
+ for thread_id, thread_embedding in embeddings.items():
271
+ similarity = cosine_similarity([query_embedding], [thread_embedding])[0][0]
272
+ scores.append((thread_id, similarity))
273
+
274
+ # Sort by similarity and filter out low scores
275
+ scores.sort(key=lambda x: x[1], reverse=True)
276
+ relevant_threads = [thread_id for thread_id, score in scores if score > 0.2]
277
+
278
+ # Get thread data
279
+ search_results = [email_threads[thread_id] for thread_id in relevant_threads]
280
+
281
+ if not search_results:
282
+ return "No relevant threads found"
283
+
284
+ return f"Found {len(search_results)} relevant threads"
285
+
286
+ except Exception as e:
287
+ search_results = []
288
+ return f"Error: {str(e)}"
289
+
290
+ # Generate Q&A for thread
291
+ def generate_qa(thread_id):
292
+ if not qa_model or thread_id not in email_threads:
293
+ return "Unable to generate Q&A - model not loaded or thread not found"
294
+
295
+ try:
296
+ thread = email_threads[thread_id]
297
+
298
+ # Create thread context
299
+ context = f"Thread subject: {thread['subject']}\n\n"
300
+ for email in thread["emails"]:
301
+ sender = email["sender"]["emailAddress"]["address"]
302
+ context += f"From: {sender}\n"
303
+ context += f"Date: {email['receivedDateTime']}\n"
304
+ context += f"Content: {email['bodyPreview']}\n\n"
305
+
306
+ # Generate sample questions
307
+ questions = [
308
+ f"What is the main topic of this email thread about '{thread['subject']}'?",
309
+ "Who are the key participants in this conversation?",
310
+ "What was the timeline of this discussion?",
311
+ "What were the main points discussed in this thread?"
312
+ ]
313
+
314
+ # Generate answers
315
+ answers = []
316
+ for question in questions:
317
+ try:
318
+ result = qa_model(question=question, context=context)
319
+ answers.append(result["answer"])
320
+ except Exception as e:
321
+ answers.append(f"Error generating answer: {str(e)}")
322
+
323
+ # Create summary
324
+ summary = f"This is an email thread with {thread['message_count']} messages about '{thread['subject']}'. "
325
+ summary += f"The conversation started on {thread['start_date']} and ended on {thread['end_date']}. "
326
+ summary += f"There are {len(thread['participants'])} participants in this thread."
327
+
328
+ # Store Q&A data
329
+ qa_data[thread_id] = {
330
+ "questions": questions,
331
+ "answers": answers,
332
+ "summary": summary
333
+ }
334
+
335
+ return f"Generated {len(questions)} Q&A pairs for thread"
336
+
337
+ except Exception as e:
338
+ return f"Error generating Q&A: {str(e)}"
339
+
340
+ # Get thread size distribution
341
+ def get_thread_size_distribution():
342
+ if not email_threads:
343
+ return None
344
+
345
+ # Count threads by size
346
+ sizes = {}
347
+ for thread in email_threads.values():
348
+ size = thread["message_count"]
349
+ if size in sizes:
350
+ sizes[size] += 1
351
+ else:
352
+ sizes[size] = 1
353
+
354
+ # Convert to dataframe
355
+ df = pd.DataFrame([
356
+ {"Size": size, "Count": count}
357
+ for size, count in sizes.items()
358
+ ])
359
+
360
+ # Sort by size
361
+ df = df.sort_values("Size")
362
+
363
+ # Create chart
364
+ fig = px.bar(df, x="Size", y="Count", title="Thread Size Distribution")
365
+ return fig
366
+
367
+ # Get activity over time
368
+ def get_activity_over_time():
369
+ if not emails:
370
+ return None
371
+
372
+ # Count emails by date
373
+ dates = {}
374
+ for email in emails:
375
+ date = email["receivedDateTime"].split("T")[0]
376
+ if date in dates:
377
+ dates[date] += 1
378
+ else:
379
+ dates[date] = 1
380
+
381
+ # Convert to dataframe
382
+ df = pd.DataFrame([
383
+ {"Date": date, "Count": count}
384
+ for date, count in dates.items()
385
+ ])
386
+
387
+ # Sort by date
388
+ df = df.sort_values("Date")
389
+
390
+ # Create chart
391
+ fig = px.line(df, x="Date", y="Count", title="Activity Over Time")
392
+ return fig
393
+
394
+ # Get participant activity
395
+ def get_participant_activity():
396
+ if not emails:
397
+ return None
398
+
399
+ # Count emails by sender
400
+ senders = {}
401
+ for email in emails:
402
+ if "sender" in email and "emailAddress" in email["sender"]:
403
+ sender = email["sender"]["emailAddress"]["address"]
404
+ if sender in senders:
405
+ senders[sender] += 1
406
+ else:
407
+ senders[sender] = 1
408
+
409
+ # Convert to dataframe
410
+ df = pd.DataFrame([
411
+ {"Participant": sender, "Count": count}
412
+ for sender, count in senders.items()
413
+ ])
414
+
415
+ # Sort by count
416
+ df = df.sort_values("Count", ascending=False).head(10)
417
+
418
+ # Create chart
419
+ fig = px.bar(df, x="Count", y="Participant", title="Top 10 Participants", orientation='h')
420
+ return fig
421
+
422
+ # Export thread data with Q&A
423
+ def export_thread_data(thread_id):
424
+ if thread_id not in email_threads:
425
+ return None
426
+
427
+ thread = email_threads[thread_id]
428
+ qa = qa_data.get(thread_id, {"questions": [], "answers": [], "summary": ""})
429
+
430
+ export_data = {
431
+ "subject": thread["subject"],
432
+ "start_date": thread["start_date"],
433
+ "end_date": thread["end_date"],
434
+ "message_count": thread["message_count"],
435
+ "participants": thread["participants"],
436
+ "emails": [
437
+ {
438
+ "sender": email["sender"]["emailAddress"]["address"],
439
+ "received_date_time": email["receivedDateTime"],
440
+ "subject": email["subject"],
441
+ "body_preview": email["bodyPreview"]
442
+ }
443
+ for email in thread["emails"]
444
+ ],
445
+ "qa": qa
446
+ }
447
+
448
+ # Save to temporary file
449
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.json', mode='w') as f:
450
+ json.dump(export_data, f, indent=2)
451
+ return f.name
452
+
453
+ # Initialize
454
+ init_auth_app()
455
+ init_status = init_models()
456
+
457
+ # Create the Gradio interface
458
+ with gr.Blocks(title="Email Thread Analyzer with AI Q&A") as demo:
459
+ gr.Markdown("# Email Thread Analyzer with AI Q&A")
460
+
461
+ # Authentication section
462
+ with gr.Tab("Authentication"):
463
+ with gr.Row():
464
+ with gr.Column(scale=2):
465
+ gr.Markdown("## Sign in with Microsoft")
466
+ gr.Markdown("1. Click 'Get Authentication URL' to start the sign-in process")
467
+ gr.Markdown("2. Copy the authorization code from the redirect URL")
468
+ gr.Markdown("3. Paste the code below and submit")
469
+
470
+ with gr.Column(scale=3):
471
+ auth_url_button = gr.Button("Get Authentication URL")
472
+ auth_url_output = gr.Textbox(label="Authentication URL", interactive=False)
473
+ auth_code_input = gr.Textbox(label="Authorization Code")
474
+ auth_submit = gr.Button("Submit Authorization Code")
475
+ auth_status = gr.Textbox(label="Authentication Status", interactive=False, value=f"AI Models: {init_status}")
476
+
477
+ # Email Extraction section
478
+ with gr.Tab("Email Extraction"):
479
+ with gr.Row():
480
+ with gr.Column():
481
+ folder_dropdown = gr.Dropdown(label="Select Mail Folder")
482
+ refresh_folders_button = gr.Button("Refresh Folders")
483
+
484
+ with gr.Row():
485
+ max_emails_input = gr.Number(label="Max Emails", value=100, minimum=1, maximum=1000)
486
+ batch_size_input = gr.Number(label="Batch Size", value=25, minimum=1, maximum=100)
487
+
488
+ with gr.Row():
489
+ start_date_input = gr.Textbox(label="Start Date (YYYY-MM-DD)")
490
+ end_date_input = gr.Textbox(label="End Date (YYYY-MM-DD)")
491
+
492
+ extract_button = gr.Button("Extract Emails")
493
+ extraction_status = gr.Textbox(label="Extraction Status", interactive=False)
494
+
495
+ # Thread Analysis section
496
+ with gr.Tab("Thread Analysis"):
497
+ with gr.Row():
498
+ with gr.Column():
499
+ analysis_status = gr.Textbox(label="Analysis Status")
500
+
501
+ with gr.Tabs():
502
+ with gr.Tab("Thread Size"):
503
+ thread_size_plot = gr.Plot(label="Thread Size Distribution")
504
+
505
+ with gr.Tab("Activity Over Time"):
506
+ activity_plot = gr.Plot(label="Activity Over Time")
507
+
508
+ with gr.Tab("Top Participants"):
509
+ participants_plot = gr.Plot(label="Top Participants")
510
+
511
+ generate_analytics_button = gr.Button("Generate Analytics")
512
+
513
+ # Search section
514
+ with gr.Tab("Search"):
515
+ with gr.Row():
516
+ with gr.Column():
517
+ search_input = gr.Textbox(label="Search Query")
518
+ search_button = gr.Button("Search")
519
+ search_status = gr.Textbox(label="Search Status", interactive=False)
520
+
521
+ with gr.Column():
522
+ search_results_dropdown = gr.Dropdown(label="Search Results")
523
+ view_thread_button = gr.Button("View Thread")
524
+
525
+ # Q&A section
526
+ with gr.Tab("Q&A"):
527
+ with gr.Row():
528
+ with gr.Column():
529
+ thread_info = gr.Textbox(label="Thread Information", interactive=False)
530
+ qa_status = gr.Textbox(label="Q&A Status", interactive=False)
531
+
532
+ with gr.Accordion("Thread Content", open=False):
533
+ thread_content = gr.Textbox(label="Thread Content", interactive=False, lines=10)
534
+
535
+ with gr.Row():
536
+ question_dropdown = gr.Dropdown(label="Questions")
537
+ gen_qa_button = gr.Button("Generate Q&A")
538
+
539
+ answer_output = gr.Textbox(label="Answer", interactive=False, lines=5)
540
+ summary_output = gr.Textbox(label="Summary", interactive=False, lines=5)
541
+
542
+ export_thread_button = gr.Button("Export Thread Data")
543
+ export_output = gr.File(label="Export Data")
544
+
545
+ # Set up event handlers
546
+ # Authentication events
547
+ auth_url_button.click(
548
+ fn=get_auth_url,
549
+ outputs=auth_url_output
550
+ )
551
+
552
+ auth_submit.click(
553
+ fn=process_auth_code,
554
+ inputs=auth_code_input,
555
+ outputs=auth_status
556
+ )
557
+
558
+ # Folder refresh event
559
+ refresh_folders_button.click(
560
+ fn=lambda: get_mail_folders()[0],
561
+ outputs=folder_dropdown
562
+ )
563
+
564
+ # Email extraction event
565
+ extract_button.click(
566
+ fn=extract_emails,
567
+ inputs=[folder_dropdown, max_emails_input, batch_size_input, start_date_input, end_date_input],
568
+ outputs=extraction_status
569
+ )
570
+
571
+ # Analytics generation event
572
+ generate_analytics_button.click(
573
+ fn=lambda: (
574
+ "Analytics generated successfully",
575
+ get_thread_size_distribution(),
576
+ get_activity_over_time(),
577
+ get_participant_activity()
578
+ ),
579
+ outputs=[analysis_status, thread_size_plot, activity_plot, participants_plot]
580
+ )
581
+
582
+ # Search events
583
+ search_button.click(
584
+ fn=lambda query: (
585
+ search_threads(query),
586
+ [f"{thread['subject']} ({thread['message_count']} messages)" for thread in search_results]
587
+ ),
588
+ inputs=search_input,
589
+ outputs=[search_status, search_results_dropdown]
590
+ )
591
+
592
+ # Thread view event
593
+ def view_thread_details(thread_idx):
594
+ if not search_results or thread_idx < 0 or thread_idx >= len(search_results):
595
+ return "No thread selected", "", [], "", "", None
596
+
597
+ thread = search_results[thread_idx]
598
+ thread_id = thread["emails"][0]["conversationId"]
599
+
600
+ # Generate thread content
601
+ content = f"Subject: {thread['subject']}\n\n"
602
+ for email in thread["emails"]:
603
+ sender = email["sender"]["emailAddress"]["address"]
604
+ date = email["receivedDateTime"]
605
+ content += f"From: {sender} | Date: {date}\n"
606
+ content += f"Content: {email['bodyPreview']}\n\n"
607
+
608
+ # Generate Q&A if not already generated
609
+ qa_result = "Q&A already generated"
610
+ if thread_id not in qa_data:
611
+ qa_result = generate_qa(thread_id)
612
+
613
+ # Get questions, answer, summary
614
+ questions = qa_data.get(thread_id, {}).get("questions", [])
615
+ answer = qa_data.get(thread_id, {}).get("answers", [""])[0] if questions else ""
616
+ summary = qa_data.get(thread_id, {}).get("summary", "")
617
+
618
+ # Export data
619
+ export_data = export_thread_data(thread_id)
620
+
621
+ return f"Thread: {thread['subject']} ({thread['message_count']} messages)", content, questions, answer, summary, export_data
622
+
623
+ view_thread_button.click(
624
+ fn=lambda: view_thread_details(0 if not search_results_dropdown.value else search_results_dropdown.index),
625
+ outputs=[thread_info, thread_content, question_dropdown, answer_output, summary_output, export_output]
626
+ )
627
+
628
+ # Q&A events
629
+ question_dropdown.change(
630
+ fn=lambda q, thread_idx: qa_data.get(search_results[thread_idx]["emails"][0]["conversationId"], {}).get("answers", [""])[qa_data.get(search_results[thread_idx]["emails"][0]["conversationId"], {}).get("questions", []).index(q)] if q and thread_idx >= 0 and thread_idx < len(search_results) and search_results[thread_idx]["emails"][0]["conversationId"] in qa_data and q in qa_data.get(search_results[thread_idx]["emails"][0]["conversationId"], {}).get("questions", []) else "",
631
+ inputs=[question_dropdown, lambda: 0 if not search_results_dropdown.value else search_results_dropdown.index],
632
+ outputs=answer_output
633
+ )
634
+
635
+ gen_qa_button.click(
636
+ fn=lambda thread_idx: (
637
+ generate_qa(search_results[thread_idx]["emails"][0]["conversationId"]) if thread_idx >= 0 and thread_idx < len(search_results) else "No thread selected",
638
+ qa_data.get(search_results[thread_idx]["emails"][0]["conversationId"], {}).get("questions", []) if thread_idx >= 0 and thread_idx < len(search_results) else [],
639
+ qa_data.get(search_results[thread_idx]["emails"][0]["conversationId"], {}).get("answers", [""])[0] if thread_idx >= 0 and thread_idx < len(search_results) and search_results[thread_idx]["emails"][0]["conversationId"] in qa_data and qa_data.get(search_results[thread_idx]["emails"][0]["conversationId"], {}).get("questions", []) else "",
640
+ qa_data.get(search_results[thread_idx]["emails"][0]["conversationId"], {}).get("summary", "") if thread_idx >= 0 and thread_idx < len(search_results) else ""
641
+ ),
642
+ inputs=lambda: 0 if not search_results_dropdown.value else search_results_dropdown.index,
643
+ outputs=[qa_status, question_dropdown, answer_output, summary_output]
644
+ )
645
+
646
+ # Export event
647
+ export_thread_button.click(
648
+ fn=lambda thread_idx: export_thread_data(search_results[thread_idx]["emails"][0]["conversationId"]) if thread_idx >= 0 and thread_idx < len(search_results) else None,
649
+ inputs=lambda: 0 if not search_results_dropdown.value else search_results_dropdown.index,
650
+ outputs=export_output
651
+ )
652
+
653
+ # Launch the app
654
+ if __name__ == "__main__":
655
+ demo.launch()