Skip to content

Commit 6150741

Browse files
add email analyzer example (#52)
* add email analyzer example * add example content * add readme * address review * replace tabs with spaces
1 parent 7d7a16c commit 6150741

21 files changed

+453
-0
lines changed

.gitmodules

Whitespace-only changes.

examples/email-analyzer-o1/.gitignore

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts

examples/email-analyzer-o1/README.md

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Email Analysis Workflow with OpenAI o1
2+
3+
This project allows you to automatically analyze email threads and their attachments using AI. When you forward an email to specific email address, Zapier will trigger a webhook that processes the content and sends back an AI generated response suggestion.
4+
5+
## Setup Instructions
6+
7+
### 1. Set Environment Variables
8+
9+
This application uses **Resend** to send emails and **OpenAI** for LLMs. To run the application, you need the following environment variables in the `.env` file.
10+
```
11+
OPENAI_API_KEY=
12+
QSTASH_TOKEN=
13+
RESEND_API_KEY=
14+
```
15+
### 2. Deploy Your API Endpoint
16+
17+
First, deploy your API endpoint that will receive the webhook from Zapier. The endpoint should be accessible at
18+
19+
```
20+
https://your-domain.com/api/analyze
21+
```
22+
23+
You can also use `ngrok` to setup a publicly accessible endpoint on your local. See [local development guide](https://upstash.com/docs/workflow/howto/local-development)
24+
25+
### 3. Configure Zapier Integration
26+
27+
#### Step 1: Create a New Zap
28+
1. Go to [Zapier](https://zapier.com) and click "Create Zap"
29+
2. Name your Zap (e.g., "Email Analysis Workflow")
30+
31+
#### Step 2: Configure Gmail Trigger
32+
1. Choose "Gmail" as your trigger app
33+
2. Select "New Email" as the trigger event
34+
3. Connect your Gmail account if not already done. This email address will be the account you'll forward the emails to get the response suggestions.
35+
4. Optional: Add filters to only trigger on specific emails
36+
37+
![flow](./img/flow.png)
38+
39+
#### Step 4: Configure Webhook Action
40+
1. Add a new action step
41+
2. Choose "Webhooks by Zapier"
42+
3. Select "POST" as the action event
43+
4. Configure the webhook with these settings:
44+
- **URL**: Your API endpoint (e.g., `https://your-domain.com/api/analyze`)
45+
- **Payload Type**: `json`
46+
- **Data**:
47+
```json
48+
{
49+
"message": "{{body_plain}}",
50+
"subject": "{{subject}}",
51+
"to": "{{to_email}}",
52+
"attachment": "{{attachment_1}}"
53+
}
54+
```
55+
- **Wrap Request in Array**: No
56+
- **Unflatten**: Yes
57+
58+
![webhook config](./img/webhook-config.png)
59+
60+
### Field Mappings
61+
- `message`: Use Gmail's "Body Plain" field
62+
- `subject`: Use Gmail's "Raw Payload Headers Subject" field
63+
- `to`: Use Gmail's "From Email" field
64+
- `attachment`: Use Gmail's "Attachment 1 Attachment" field
65+
66+
## Limitations
67+
68+
- Currently handles one attachment per email
69+
- Supports PDF attachments
70+
- Maximum email size limit based on your API endpoint's limitations
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { serve } from "@upstash/workflow/nextjs"
2+
import pdf from "pdf-parse"
3+
4+
5+
type EmailPayload = {
6+
message: string,
7+
subject: string,
8+
to: string
9+
attachment?: string,
10+
}
11+
12+
export const { POST } = serve<EmailPayload>(async (context) => {
13+
const { message, subject, to, attachment } = context.requestPayload;
14+
15+
const pdfContent = await context.run("Process PDF Attachment", async () => {
16+
if (!attachment) {
17+
return '';
18+
}
19+
20+
// Download file
21+
const response = await fetch(attachment);
22+
const fileContent = await response.arrayBuffer();
23+
const buffer = Buffer.from(fileContent);
24+
25+
// Parse PDF
26+
try {
27+
const data = await pdf(buffer);
28+
console.log(data)
29+
return data.text;
30+
} catch (error) {
31+
console.error('Error parsing PDF:', error);
32+
return 'Unable to extract PDF content';
33+
}
34+
});
35+
36+
const aiResponse = await context.api.openai.call("get ai response", {
37+
token: process.env.OPENAI_API_KEY!,
38+
operation: "chat.completions.create",
39+
body: {
40+
model: "o1",
41+
messages: [
42+
{
43+
role: "system",
44+
content: `You are an AI assistant that writes email responses. Write a natural, professional response
45+
that continues the email thread. The response should be concise but helpful, maintaining
46+
the flow of the conversation.`
47+
},
48+
{
49+
role: "user",
50+
content: `
51+
Here's the email thread context. Please write a response to this email thread that addresses the latest message:
52+
${message}.
53+
54+
Here's the pdf attachment, if exists:
55+
${pdfContent}
56+
`,
57+
}
58+
],
59+
},
60+
})
61+
62+
await context.api.resend.call("Send LLM Proposal", {
63+
token: process.env.RESEND_API_KEY!,
64+
body: {
65+
from: "Acme <[email protected]>",
66+
to,
67+
subject,
68+
text: aiResponse.body.choices[0].message.content
69+
}
70+
})
71+
})
25.3 KB
Binary file not shown.
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
:root {
6+
--background: #ffffff;
7+
--foreground: #171717;
8+
}
9+
10+
@media (prefers-color-scheme: dark) {
11+
:root {
12+
--background: #0a0a0a;
13+
--foreground: #ededed;
14+
}
15+
}
16+
17+
body {
18+
color: var(--foreground);
19+
background: var(--background);
20+
font-family: Arial, Helvetica, sans-serif;
21+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Metadata } from "next";
2+
import { Geist, Geist_Mono } from "next/font/google";
3+
import "./globals.css";
4+
5+
const geistSans = Geist({
6+
variable: "--font-geist-sans",
7+
subsets: ["latin"],
8+
});
9+
10+
const geistMono = Geist_Mono({
11+
variable: "--font-geist-mono",
12+
subsets: ["latin"],
13+
});
14+
15+
export const metadata: Metadata = {
16+
title: "Create Next App",
17+
description: "Generated by create next app",
18+
};
19+
20+
export default function RootLayout({
21+
children,
22+
}: Readonly<{
23+
children: React.ReactNode;
24+
}>) {
25+
return (
26+
<html lang="en">
27+
<body
28+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
29+
>
30+
{children}
31+
</body>
32+
</html>
33+
);
34+
}
+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import Image from "next/image";
2+
3+
export default function Home() {
4+
return (
5+
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
6+
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
7+
<Image
8+
className="dark:invert"
9+
src="/next.svg"
10+
alt="Next.js logo"
11+
width={180}
12+
height={38}
13+
priority
14+
/>
15+
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
16+
<li className="mb-2">
17+
Get started by editing{" "}
18+
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
19+
app/page.tsx
20+
</code>
21+
.
22+
</li>
23+
<li>Save and see your changes instantly.</li>
24+
</ol>
25+
26+
<div className="flex gap-4 items-center flex-col sm:flex-row">
27+
<a
28+
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
29+
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
30+
target="_blank"
31+
rel="noopener noreferrer"
32+
>
33+
<Image
34+
className="dark:invert"
35+
src="/vercel.svg"
36+
alt="Vercel logomark"
37+
width={20}
38+
height={20}
39+
/>
40+
Deploy now
41+
</a>
42+
<a
43+
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
44+
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
45+
target="_blank"
46+
rel="noopener noreferrer"
47+
>
48+
Read our docs
49+
</a>
50+
</div>
51+
</main>
52+
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
53+
<a
54+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
55+
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
56+
target="_blank"
57+
rel="noopener noreferrer"
58+
>
59+
<Image
60+
aria-hidden
61+
src="/file.svg"
62+
alt="File icon"
63+
width={16}
64+
height={16}
65+
/>
66+
Learn
67+
</a>
68+
<a
69+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
70+
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
71+
target="_blank"
72+
rel="noopener noreferrer"
73+
>
74+
<Image
75+
aria-hidden
76+
src="/window.svg"
77+
alt="Window icon"
78+
width={16}
79+
height={16}
80+
/>
81+
Examples
82+
</a>
83+
<a
84+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
85+
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
86+
target="_blank"
87+
rel="noopener noreferrer"
88+
>
89+
<Image
90+
aria-hidden
91+
src="/globe.svg"
92+
alt="Globe icon"
93+
width={16}
94+
height={16}
95+
/>
96+
Go to nextjs.org →
97+
</a>
98+
</footer>
99+
</div>
100+
);
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { dirname } from "path";
2+
import { fileURLToPath } from "url";
3+
import { FlatCompat } from "@eslint/eslintrc";
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = dirname(__filename);
7+
8+
const compat = new FlatCompat({
9+
baseDirectory: __dirname,
10+
});
11+
12+
const eslintConfig = [
13+
...compat.extends("next/core-web-vitals", "next/typescript"),
14+
];
15+
16+
export default eslintConfig;
24.4 KB
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { NextConfig } from "next";
2+
3+
const nextConfig: NextConfig = {
4+
serverExternalPackages: ["pdf-parse"]
5+
};
6+
7+
export default nextConfig;

0 commit comments

Comments
 (0)