diff --git a/.gitignore b/.gitignore index 35022420..6c866cd3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ examples/*/pnpm-lock.yaml examples/ngrok.log restart.sh -bootstrap.sh \ No newline at end of file +bootstrap.sh +.env*.local diff --git a/examples/email-analyzer-o1/app/actions.ts b/examples/email-analyzer-o1/app/actions.ts new file mode 100644 index 00000000..656bb502 --- /dev/null +++ b/examples/email-analyzer-o1/app/actions.ts @@ -0,0 +1,63 @@ +"use server" + +import { Client as WorkflowClient } from '@upstash/workflow'; + +type EmailPayload = { + message: string; + subject: string; + to: string; + attachment?: string; +} + +function getWorkflowClient(): WorkflowClient { + const token = process.env.QSTASH_TOKEN; + + if (!token) { + throw new Error( + 'QSTASH_TOKEN environment variable is required' + ); + } + + return new WorkflowClient({ + token, + // VERCEL AUTOMATION BYPASS SECRET is used to bypass the verification of the request + headers: process.env.VERCEL_AUTOMATION_BYPASS_SECRET + ? { + 'Upstash-Forward-X-Vercel-Protection-Bypass': + process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + 'x-vercel-protection-bypass': + process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + } + : undefined, + }); +} + +export async function triggerEmailAnalysis(formData: EmailPayload) { + try { + const workflowClient = getWorkflowClient() + const result = await workflowClient.trigger({ + url: `${process.env.UPSTASH_WORKFLOW_URL ?? process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000'}/api/analyze`, + body: formData, + headers: process.env.VERCEL_AUTOMATION_BYPASS_SECRET + ? { + 'Upstash-Forward-X-Vercel-Protection-Bypass': + process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + 'X-Vercel-Protection-Bypass': + process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + "upstash-callback-forward-X-Vercel-Protection-Bypass": + process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + "upstash-failure-callback-forward-X-Vercel-Protection-Bypass": + process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + } + : undefined, + }); + + return { success: true, workflowRunId: result.workflowRunId }; + } catch (error) { + console.error('Error triggering workflow:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Failed to trigger workflow' + }; + } +} diff --git a/examples/email-analyzer-o1/app/api/analyze/route.ts b/examples/email-analyzer-o1/app/api/analyze/route.ts index d5116f8e..3debf942 100644 --- a/examples/email-analyzer-o1/app/api/analyze/route.ts +++ b/examples/email-analyzer-o1/app/api/analyze/route.ts @@ -1,71 +1,103 @@ -import { serve } from "@upstash/workflow/nextjs" -import pdf from "pdf-parse" - +import { serve } from "@upstash/workflow/nextjs"; +import pdf from "pdf-parse"; +import { Client as QStashClient } from "@upstash/qstash"; type EmailPayload = { - message: string, - subject: string, - to: string - attachment?: string, + message: string; + subject: string; + to: string; + attachment?: string; +}; + +function getQStashClient(): QStashClient { + const token = process.env.QSTASH_TOKEN; + + if (!token) { + throw new Error("QSTASH_TOKEN environment variable is required"); + } + + return new QStashClient({ + token, + headers: process.env.VERCEL_AUTOMATION_BYPASS_SECRET + ? { + "Upstash-Forward-X-Vercel-Protection-Bypass": process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + "x-vercel-protection-bypass": process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + "upstash-callback-forward-X-Vercel-Protection-Bypass": + process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + "upstash-failure-callback-forward-X-Vercel-Protection-Bypass": + process.env.VERCEL_AUTOMATION_BYPASS_SECRET, + } + : undefined, + }); } -export const { POST } = serve(async (context) => { - const { message, subject, to, attachment } = context.requestPayload; +export const { POST } = serve( + async (context) => { + const { message, subject, to, attachment } = context.requestPayload; + + const somethingThatWorks = await context.run("somethingThatWorks", async () => { + return "somethingThatWorks"; + }); - const pdfContent = await context.run("Process PDF Attachment", async () => { - if (!attachment) { - return ''; - } + const pdfContent = await context.run("Process PDF Attachment", async () => { + if (!attachment) { + return ""; + } + console.log(somethingThatWorks); - // Download file - const response = await fetch(attachment); - const fileContent = await response.arrayBuffer(); - const buffer = Buffer.from(fileContent); + // Download file + const response = await fetch(attachment); + const fileContent = await response.arrayBuffer(); + const buffer = Buffer.from(fileContent); - // Parse PDF - try { - const data = await pdf(buffer); - console.log(data) - return data.text; - } catch (error) { - console.error('Error parsing PDF:', error); - return 'Unable to extract PDF content'; - } - }); + // Parse PDF + try { + const data = await pdf(buffer); + console.log(data); + return data.text; + } catch (error) { + console.error("Error parsing PDF:", error); + return "Unable to extract PDF content"; + } + }); - const aiResponse = await context.api.openai.call("get ai response", { - token: process.env.OPENAI_API_KEY!, - operation: "chat.completions.create", - body: { - model: "o1", - messages: [ - { - role: "system", - content: `You are an AI assistant that writes email responses. Write a natural, professional response + const aiResponse = await context.api.openai.call("get ai response", { + token: process.env.OPENAI_API_KEY!, + operation: "chat.completions.create", + body: { + model: "o1", + messages: [ + { + role: "system", + content: `You are an AI assistant that writes email responses. Write a natural, professional response that continues the email thread. The response should be concise but helpful, maintaining - the flow of the conversation.` - }, - { - role: "user", - content: ` + the flow of the conversation.`, + }, + { + role: "user", + content: ` Here's the email thread context. Please write a response to this email thread that addresses the latest message: ${message}. Here's the pdf attachment, if exists: ${pdfContent} `, - } - ], - }, - }) + }, + ], + }, + }); - await context.api.resend.call("Send LLM Proposal", { - token: process.env.RESEND_API_KEY!, - body: { - from: "Acme ", - to, - subject, - text: aiResponse.body.choices[0].message.content - } - }) -}) + await context.api.resend.call("Send LLM Proposal", { + token: process.env.RESEND_API_KEY!, + body: { + from: "Acme ", + to, + subject, + text: aiResponse.body.choices[0].message.content, + }, + }); + }, + { + qstashClient: getQStashClient(), + } +); diff --git a/examples/email-analyzer-o1/app/page.tsx b/examples/email-analyzer-o1/app/page.tsx index 9007252c..fe7613ab 100644 --- a/examples/email-analyzer-o1/app/page.tsx +++ b/examples/email-analyzer-o1/app/page.tsx @@ -1,101 +1,122 @@ -import Image from "next/image"; +"use client" + +import { useState } from "react"; +import { triggerEmailAnalysis } from "./actions"; export default function Home() { + const [formData, setFormData] = useState({ + to: "", + subject: "", + message: "", + attachment: "", + }); + const [loading, setLoading] = useState(false); + const [result, setResult] = useState<{ success: boolean; workflowRunId?: string; error?: string } | null>(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setResult(null); + + const payload = { + to: formData.to, + subject: formData.subject, + message: formData.message, + attachment: formData.attachment || undefined, + }; + + const response = await triggerEmailAnalysis(payload); + setResult(response); + setLoading(false); + }; + return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. Save and see your changes instantly.
  4. -
+
+
+

Email Analyzer Test

-
- - Vercel logomark +
+ + setFormData({ ...formData, to: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black" + required + /> +
+ +
+ + setFormData({ ...formData, subject: e.target.value })} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-black" + required + /> +
+ +
+ +