eaglelandsonce commited on
Commit
886db05
·
verified ·
1 Parent(s): 2b1d7a5

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +296 -0
  2. requirements.txt +3 -0
app.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ """
3
+ Email Deadline Summarizer (Gradio)
4
+ ----------------------------------
5
+ Drag & drop a CSV of emails, paste your OpenAI API key, and get
6
+ deadline-driven summaries + next steps.
7
+
8
+ Expected CSV columns (case-insensitive, best-effort mapping):
9
+ - subject
10
+ - received / date / datetime / timestamp
11
+ - from / sender / sender_name / sender_email
12
+ - body / content / text / snippet
13
+
14
+ Outputs:
15
+ - A table with: Subject, Received, Sender Name, Summary, Next Step, Explicit Deadline
16
+ - A downloadable CSV of the results
17
+
18
+ Run:
19
+ pip install -r requirements.txt
20
+ python app.py
21
+ """
22
+
23
+ import io
24
+ import os
25
+ import json
26
+ import time
27
+ import traceback
28
+ from typing import List, Dict, Any, Optional, Tuple
29
+
30
+ import gradio as gr
31
+ import pandas as pd
32
+
33
+ # OpenAI SDK v1.x
34
+ try:
35
+ from openai import OpenAI
36
+ except Exception:
37
+ OpenAI = None
38
+
39
+
40
+ # -----------------------------
41
+ # Utilities
42
+ # -----------------------------
43
+ CANDIDATE_DATE_COLS = ["received", "date", "datetime", "timestamp"]
44
+ CANDIDATE_FROM_COLS = ["from", "sender", "sender_name", "sender_email"]
45
+ CANDIDATE_SUBJECT_COLS = ["subject", "title"]
46
+ CANDIDATE_BODY_COLS = ["body", "content", "text", "snippet"]
47
+
48
+ DEFAULT_MODEL = "gpt-4o-mini" # adjust as desired
49
+
50
+
51
+ def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
52
+ """Map common column variations to a standard schema, if possible."""
53
+ lower_cols = {c.lower().strip(): c for c in df.columns}
54
+ def pick(candidates: List[str]) -> Optional[str]:
55
+ for c in candidates:
56
+ if c in lower_cols:
57
+ return lower_cols[c]
58
+ return None
59
+
60
+ col_subject = pick(CANDIDATE_SUBJECT_COLS)
61
+ col_date = pick(CANDIDATE_DATE_COLS)
62
+ col_from = pick(CANDIDATE_FROM_COLS)
63
+ col_body = pick(CANDIDATE_BODY_COLS)
64
+
65
+ # Create a new standardized dataframe with only the columns we need (if available)
66
+ std = pd.DataFrame()
67
+ if col_subject and col_subject in df:
68
+ std["subject"] = df[col_subject].astype(str)
69
+ else:
70
+ std["subject"] = ""
71
+
72
+ if col_date and col_date in df:
73
+ std["received"] = df[col_date].astype(str)
74
+ else:
75
+ std["received"] = ""
76
+
77
+ if col_from and col_from in df:
78
+ std["sender_name"] = df[col_from].astype(str)
79
+ else:
80
+ std["sender_name"] = ""
81
+
82
+ if col_body and col_body in df:
83
+ std["body"] = df[col_body].astype(str)
84
+ else:
85
+ # If no body-like column found, try to assemble from other fields
86
+ std["body"] = (
87
+ df.apply(lambda r: " ".join([str(x) for x in r.values if pd.notna(x)]), axis=1)
88
+ if not df.empty else ""
89
+ ).astype(str)
90
+
91
+ return std
92
+
93
+
94
+ def _build_prompt() -> str:
95
+ """The instruction we send for each email."""
96
+ return (
97
+ "You are an executive assistant that triages emails for deadlines.\n"
98
+ "For the given email (subject, received time, sender, and body), produce a JSON object with:\n"
99
+ "- Subject: the email's subject line\n"
100
+ "- Received: the time/date received (restate clearly)\n"
101
+ "- Sender Name: the sender's name (or best guess from From field)\n"
102
+ "- Summary: a concise 2–3 sentence summary of the content\n"
103
+ "- Next Step: one concrete action item to meet the deadline\n"
104
+ "- Explicit Deadline: a specific date/time. If none is stated, infer the *earliest prudent* deadline (today if urgent) and clearly label as inferred.\n\n"
105
+ "Rules:\n"
106
+ "1) If no action or deadline is implied, mark Explicit Deadline as 'None' and Next Step as 'Monitor only'.\n"
107
+ "2) Keep JSON keys exactly as written above.\n"
108
+ "3) Return ONLY valid minified JSON (no backticks, no extra text)."
109
+ )
110
+
111
+
112
+ def _call_openai(client: "OpenAI", model: str, email: Dict[str, str]) -> Dict[str, Any]:
113
+ """Call OpenAI to summarize a single email into deadline-driven JSON."""
114
+ system = _build_prompt()
115
+ user = json.dumps({
116
+ "Subject": email.get("subject", ""),
117
+ "Received": email.get("received", ""),
118
+ "Sender Name": email.get("sender_name", ""),
119
+ "Body": email.get("body", ""),
120
+ }, ensure_ascii=False)
121
+
122
+ resp = client.responses.create(
123
+ model=model,
124
+ input=[
125
+ {"role": "system", "content": system},
126
+ {"role": "user", "content": user},
127
+ ],
128
+ temperature=0.2,
129
+ )
130
+ # Extract text depending on SDK's shape; using .output_text for convenience
131
+ text = getattr(resp, "output_text", None)
132
+ if text is None:
133
+ # Fallback: attempt to navigate the structure
134
+ try:
135
+ text = resp.output[0].content[0].text
136
+ except Exception:
137
+ text = ""
138
+
139
+ # Parse JSON
140
+ try:
141
+ data = json.loads(text)
142
+ if not isinstance(data, dict):
143
+ raise ValueError("Model did not return a JSON object.")
144
+ return {
145
+ "Subject": data.get("Subject", email.get("subject", "")),
146
+ "Received": data.get("Received", email.get("received", "")),
147
+ "Sender Name": data.get("Sender Name", email.get("sender_name", "")),
148
+ "Summary": data.get("Summary", ""),
149
+ "Next Step": data.get("Next Step", ""),
150
+ "Explicit Deadline": data.get("Explicit Deadline", ""),
151
+ }
152
+ except Exception:
153
+ # Return a recoverable error row
154
+ return {
155
+ "Subject": email.get("subject", ""),
156
+ "Received": email.get("received", ""),
157
+ "Sender Name": email.get("sender_name", ""),
158
+ "Summary": f"ERROR parsing model output. Raw: {text[:400]}",
159
+ "Next Step": "—",
160
+ "Explicit Deadline": "—",
161
+ }
162
+
163
+
164
+ def process_csv(
165
+ csv_file: Optional[io.BytesIO],
166
+ api_key: str,
167
+ model: str,
168
+ max_rows: int,
169
+ assume_utc_dates: bool,
170
+ ) -> Tuple[pd.DataFrame, str]:
171
+ """Main pipeline: read CSV, normalize, LLM summarize, return DF + CSV bytes path."""
172
+ if OpenAI is None:
173
+ raise RuntimeError("OpenAI SDK not installed. Please `pip install openai>=1.40`.")
174
+
175
+ if not api_key:
176
+ raise gr.Error("Please provide your OpenAI API key.")
177
+
178
+ if csv_file is None:
179
+ raise gr.Error("Please upload a CSV file.")
180
+
181
+ # Load CSV
182
+ try:
183
+ df = pd.read_csv(csv_file)
184
+ except Exception:
185
+ # Try with ISO-8859-1 fallback
186
+ csv_file.seek(0)
187
+ df = pd.read_csv(csv_file, encoding="latin-1")
188
+
189
+ if df.empty:
190
+ raise gr.Error("The uploaded CSV appears to be empty.")
191
+
192
+ # Normalize cols -> subject, received, sender_name, body
193
+ std = _normalize_columns(df)
194
+
195
+ # Trim to max_rows
196
+ if max_rows > 0:
197
+ std = std.head(max_rows)
198
+
199
+ # Date normalization (optional best-effort)
200
+ if assume_utc_dates and "received" in std.columns:
201
+ # Just a simple pass-through; user can format later
202
+ std["received"] = std["received"].astype(str)
203
+
204
+ client = OpenAI(api_key=api_key)
205
+
206
+ # Process rows
207
+ rows = []
208
+ for _, r in std.iterrows():
209
+ email = {
210
+ "subject": r.get("subject", ""),
211
+ "received": r.get("received", ""),
212
+ "sender_name": r.get("sender_name", ""),
213
+ "body": r.get("body", ""),
214
+ }
215
+ try:
216
+ out = _call_openai(client, model, email)
217
+ except Exception as e:
218
+ out = {
219
+ "Subject": email["subject"],
220
+ "Received": email["received"],
221
+ "Sender Name": email["sender_name"],
222
+ "Summary": f"ERROR calling model: {str(e)}",
223
+ "Next Step": "—",
224
+ "Explicit Deadline": "—",
225
+ }
226
+ rows.append(out)
227
+ # gentle pacing to avoid rate spikes
228
+ time.sleep(0.15)
229
+
230
+ result_df = pd.DataFrame(rows, columns=[
231
+ "Subject", "Received", "Sender Name", "Summary", "Next Step", "Explicit Deadline"
232
+ ])
233
+
234
+ # Save CSV to a temp in /mnt/data for download
235
+ out_path = "/mnt/data/deadline_email_summaries.csv"
236
+ result_df.to_csv(out_path, index=False)
237
+
238
+ return result_df, out_path
239
+
240
+
241
+ # -----------------------------
242
+ # Gradio UI
243
+ # -----------------------------
244
+ with gr.Blocks(title="Email Deadline Summarizer") as demo:
245
+ gr.Markdown(
246
+ "# Email Deadline Summarizer\n"
247
+ "Upload a CSV of emails, add your OpenAI API key, and get deadline-driven summaries + next steps.\n"
248
+ "- ⚠️ Costs: Each row triggers a model call. Use the row limit to control spend.\n"
249
+ "- 🔐 Your key is used only in this session."
250
+ )
251
+
252
+ with gr.Row():
253
+ csv_in = gr.File(label="Drag & drop your CSV", file_types=[".csv"])
254
+ api_key_in = gr.Textbox(
255
+ label="OpenAI API Key (starts with `sk-...`)",
256
+ type="password",
257
+ placeholder="Paste your key here"
258
+ )
259
+
260
+ with gr.Row():
261
+ model_in = gr.Dropdown(
262
+ label="Model",
263
+ choices=[
264
+ "gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini",
265
+ "gpt-4o-mini-transcribe", "gpt-4o-realtime-preview"
266
+ ],
267
+ value=DEFAULT_MODEL
268
+ )
269
+ max_rows_in = gr.Slider(
270
+ label="Max rows to process (per run)",
271
+ minimum=1, maximum=500, value=25, step=1
272
+ )
273
+ assume_utc_in = gr.Checkbox(
274
+ label="Received timestamps are UTC strings (best-effort)",
275
+ value=True
276
+ )
277
+
278
+ run_btn = gr.Button("Summarize Emails")
279
+ out_df = gr.Dataframe(label="Deadline-Driven Summaries", interactive=False)
280
+ out_file = gr.File(label="Download results CSV")
281
+
282
+ def _run(csv_file, api_key, model, max_rows, assume_utc):
283
+ try:
284
+ return process_csv(csv_file, api_key, model, int(max_rows), bool(assume_utc))
285
+ except Exception as e:
286
+ tb = traceback.format_exc()
287
+ raise gr.Error(f"{e}\n\n{tb}")
288
+
289
+ run_btn.click(
290
+ fn=_run,
291
+ inputs=[csv_in, api_key_in, model_in, max_rows_in, assume_utc_in],
292
+ outputs=[out_df, out_file]
293
+ )
294
+
295
+ if __name__ == "__main__":
296
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=4.44.0
2
+ pandas>=2.1.4
3
+ openai>=1.40.0