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
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
"jotai": "2.13.1",
"jsdom": "26.1.0",
"json5": "2.2.3",
"jsonrepair": "3.13.0",
"linkify-react": "4.3.2",
"linkifyjs": "4.3.2",
"lodash": "4.17.21",
Expand Down Expand Up @@ -177,7 +178,7 @@
"cross-env": "7.0.3",
"dotenv": "17.2.1",
"postcss": "8.5.6",
"prisma": "6.15.0",
"prisma": "6.6.0",
"serwist": "9.2.0",
"tailwindcss": "3.4.17",
"tsconfig": "workspace:*",
Expand Down
9 changes: 8 additions & 1 deletion apps/web/utils/ai/choose-rule/ai-choose-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,14 @@ ${
</user_info>`
}

Respond with a valid JSON object.`;
Respond with a valid JSON object:

Example response format:
{
"reason": "This email is a newsletter subscription",
"ruleName": "Newsletter",
"noMatchFound": false
}`;

const prompt = `Select a rule to apply to this email that was sent to me:

Expand Down
156 changes: 83 additions & 73 deletions apps/web/utils/ai/rule/prompt-to-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,39 +82,43 @@ You can use multiple conditions in a rule, but aim for simplicity.
In most cases, you should use the "aiInstructions" and sometimes you will use other fields in addition.
If a rule can be handled fully with static conditions, do so, but this is rarely possible.

IMPORTANT: You must return a JSON object.

<examples>
<example>
<input>
When I get a newsletter, archive it and label it as "Newsletter"
</input>
<output>
[{
"name": "Label Newsletters",
"condition": {
"aiInstructions": "Apply this rule to newsletters"
${
hasSmartCategories
? `,
"categories": {
"categoryFilterType": "INCLUDE",
"categoryFilters": ["Newsletters"]
},
"conditionalOperator": "OR"`
: ""
}
},
"actions": [
{
"type": "ARCHIVE"
{
"rules": [{
"name": "Label Newsletters",
"condition": {
"aiInstructions": "Apply this rule to newsletters"
${
hasSmartCategories
? `,
"categories": {
"categoryFilterType": "INCLUDE",
"categoryFilters": ["Newsletters"]
},
"conditionalOperator": "OR"`
: ""
}
},
{
"type": "LABEL",
"fields": {
"label": "Newsletter"
"actions": [
{
"type": "ARCHIVE"
},
{
"type": "LABEL",
"fields": {
"label": "Newsletter"
}
}
}
]
}]
]
}]
}
</output>
</example>

Expand All @@ -123,26 +127,28 @@ If a rule can be handled fully with static conditions, do so, but this is rarely
When someone mentions system outages or critical issues, forward to urgent-support@company.com and label as Urgent-Support
</input>
<output>
[{
"name": "Forward Urgent Emails",
"condition": {
"aiInstructions": "Apply this rule to emails mentioning system outages or critical issues"
},
"actions": [
{
"type": "FORWARD",
"fields": {
"to": "urgent-support@company.com"
}
{
"rules": [{
"name": "Forward Urgent Emails",
"condition": {
"aiInstructions": "Apply this rule to emails mentioning system outages or critical issues"
},
{
"type": "LABEL",
"fields": {
"label": "Urgent-Support"
"actions": [
{
"type": "FORWARD",
"fields": {
"to": "urgent-support@company.com"
}
},
{
"type": "LABEL",
"fields": {
"label": "Urgent-Support"
}
}
}
]
}]
]
}]
}
</output>
</example>

Expand All @@ -151,24 +157,26 @@ If a rule can be handled fully with static conditions, do so, but this is rarely
Label all urgent emails from company.com as "Urgent"
</input>
<output>
[{
"name": "Matt Urgent Emails",
"condition": {
"conditionalOperator": "AND",
"aiInstructions": "Apply this rule to urgent emails",
"static": {
"from": "@company.com"
}
},
"actions": [
{
"type": "LABEL",
"fields": {
"label": "Urgent"
{
"rules": [{
"name": "Matt Urgent Emails",
"condition": {
"conditionalOperator": "AND",
"aiInstructions": "Apply this rule to urgent emails",
"static": {
"from": "@company.com"
}
},
"actions": [
{
"type": "LABEL",
"fields": {
"label": "Urgent"
}
}
}
]
}]
]
}]
}
</output>
</example>

Expand All @@ -184,20 +192,22 @@ If a rule can be handled fully with static conditions, do so, but this is rarely
"""
</input>
<output>
[{
"name": "Reply to Call Requests",
"condition": {
"aiInstructions": "Apply this rule to emails from people asking to set up a call"
},
"actions": [
{
"type": "REPLY",
"fields": {
"content": "Hi {{name}},\nThank you for your message.\nI'll respond within 2 hours.\nBest,\nAlice"
{
"rules": [{
"name": "Reply to Call Requests",
"condition": {
"aiInstructions": "Apply this rule to emails from people asking to set up a call"
},
"actions": [
{
"type": "REPLY",
"fields": {
"content": "Hi {{name}},\nThank you for your message.\nI'll respond within 2 hours.\nBest,\nAlice"
}
}
}
]
}]
]
}]
}
</output>
</example>
</examples>
Expand Down
14 changes: 14 additions & 0 deletions apps/web/utils/llms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type StreamTextOnFinishCallback,
type StreamTextOnStepFinishCallback,
} from "ai";
import { jsonrepair } from "jsonrepair";
import type { LanguageModelV2 } from "@ai-sdk/provider";
import { saveAiUsage } from "@/utils/usage";
import type { UserAIFields } from "@/utils/llms/types";
Expand Down Expand Up @@ -134,8 +135,21 @@ export function createGenerateObject({
prompt: options.prompt?.slice(0, MAX_LOG_LENGTH),
});

if (
!options.system?.includes("JSON") &&
typeof options.prompt === "string" &&
!options.prompt?.includes("JSON")
) {
logger.warn("Missing JSON in prompt", { label });
}

const result = await generateObject(
{
experimental_repairText: async ({ text }) => {
logger.info("Repairing text", { label });
const fixed = jsonrepair(text);
return fixed;
},
...options,
...commonOptions,
},
Expand Down
Loading
Loading