Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export function Rules({ size = "md" }: { size?: "sm" | "md" }) {
from: null,
subject: null,
body: null,
promptText: null,
};
return [...(baseRules || []), coldEmailBlockerRule];
}, [baseRules, emailAccountData, emailAccountId]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- AlterTable
ALTER TABLE "Rule" ADD COLUMN "promptText" TEXT;

-- CreateTable
CREATE TABLE "RuleHistory" (
"id" TEXT NOT NULL,
Comment thread
elie222 marked this conversation as resolved.
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
Comment thread
elie222 marked this conversation as resolved.
"ruleId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"triggerType" TEXT NOT NULL,
"promptText" TEXT,
"name" TEXT NOT NULL,
"instructions" TEXT,
"enabled" BOOLEAN NOT NULL,
"automate" BOOLEAN NOT NULL,
"runOnThreads" BOOLEAN NOT NULL,
"conditionalOperator" TEXT NOT NULL,
"from" TEXT,
"to" TEXT,
"subject" TEXT,
"body" TEXT,
"categoryFilterType" TEXT,
"systemType" TEXT,
"actions" JSONB NOT NULL,
"categoryFilters" JSONB,

CONSTRAINT "RuleHistory_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "RuleHistory_ruleId_createdAt_idx" ON "RuleHistory"("ruleId", "createdAt");

-- CreateIndex
CREATE UNIQUE INDEX "RuleHistory_ruleId_version_key" ON "RuleHistory"("ruleId", "version");

-- AddForeignKey
ALTER TABLE "RuleHistory" ADD CONSTRAINT "RuleHistory_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "Rule"("id") ON DELETE CASCADE ON UPDATE CASCADE;
33 changes: 33 additions & 0 deletions apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ model Rule {

systemType SystemType?

promptText String? // natural language for this rule for prompt file. prompt file is combination of these fields

history RuleHistory[]

@@unique([name, emailAccountId])
@@unique([emailAccountId, systemType])
}
Expand All @@ -272,6 +276,35 @@ model Action {
url String?
}

model RuleHistory {
id String @id @default(cuid())
createdAt DateTime @default(now())
ruleId String
rule Rule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
version Int
triggerType String // "ai_update" (AI), "manual_update" (user), "ai_creation" (AI), "manual_creation" (user), "system_creation" (system), "system_update" (system)
promptText String? // The prompt text that generated this version

name String
instructions String?
enabled Boolean
automate Boolean
runOnThreads Boolean
conditionalOperator String
from String?
to String?
subject String?
body String?
categoryFilterType String?
systemType String?

actions Json
categoryFilters Json?

@@unique([ruleId, version])
@@index([ruleId, createdAt])
}

// Rule/Action models represent the rules and actions that the AI can take.
// ExecutedRule/ExecutedAction models represent the rules/actions that have been planned or executed by the AI.
model ExecutedRule {
Expand Down
14 changes: 14 additions & 0 deletions apps/web/utils/actions/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { actionClient } from "@/utils/actions/safe-action";
import { getGmailClientForEmail } from "@/utils/account";
import { getEmailAccountWithAi } from "@/utils/user/get";
import { prefixPath } from "@/utils/path";
import { createRuleHistory } from "@/utils/rule/rule-history";

const logger = createScopedLogger("actions/rule");

Expand Down Expand Up @@ -110,6 +111,11 @@ export const createRuleAction = actionClient
include: { actions: true, categoryFilters: true, group: true },
});

// Track rule creation in history
after(() =>
createRuleHistory({ rule, triggerType: "manual_creation" }),
);

after(() => updatePromptFileOnRuleCreated({ emailAccountId, rule }));

return { rule };
Expand Down Expand Up @@ -237,6 +243,14 @@ export const updateRuleAction = actionClient
: []),
]);

// Track rule update in history
after(() =>
createRuleHistory({
rule: updatedRule,
triggerType: "manual_update",
}),
);

// update prompt file
after(() =>
updatePromptFileOnRuleUpdated({
Expand Down
1 change: 1 addition & 0 deletions apps/web/utils/reply-tracker/enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export async function enableReplyTracker({
},
emailAccountId: emailAccount.id,
systemType: SystemType.TO_REPLY,
triggerType: "system_creation",
});

if (newRule && "error" in newRule) {
Expand Down
74 changes: 74 additions & 0 deletions apps/web/utils/rule/rule-history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import prisma from "@/utils/prisma";
import type { RuleWithRelations } from "@/utils/ai/rule/create-prompt-from-rule";

export type RuleHistoryTrigger =
| "ai_update" // AI updates existing rule from prompt changes
| "manual_update" // User manually edits existing rule
| "ai_creation" // AI creates rule from parsing prompts
| "manual_creation" // User manually creates new rule
| "system_creation" // System automatically creates rule (e.g., reply tracker)
| "system_update"; // System automatically updates rule

/**
* Creates a complete snapshot of a rule in the RuleHistory table
*/
export async function createRuleHistory({
rule,
triggerType,
}: {
rule: RuleWithRelations;
triggerType: RuleHistoryTrigger;
}) {
// Get the current version number for this rule
const lastHistory = await prisma.ruleHistory.findFirst({
where: { ruleId: rule.id },
orderBy: { version: "desc" },
select: { version: true },
});

const nextVersion = (lastHistory?.version ?? 0) + 1;
Comment thread
elie222 marked this conversation as resolved.

// Serialize actions to JSON
const actionsSnapshot = rule.actions.map((action) => ({
id: action.id,
type: action.type,
label: action.label,
subject: action.subject,
content: action.content,
to: action.to,
cc: action.cc,
bcc: action.bcc,
url: action.url,
}));

// Serialize category filters to JSON
const categoryFiltersSnapshot = rule.categoryFilters?.map((category) => ({
id: category.id,
name: category.name,
description: category.description,
}));

return prisma.ruleHistory.create({
data: {
ruleId: rule.id,
name: rule.name,
instructions: rule.instructions,
enabled: rule.enabled,
automate: rule.automate,
runOnThreads: rule.runOnThreads,
conditionalOperator: rule.conditionalOperator,
from: rule.from,
to: rule.to,
subject: rule.subject,
body: rule.body,
categoryFilterType: rule.categoryFilterType,
systemType: rule.systemType,
promptText: rule.promptText,
actions: actionsSnapshot,
categoryFilters: categoryFiltersSnapshot,
triggerType,
// Note: this is unique and can fail in race conditions. Not a big deal for now.
version: nextVersion,
},
});
}
28 changes: 26 additions & 2 deletions apps/web/utils/rule/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getUserCategoriesForNames } from "@/utils/category.server";
import { getActionRiskLevel, type RiskAction } from "@/utils/risk";
import { hasExampleParams } from "@/app/(app)/[emailAccountId]/assistant/examples";
import { SafeError } from "@/utils/error";
import { createRuleHistory } from "@/utils/rule/rule-history";

const logger = createScopedLogger("rule");

Expand All @@ -33,11 +34,13 @@ export async function safeCreateRule({
emailAccountId,
categoryNames,
systemType,
triggerType = "ai_creation",
}: {
result: CreateOrUpdateRuleSchemaWithCategories;
emailAccountId: string;
categoryNames?: string[] | null;
systemType?: SystemType | null;
triggerType?: "ai_creation" | "manual_creation" | "system_creation";
}) {
const categoryIds = await getUserCategoriesForNames({
emailAccountId,
Expand All @@ -50,6 +53,7 @@ export async function safeCreateRule({
emailAccountId,
categoryIds,
systemType,
triggerType,
});
return rule;
} catch (error) {
Expand All @@ -59,6 +63,7 @@ export async function safeCreateRule({
result: { ...result, name: `${result.name} - ${Date.now()}` },
emailAccountId,
categoryIds,
triggerType,
});
return rule;
}
Expand All @@ -79,18 +84,21 @@ export async function safeUpdateRule({
result,
emailAccountId,
categoryIds,
triggerType = "ai_update",
}: {
ruleId: string;
result: CreateOrUpdateRuleSchemaWithCategories;
emailAccountId: string;
categoryIds?: string[] | null;
triggerType?: "ai_update" | "manual_update" | "system_update";
}) {
try {
const rule = await updateRule({
ruleId,
result,
emailAccountId,
categoryIds,
triggerType,
});
return { id: rule.id };
} catch (error) {
Expand All @@ -100,6 +108,7 @@ export async function safeUpdateRule({
result: { ...result, name: `${result.name} - ${Date.now()}` },
emailAccountId,
categoryIds,
triggerType: "ai_creation", // Default for safeUpdateRule fallback
});
return { id: rule.id };
}
Expand All @@ -121,15 +130,17 @@ export async function createRule({
emailAccountId,
categoryIds,
systemType,
triggerType = "ai_creation",
}: {
result: CreateOrUpdateRuleSchemaWithCategories;
emailAccountId: string;
categoryIds?: string[] | null;
systemType?: SystemType | null;
triggerType?: "ai_creation" | "manual_creation" | "system_creation";
}) {
const mappedActions = mapActionFields(result.actions);

return prisma.rule.create({
const rule = await prisma.rule.create({
data: {
name: result.name,
emailAccountId,
Expand Down Expand Up @@ -163,20 +174,27 @@ export async function createRule({
},
include: { actions: true, categoryFilters: true, group: true },
});

// Track rule creation in history
await createRuleHistory({ rule, triggerType });

return rule;
}

async function updateRule({
ruleId,
result,
emailAccountId,
categoryIds,
triggerType = "ai_update",
}: {
ruleId: string;
result: CreateOrUpdateRuleSchemaWithCategories;
emailAccountId: string;
categoryIds?: string[] | null;
triggerType?: "ai_update" | "manual_update" | "system_update";
}) {
return prisma.rule.update({
const rule = await prisma.rule.update({
where: { id: ruleId },
data: {
name: result.name,
Expand All @@ -201,7 +219,13 @@ async function updateRule({
}
: undefined,
},
include: { actions: true, categoryFilters: true, group: true },
});

// Track rule update in history
await createRuleHistory({ rule, triggerType });

return rule;
}

export async function updateRuleActions({
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.3.29
v1.4.0