diff --git a/apps/web/__tests__/ai-diff-rules.test.ts b/apps/web/__tests__/ai-diff-rules.test.ts index 403afe0aa8..c137168abe 100644 --- a/apps/web/__tests__/ai-diff-rules.test.ts +++ b/apps/web/__tests__/ai-diff-rules.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest"; import { aiDiffRules } from "@/utils/ai/rule/diff-rules"; import { getEmailAccount } from "@/__tests__/helpers"; -// pnpm test-ai ai-diff-rules +// RUN_AI_TESTS=true pnpm test-ai ai-diff-rules const isAiTest = process.env.RUN_AI_TESTS === "true"; @@ -33,13 +33,14 @@ describe.runIf(isAiTest)("aiDiffRules", () => { }); expect(result).toEqual({ - addedRules: ['Label all emails from support@company.com as "Support"'], + addedRules: ['* Label all emails from support@company.com as "Support"'], editedRules: [ { - oldRule: `Archive all newsletters and label them "Newsletter"`, - newRule: `Archive all newsletters and label them "Newsletter Updates"`, + oldRule: `* Archive all newsletters and label them "Newsletter"`, + newRule: `* Archive all newsletters and label them "Newsletter Updates"`, }, ], + removedRules: [`* Label receipts as "Receipt"`], }); }, 15_000); diff --git a/apps/web/package.json b/apps/web/package.json index 9a9ef90d1a..f3b9876d0d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -40,7 +40,7 @@ "@mux/mux-player-react": "3.4.0", "@next/mdx": "15.3.3", "@next/third-parties": "15.3.3", - "@openrouter/ai-sdk-provider": "1.0.0-beta.6", + "@openrouter/ai-sdk-provider": "1.1.0", "@portabletext/react": "3.2.1", "@prisma/client": "6.6.0", "@radix-ui/react-alert-dialog": "1.1.14", diff --git a/apps/web/utils/ai/rule/diff-rules.ts b/apps/web/utils/ai/rule/diff-rules.ts index bd99e1b40d..d0a8172f6b 100644 --- a/apps/web/utils/ai/rule/diff-rules.ts +++ b/apps/web/utils/ai/rule/diff-rules.ts @@ -2,20 +2,7 @@ import z from "zod"; import { createPatch } from "diff"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import { getModel } from "@/utils/llms/model"; -import { createGenerateText } from "@/utils/llms"; - -const inputSchema = z.object({ - addedRules: z.array(z.string()).describe("The added rules"), - editedRules: z - .array( - z.object({ - oldRule: z.string().describe("The old rule"), - newRule: z.string().describe("The new rule"), - }), - ) - .describe("The edited rules"), - removedRules: z.array(z.string()).describe("The removed rules"), -}); +import { createGenerateObject } from "@/utils/llms"; export async function aiDiffRules({ emailAccount, @@ -53,11 +40,26 @@ Organize your response using the 'diff_rules' function. IMPORTANT: Do not include a rule in more than one category. If a rule is edited, do not include it in the 'removedRules' category! If a rule is edited, it is an edit and not a removal! Be extra careful to not make this mistake. + +Return the result in JSON format. Do not include any other text in your response. + + +{ + "addedRules": ["rule text1", "rule text2"], + "editedRules": [ + { + "oldRule": "rule text3", + "newRule": "rule text4 updated" + }, + ], + "removedRules": ["rule text5", "rule text6"] +} + `; const modelOptions = getModel(emailAccount.user, "chat"); - const generateObject = createGenerateText({ + const generateObject = createGenerateObject({ userEmail: emailAccount.email, label: "Diff rules", modelOptions, @@ -67,17 +69,22 @@ If a rule is edited, it is an edit and not a removal! Be extra careful to not ma ...modelOptions, system, prompt, - tools: { - diff_rules: { - description: - "Analyze two prompt files and their diff to return the differences", - inputSchema, - }, - }, + schemaName: "diff_rules", + schemaDescription: + "The result of the diff rules analysis. Return the result in JSON format. Do not include any other text in your response.", + schema: z.object({ + addedRules: z.array(z.string()).describe("The added rules"), + editedRules: z + .array( + z.object({ + oldRule: z.string().describe("The old rule"), + newRule: z.string().describe("The new rule"), + }), + ) + .describe("The edited rules"), + removedRules: z.array(z.string()).describe("The removed rules"), + }), }); - const parsedRules = result.toolCalls?.[0]?.input as z.infer< - typeof inputSchema - >; - return parsedRules; + return result.object; } diff --git a/apps/web/utils/llms/index.ts b/apps/web/utils/llms/index.ts index 7d24bb04a5..3b25dc3d48 100644 --- a/apps/web/utils/llms/index.ts +++ b/apps/web/utils/llms/index.ts @@ -32,6 +32,8 @@ import { createScopedLogger } from "@/utils/logger"; const logger = createScopedLogger("llms"); +const MAX_LOG_LENGTH = 200; + const commonOptions: { experimental_telemetry: { isEnabled: boolean }; headers?: Record; @@ -53,8 +55,8 @@ export function createGenerateText({ const generate = async (model: LanguageModelV2) => { logger.trace("Generating text", { label, - system: options.system?.slice(0, 200), - prompt: options.prompt?.slice(0, 200), + system: options.system?.slice(0, MAX_LOG_LENGTH), + prompt: options.prompt?.slice(0, MAX_LOG_LENGTH), }); const result = await generateText( @@ -128,8 +130,8 @@ export function createGenerateObject({ logger.trace("Generating object", { label, - system: options.system?.slice(0, 200), - prompt: options.prompt?.slice(0, 200), + system: options.system?.slice(0, MAX_LOG_LENGTH), + prompt: options.prompt?.slice(0, MAX_LOG_LENGTH), }); const result = await generateObject( diff --git a/biome.json b/biome.json index fe61b28e3f..608a982c40 100644 --- a/biome.json +++ b/biome.json @@ -21,6 +21,7 @@ "useCollapsedElseIf": "off" }, "suspicious": { + "noConsole": "warn", "noExplicitAny": "off", "noArrayIndexKey": "off", "noEmptyBlockStatements": "off", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 278ca2fafc..c97e00633a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,8 +183,8 @@ importers: specifier: 15.3.3 version: 15.3.3(next@15.3.3(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.49.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0) '@openrouter/ai-sdk-provider': - specifier: 1.0.0-beta.6 - version: 1.0.0-beta.6(ai@5.0.0(zod@3.25.46))(zod@3.25.46) + specifier: 1.1.0 + version: 1.1.0(ai@5.0.0(zod@3.25.46))(zod@3.25.46) '@portabletext/react': specifier: 3.2.1 version: 3.2.1(react@19.1.0) @@ -2656,11 +2656,11 @@ packages: '@octokit/types@13.6.2': resolution: {integrity: sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==} - '@openrouter/ai-sdk-provider@1.0.0-beta.6': - resolution: {integrity: sha512-1cj8yUek4ib10MqZ+9fw1p1a3SR0Zhx3HH1J3+WQIXXuH/rUhiAkwovgPi1ADuC1QECPSPiwfhK2GecnDm8hGg==} + '@openrouter/ai-sdk-provider@1.1.0': + resolution: {integrity: sha512-e5cW/KbgGakHOOsDhnHI6a0IDul9ER5J4QGM4yN9EfQ8XHfOFgwGGpLOopoRwkqaX5UdyQrpzei+1DPzg95i0A==} engines: {node: '>=18'} peerDependencies: - ai: ^5.0.0-beta.12 + ai: ^5.0.0 zod: ^3.24.1 || ^v4 '@opentelemetry/api-logs@0.57.2': @@ -13733,7 +13733,7 @@ snapshots: '@octokit/openapi-types': 22.2.0 optional: true - '@openrouter/ai-sdk-provider@1.0.0-beta.6(ai@5.0.0(zod@3.25.46))(zod@3.25.46)': + '@openrouter/ai-sdk-provider@1.1.0(ai@5.0.0(zod@3.25.46))(zod@3.25.46)': dependencies: ai: 5.0.0(zod@3.25.46) zod: 3.25.46