Untitled

 avatar
unknown
typescript
18 days ago
5.8 kB
4
Indexable
import { deepseek } from "@ai-sdk/deepseek";
import { Transaction } from "@prisma/client";
import { streamText, tool } from "ai";
import { getTranslations } from "next-intl/server";
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
import { z } from "zod";
import { CURRENCY_COOKIE_KEY } from "~/constants/app-config";
import { auth } from "~/lib/auth";
import { db } from "~/lib/db";

function serializeQueryResult(data: any): any {
  if (typeof data === "bigint") {
    return data.toString(); // Evita erro de JSON.stringify
  }

  if (Array.isArray(data)) {
    return data.map(serializeQueryResult); // Processa cada item do array
  }

  if (Buffer.isBuffer(data)) {
    return data.toString("base64"); // Converte para Base64
  }

  if (typeof data === "object" && data !== null) {
    return Object.fromEntries(
      Object.entries(data).map(([key, value]) => [
        key,
        serializeQueryResult(value),
      ]),
    );
  }

  return data; // Retorna o valor original se não for BigInt
}

// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

export async function POST(req: Request) {
  const session = await auth();

  if (!session?.user?.id) {
    return NextResponse.json({ error: "Unauthenticated" }, { status: 401 });
  }

  const userId = session.user.id;

  const { messages } = await req.json();

  const TransactionCategoryLabels = await getTranslations(
    "TRANSACTION_CATEGORY_LABELS",
  );

  const TransactionTypeLabels = await getTranslations(
    "TRANSACTION_TYPE_LABELS",
  );

  const TransactionPaymentMethodLabels = await getTranslations(
    "TRANSACTION_PAYMENT_METHOD_LABELS",
  );

  const cookieStore = cookies();
  const currencyCookie = cookieStore.get(CURRENCY_COOKIE_KEY);

  const result = streamText({
    model: deepseek("deepseek-chat"),
    messages,
    maxSteps: 5,
    system: `
      Você é um assistente financeiro responsável por analisar as transações do usuário.
      Responda apenas perguntas relacionadas às transações.
      Forneça apenas as informações solicitadas pelo usuário, sem incluir detalhes extras.
      Responda no mesmo idioma da mensagem enviada pelo usuário.
      Formate os valores na moeda definida: ${currencyCookie?.value}.
    `.trim(),
    tools: {
      PostgreSQL: tool({
        description: `
          Execute uma consulta no PostgreSQL para recuperar informações das tabelas do banco de dados.
          As estruturas de enums e tabelas estão definidas abaixo:

          ENUMS:
          """
          CREATE TYPE TransactionType AS ENUM ('DEPOSIT', 'EXPENSE', 'INVESTMENT');

          CREATE TYPE TransactionCategory AS ENUM (
            'HOUSING', 'TRANSPORTATION', 'FOOD', 'ENTERTAINMENT',
            'HEALTH', 'UTILITY', 'SALARY', 'EDUCATION', 'OTHER'
          );

          CREATE TYPE TransactionPaymentMethod AS ENUM (
            'CREDIT_CARD', 'DEBIT_CARD', 'BANK_TRANSFER',
            'BANK_SLIP', 'CASH', 'PIX', 'OTHER'
          );

          CREATE TYPE Plan AS ENUM ('BASIC', 'PREMIUM');
          """

          TABLES:
          """
          CREATE TABLE users (
            id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
            name TEXT,
            email TEXT UNIQUE NOT NULL,
            image TEXT,
            createdAt TIMESTAMP DEFAULT now() NOT NULL,
            updatedAt TIMESTAMP DEFAULT now() NOT NULL,
            plan Plan DEFAULT 'BASIC' NOT NULL,
            stripeCustomerId TEXT,
            stripeSubscriptionId TEXT,
            subscriptionExpiresAt TIMESTAMP
          );

          CREATE TABLE transactions (
            id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
            name TEXT NOT NULL,
            type TransactionType NOT NULL,
            amount DECIMAL(10,2) NOT NULL,
            category TransactionCategory NOT NULL,
            paymentMethod TransactionPaymentMethod NOT NULL,
            date TIMESTAMP NOT NULL,
            createdAt TIMESTAMP DEFAULT now() NOT NULL,
            updatedAt TIMESTAMP DEFAULT now() NOT NULL,
            userId UUID NOT NULL,
            CONSTRAINT fk_user FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
          );
          """

          Apenas operações de busca (SELECT) são permitidas. Qualquer operação de escrita/modificação é proibida.
          As transações retornadas devem pertencer exclusivamente ao usuário identificado pelo UUID: ${userId}.
          O retorno deve ser limitado a no máximo 25 itens por consulta.
          Se algum nome de tabela ou campo estiver em camelCase (exemplo: paymentMethod), utilize aspas duplas ao referenciá-lo (exemplo: "paymentMethod").
        `.trim(),

        parameters: z.object({
          query: z
            .string()
            .describe("A query do PostgreSQL para ser executada."),
          params: z
            .array(z.string())
            .describe("Parâmetros da query a ser executada"),
        }),
        execute: async ({ query, params }) => {
          console.log({ query, params });

          try {
            const result = await db.$queryRawUnsafe(`${query}`, `${params}`);

            const safeResult = serializeQueryResult(result);

            return {
              result: JSON.stringify(safeResult),
            };
          } catch (error) {
            console.log(error);

            return {
              error: true,
              message: "Ocorreu um erro no banco de dados, informe o usuário",
            };
          }
        },
      }),
    },
  });

  return result.toDataStreamResponse();
}
Editor is loading...
Leave a Comment