File size: 9,913 Bytes
886db05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# app.py
"""
Email Deadline Summarizer (Gradio)
----------------------------------
Drag & drop a CSV of emails, paste your OpenAI API key, and get
deadline-driven summaries + next steps.

Expected CSV columns (case-insensitive, best-effort mapping):
- subject
- received / date / datetime / timestamp
- from / sender / sender_name / sender_email
- body / content / text / snippet

Outputs:
- A table with: Subject, Received, Sender Name, Summary, Next Step, Explicit Deadline
- A downloadable CSV of the results

Run:
    pip install -r requirements.txt
    python app.py
"""

import io
import os
import json
import time
import traceback
from typing import List, Dict, Any, Optional, Tuple

import gradio as gr
import pandas as pd

# OpenAI SDK v1.x
try:
    from openai import OpenAI
except Exception:
    OpenAI = None


# -----------------------------
# Utilities
# -----------------------------
CANDIDATE_DATE_COLS = ["received", "date", "datetime", "timestamp"]
CANDIDATE_FROM_COLS = ["from", "sender", "sender_name", "sender_email"]
CANDIDATE_SUBJECT_COLS = ["subject", "title"]
CANDIDATE_BODY_COLS = ["body", "content", "text", "snippet"]

DEFAULT_MODEL = "gpt-4o-mini"  # adjust as desired


def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
    """Map common column variations to a standard schema, if possible."""
    lower_cols = {c.lower().strip(): c for c in df.columns}
    def pick(candidates: List[str]) -> Optional[str]:
        for c in candidates:
            if c in lower_cols:
                return lower_cols[c]
        return None

    col_subject = pick(CANDIDATE_SUBJECT_COLS)
    col_date = pick(CANDIDATE_DATE_COLS)
    col_from = pick(CANDIDATE_FROM_COLS)
    col_body = pick(CANDIDATE_BODY_COLS)

    # Create a new standardized dataframe with only the columns we need (if available)
    std = pd.DataFrame()
    if col_subject and col_subject in df:
        std["subject"] = df[col_subject].astype(str)
    else:
        std["subject"] = ""

    if col_date and col_date in df:
        std["received"] = df[col_date].astype(str)
    else:
        std["received"] = ""

    if col_from and col_from in df:
        std["sender_name"] = df[col_from].astype(str)
    else:
        std["sender_name"] = ""

    if col_body and col_body in df:
        std["body"] = df[col_body].astype(str)
    else:
        # If no body-like column found, try to assemble from other fields
        std["body"] = (
            df.apply(lambda r: " ".join([str(x) for x in r.values if pd.notna(x)]), axis=1)
            if not df.empty else ""
        ).astype(str)

    return std


def _build_prompt() -> str:
    """The instruction we send for each email."""
    return (
        "You are an executive assistant that triages emails for deadlines.\n"
        "For the given email (subject, received time, sender, and body), produce a JSON object with:\n"
        "- Subject: the email's subject line\n"
        "- Received: the time/date received (restate clearly)\n"
        "- Sender Name: the sender's name (or best guess from From field)\n"
        "- Summary: a concise 2–3 sentence summary of the content\n"
        "- Next Step: one concrete action item to meet the deadline\n"
        "- 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"
        "Rules:\n"
        "1) If no action or deadline is implied, mark Explicit Deadline as 'None' and Next Step as 'Monitor only'.\n"
        "2) Keep JSON keys exactly as written above.\n"
        "3) Return ONLY valid minified JSON (no backticks, no extra text)."
    )


def _call_openai(client: "OpenAI", model: str, email: Dict[str, str]) -> Dict[str, Any]:
    """Call OpenAI to summarize a single email into deadline-driven JSON."""
    system = _build_prompt()
    user = json.dumps({
        "Subject": email.get("subject", ""),
        "Received": email.get("received", ""),
        "Sender Name": email.get("sender_name", ""),
        "Body": email.get("body", ""),
    }, ensure_ascii=False)

    resp = client.responses.create(
        model=model,
        input=[
            {"role": "system", "content": system},
            {"role": "user", "content": user},
        ],
        temperature=0.2,
    )
    # Extract text depending on SDK's shape; using .output_text for convenience
    text = getattr(resp, "output_text", None)
    if text is None:
        # Fallback: attempt to navigate the structure
        try:
            text = resp.output[0].content[0].text
        except Exception:
            text = ""

    # Parse JSON
    try:
        data = json.loads(text)
        if not isinstance(data, dict):
            raise ValueError("Model did not return a JSON object.")
        return {
            "Subject": data.get("Subject", email.get("subject", "")),
            "Received": data.get("Received", email.get("received", "")),
            "Sender Name": data.get("Sender Name", email.get("sender_name", "")),
            "Summary": data.get("Summary", ""),
            "Next Step": data.get("Next Step", ""),
            "Explicit Deadline": data.get("Explicit Deadline", ""),
        }
    except Exception:
        # Return a recoverable error row
        return {
            "Subject": email.get("subject", ""),
            "Received": email.get("received", ""),
            "Sender Name": email.get("sender_name", ""),
            "Summary": f"ERROR parsing model output. Raw: {text[:400]}",
            "Next Step": "—",
            "Explicit Deadline": "—",
        }


def process_csv(
    csv_file: Optional[io.BytesIO],
    api_key: str,
    model: str,
    max_rows: int,
    assume_utc_dates: bool,
) -> Tuple[pd.DataFrame, str]:
    """Main pipeline: read CSV, normalize, LLM summarize, return DF + CSV bytes path."""
    if OpenAI is None:
        raise RuntimeError("OpenAI SDK not installed. Please `pip install openai>=1.40`.")

    if not api_key:
        raise gr.Error("Please provide your OpenAI API key.")

    if csv_file is None:
        raise gr.Error("Please upload a CSV file.")

    # Load CSV
    try:
        df = pd.read_csv(csv_file)
    except Exception:
        # Try with ISO-8859-1 fallback
        csv_file.seek(0)
        df = pd.read_csv(csv_file, encoding="latin-1")

    if df.empty:
        raise gr.Error("The uploaded CSV appears to be empty.")

    # Normalize cols -> subject, received, sender_name, body
    std = _normalize_columns(df)

    # Trim to max_rows
    if max_rows > 0:
        std = std.head(max_rows)

    # Date normalization (optional best-effort)
    if assume_utc_dates and "received" in std.columns:
        # Just a simple pass-through; user can format later
        std["received"] = std["received"].astype(str)

    client = OpenAI(api_key=api_key)

    # Process rows
    rows = []
    for _, r in std.iterrows():
        email = {
            "subject": r.get("subject", ""),
            "received": r.get("received", ""),
            "sender_name": r.get("sender_name", ""),
            "body": r.get("body", ""),
        }
        try:
            out = _call_openai(client, model, email)
        except Exception as e:
            out = {
                "Subject": email["subject"],
                "Received": email["received"],
                "Sender Name": email["sender_name"],
                "Summary": f"ERROR calling model: {str(e)}",
                "Next Step": "—",
                "Explicit Deadline": "—",
            }
        rows.append(out)
        # gentle pacing to avoid rate spikes
        time.sleep(0.15)

    result_df = pd.DataFrame(rows, columns=[
        "Subject", "Received", "Sender Name", "Summary", "Next Step", "Explicit Deadline"
    ])

    # Save CSV to a temp in /mnt/data for download
    out_path = "/mnt/data/deadline_email_summaries.csv"
    result_df.to_csv(out_path, index=False)

    return result_df, out_path


# -----------------------------
# Gradio UI
# -----------------------------
with gr.Blocks(title="Email Deadline Summarizer") as demo:
    gr.Markdown(
        "# Email Deadline Summarizer\n"
        "Upload a CSV of emails, add your OpenAI API key, and get deadline-driven summaries + next steps.\n"
        "- ⚠️ Costs: Each row triggers a model call. Use the row limit to control spend.\n"
        "- 🔐 Your key is used only in this session."
    )

    with gr.Row():
        csv_in = gr.File(label="Drag & drop your CSV", file_types=[".csv"])
        api_key_in = gr.Textbox(
            label="OpenAI API Key (starts with `sk-...`)",
            type="password",
            placeholder="Paste your key here"
        )

    with gr.Row():
        model_in = gr.Dropdown(
            label="Model",
            choices=[
                "gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini",
                "gpt-4o-mini-transcribe", "gpt-4o-realtime-preview"
            ],
            value=DEFAULT_MODEL
        )
        max_rows_in = gr.Slider(
            label="Max rows to process (per run)",
            minimum=1, maximum=500, value=25, step=1
        )
        assume_utc_in = gr.Checkbox(
            label="Received timestamps are UTC strings (best-effort)",
            value=True
        )

    run_btn = gr.Button("Summarize Emails")
    out_df = gr.Dataframe(label="Deadline-Driven Summaries", interactive=False)
    out_file = gr.File(label="Download results CSV")

    def _run(csv_file, api_key, model, max_rows, assume_utc):
        try:
            return process_csv(csv_file, api_key, model, int(max_rows), bool(assume_utc))
        except Exception as e:
            tb = traceback.format_exc()
            raise gr.Error(f"{e}\n\n{tb}")

    run_btn.click(
        fn=_run,
        inputs=[csv_in, api_key_in, model_in, max_rows_in, assume_utc_in],
        outputs=[out_df, out_file]
    )

if __name__ == "__main__":
    demo.launch()