Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/emails/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export { default as OTPEmail } from "./templates/email-otp";
export { default as WebhookAddedEmail } from "./templates/webhook-added";
export { default as WebhookFailedEmail } from "./templates/webhook-failed";
export { default as WebhookDisabledEmail } from "./templates/webhook-disabled";
export { default as DataReadyEmail } from "./templates/data-ready-email";
export { default as DataProcessingFailedEmail } from "./templates/data-processing-failed-email";
68 changes: 68 additions & 0 deletions packages/emails/src/templates/data-processing-failed-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Heading, Section, Text } from "@react-email/components";

import { Button } from "../components/button";
import { DefaultLayout } from "../components/default-layout";

export function DataProcessingFailedEmail({
email = "john@doe.com",
namespace = {
name: "My Namespace",
slug: "my-namespace",
},
organization = {
name: "Acme, Inc",
slug: "acme",
},
error = "Failed to process documents",
domain = "https://app.agentset.ai",
}: {
email: string;
namespace: {
name: string;
slug: string;
};
organization: {
name: string;
slug: string;
};
error: string;
domain?: string;
}) {
return (
<DefaultLayout
preview="There was an issue processing your data"
footer={{ email, domain }}
>
<Heading className="mx-0 my-7 p-0 text-xl font-medium text-black">
There was an issue processing your data
</Heading>

<Text className="text-sm leading-6 text-black">
<strong>{namespace.name}</strong> on{" "}
<a href="https://agentset.ai" className="text-black underline">
agentset.ai
</a>{" "}
ran into an issue while processing your data.
</Text>

<Text className="text-sm leading-6 text-black">
<strong>Error:</strong> {error}
</Text>

<Text className="text-sm leading-6 text-black">
Head over to your documents page to review the details and retry if
needed.
</Text>

<Section className="my-6">
<Button
href={`${domain}/${organization.slug}/${namespace.slug}/documents`}
>
View Documents
</Button>
</Section>
</DefaultLayout>
);
}

export default DataProcessingFailedEmail;
76 changes: 76 additions & 0 deletions packages/emails/src/templates/data-ready-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Heading, Section, Text } from "@react-email/components";

import { Button } from "../components/button";
import { DefaultLayout } from "../components/default-layout";

export function DataReadyEmail({
email = "john@doe.com",
namespace = {
name: "My Namespace",
slug: "my-namespace",
},
organization = {
name: "Acme, Inc",
slug: "acme",
},
domain = "https://app.agentset.ai",
}: {
email: string;
namespace: {
name: string;
slug: string;
};
organization: {
name: string;
slug: string;
Comment on lines +22 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

organization.name is accepted but never used

The organization prop type declares both name and slug, but name is never rendered anywhere in the template. Only organization.slug is used to construct the playground URL. Carrying an unused field through the type signature can mislead future maintainers and requires callers to supply data unnecessarily.

If there are no plans to display the organization name in this email, consider narrowing the type:

Suggested change
};
organization: {
name: string;
slug: string;
organization: {
slug: string;
};

Context Used: Rule from dashboard - Guidelines for writing clean, maintainable, and human-readable code. Apply these rules when writing ... (source)

};
domain?: string;
}) {
return (
<DefaultLayout preview="Your data is ready!" footer={{ email, domain }}>
<Heading className="mx-0 my-7 p-0 text-xl font-medium text-black">
Your data is ready!
</Heading>

<Text className="text-sm leading-6 text-black">
<strong>{namespace.name}</strong> on{" "}
<a href="https://agentset.ai" className="text-black underline">
agentset.ai
</a>{" "}
just finished building your data.
</Text>

<Text className="text-sm leading-6 text-black">
Jump in and start chatting. Tell your namespace what you want to find,
and get answers from your documents.
</Text>

<Text className="text-sm leading-6 text-black">
A few tips to get you started:
</Text>

<Text className="ml-1 text-sm leading-4 text-black">
◆ Start small. Try one simple question first to see how it works.
</Text>

<Text className="ml-1 text-sm leading-4 text-black">
◆ Be specific. &quot;What are the key findings?&quot; beats &quot;tell
me stuff.&quot;
</Text>

<Text className="ml-1 text-sm leading-4 text-black">
◆ Refine as you go. Follow up to dig deeper into any answer.
</Text>

<Section className="my-6">
<Button
href={`${domain}/${organization.slug}/${namespace.slug}/playground`}
>
Open Playground
</Button>
</Section>
</DefaultLayout>
);
}

export default DataReadyEmail;
69 changes: 69 additions & 0 deletions packages/jobs/src/tasks/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ import {
emitIngestJobWebhook,
} from "../webhook";
import { processDocument } from "./process-document";
import {
DataProcessingFailedEmail,
DataReadyEmail,
sendEmail,
} from "@agentset/emails";
import { getOrganizationOwner } from "@agentset/webhooks/server";

const BATCH_SIZE = 30;

Expand Down Expand Up @@ -95,6 +101,8 @@ export const ingestJob = schemaTask({
namespace: {
select: {
id: true,
slug: true,
name: true,
embeddingConfig: true,
vectorStoreConfig: true,
organization: {
Expand Down Expand Up @@ -599,6 +607,67 @@ export const ingestJob = schemaTask({
},
});

// Send notification email when all processing in the namespace is done
try {
const activeJobCount = await db.ingestJob.count({
where: {
namespaceId: ingestionJob.namespace.id,
id: { not: ingestionJob.id },
status: {
in: [
IngestJobStatus.BACKLOG,
IngestJobStatus.QUEUED,
IngestJobStatus.QUEUED_FOR_RESYNC,
IngestJobStatus.PRE_PROCESSING,
IngestJobStatus.PROCESSING,
],
},
},
});

if (activeJobCount === 0) {
const owner = await getOrganizationOwner({
db,
organizationId: ingestionJob.namespace.organization.id,
});

if (owner) {
if (!jobFailed) {
await sendEmail({
email: owner.email,
subject: "Your data is ready!",
react: DataReadyEmail({
email: owner.email,
namespace: {
name: ingestionJob.namespace.name,
slug: ingestionJob.namespace.slug,
},
organization: owner.organization,
}),
variant: "notifications",
});
} else {
await sendEmail({
email: owner.email,
subject: "There was an issue processing your data",
react: DataProcessingFailedEmail({
email: owner.email,
namespace: {
name: ingestionJob.namespace.name,
slug: ingestionJob.namespace.slug,
},
organization: owner.organization,
error: jobError || "Failed to process documents",
}),
variant: "notifications",
});
}
}
}
} catch {
// Email notification is best-effort — don't fail the ingest job
}

return {
ingestionJobId: ingestionJob.id,
documentsCreated: documentsIds.length,
Expand Down