Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 490031fbee | |||
| da83fb5b8a | |||
| da98e0345d | |||
| 359f0ca4c7 | |||
| c58ee71c9c | |||
| 04be7a66c0 | |||
| ef333ae7f2 | |||
|
|
4453fb7943 | ||
|
|
25f7a72b64 | ||
|
|
895e2fc04b | ||
|
|
26bb716d96 | ||
| 017e538396 | |||
| 99a04d2a3e | |||
| 3f64b70e18 | |||
| b2c11ca77a | |||
|
|
272e4df227 | ||
| 2a6fb86059 | |||
|
|
d8c581bd21 | ||
| 0ca1c7bbf0 | |||
|
|
315044f5ce | ||
| 579e3ad1c5 | |||
| abad698d25 | |||
|
|
ef1ce8aa87 | ||
|
|
152088a923 | ||
|
|
53b9afeafd | ||
|
|
99ce034ca8 | ||
| 24fd38eeb0 | |||
|
|
98b0510dfb | ||
|
|
98691268c0 | ||
| b154474075 | |||
|
|
a91c4d56b0 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.next
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.md
|
||||||
|
.husky
|
||||||
15
.env.example
15
.env.example
@@ -1,13 +1,4 @@
|
|||||||
ANTHROPIC_API_KEY=
|
|
||||||
NEXT_PUBLIC_CONTACT_EMAIL=
|
NEXT_PUBLIC_CONTACT_EMAIL=
|
||||||
POSTGRES_DATABASE=
|
OVHCLOUD_API_KEY=
|
||||||
POSTGRES_HOST=
|
PURCHASE_REFLECTION_THRESHOLD=50
|
||||||
POSTGRES_PASSWORD=
|
NUMBER_OF_WEEKS=4
|
||||||
POSTGRES_PRISMA_URL=
|
|
||||||
POSTGRES_URL=
|
|
||||||
POSTGRES_URL_NON_POOLING=
|
|
||||||
POSTGRES_URL_NO_SSL=
|
|
||||||
POSTGRES_USER=
|
|
||||||
PURCHASE_REFLECTION_THRESHOLD=
|
|
||||||
NUMBER_OF_WEEKS=
|
|
||||||
RATE_LIMIT=
|
|
||||||
|
|||||||
41
.gitea/workflows/deploy.yml
Normal file
41
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-build-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
- name: Run linting
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Deploy to VPS
|
||||||
|
env:
|
||||||
|
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
ssh-keyscan -H 51.210.247.57 >> ~/.ssh/known_hosts
|
||||||
|
ssh debian@51.210.247.57 << 'EOF'
|
||||||
|
cd /home/debian/synthetic-consumer-data
|
||||||
|
git pull origin main
|
||||||
|
cd /home/debian/infrastructure
|
||||||
|
docker-compose up -d --build synthetic-consumer-data
|
||||||
|
EOF
|
||||||
86
.github/workflows/ci.yml
vendored
Normal file
86
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-typecheck:
|
||||||
|
name: Lint & Type Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: yarn lint
|
||||||
|
|
||||||
|
- name: Run TypeScript type check
|
||||||
|
run: yarn typecheck
|
||||||
|
|
||||||
|
- name: Run Prettier check
|
||||||
|
run: yarn format
|
||||||
|
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-typecheck
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: yarn build
|
||||||
|
|
||||||
|
- name: Report deployment readiness
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
run: echo "✅ All checks passed - ready for deployment"
|
||||||
|
|
||||||
|
commitlint:
|
||||||
|
name: Commit Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Validate commit messages
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||||
|
yarn commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||||
|
else
|
||||||
|
yarn commitlint --from HEAD~1 --to HEAD --verbose
|
||||||
|
fi
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -133,3 +133,4 @@ dist
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vercel
|
.vercel
|
||||||
|
.env*.local
|
||||||
|
|||||||
64
Dockerfile
Normal file
64
Dockerfile
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
||||||
|
RUN \
|
||||||
|
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||||
|
elif [ -f package-lock.json ]; then npm ci; \
|
||||||
|
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
||||||
|
else echo "Lockfile not found." && exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build arguments for environment variables needed at build time
|
||||||
|
ARG NEXT_PUBLIC_CONTACT_EMAIL
|
||||||
|
ENV NEXT_PUBLIC_CONTACT_EMAIL=$NEXT_PUBLIC_CONTACT_EMAIL
|
||||||
|
|
||||||
|
# Next.js telemetry
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
if [ -f yarn.lock ]; then yarn run build; \
|
||||||
|
elif [ -f package-lock.json ]; then npm run build; \
|
||||||
|
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
||||||
|
else echo "Lockfile not found." && exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Synthetic Consumers Data Generator
|
# Synthetic Consumers Data Generator
|
||||||
|
|
||||||
A NextJS application that makes use of the Anthropic Claude API to generate synthetic consumers and their weekly purchase history. For creating synthetic datasets for retail/e-commerce applications with believable user behaviors and spending patterns.
|
A NextJS application that makes use of Anthropic models via Vercel AI Gateway to generate synthetic consumers and their weekly purchase history. For creating synthetic datasets for retail/e-commerce applications with believable user behaviors and spending patterns.
|
||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
@@ -51,10 +51,6 @@ yarn dev
|
|||||||
- `yarn audit` - Run audit
|
- `yarn audit` - Run audit
|
||||||
- `yarn vercel:link` - Link Vercel project
|
- `yarn vercel:link` - Link Vercel project
|
||||||
- `yarn vercel:env` - Pull .env from Vercel
|
- `yarn vercel:env` - Pull .env from Vercel
|
||||||
- `yarn prisma:migrate` - Migrate database
|
|
||||||
- `yarn prisma:push` - Push migrations
|
|
||||||
- `yarn prisma:generate`- Generate Prisma types
|
|
||||||
- `yarn prisma:reset` - Reset database
|
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { generate } from '@utils/consumer/store';
|
import { generate } from '@utils/consumer/store';
|
||||||
import { rateLimiter } from '@utils/rateLimiter';
|
|
||||||
|
|
||||||
export async function POST() {
|
export async function POST() {
|
||||||
const rateLimit = await rateLimiter();
|
|
||||||
if (rateLimit) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Rate limit exceeded.' },
|
|
||||||
{ status: 429 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await generate();
|
const data = await generate();
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
id: data.id,
|
|
||||||
consumer: data.consumer
|
consumer: data.consumer
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,17 +1,8 @@
|
|||||||
import { generate } from '@purchases/store';
|
import { generate } from '@purchases/store';
|
||||||
import { purchasesRequestSchema } from '@purchases/types';
|
import { purchasesRequestSchema } from '@purchases/types';
|
||||||
import { rateLimiter } from '@utils/rateLimiter';
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
const rateLimit = await rateLimiter();
|
|
||||||
if (rateLimit) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Rate limit exceeded.' },
|
|
||||||
{ status: 429 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
|
|
||||||
@@ -23,9 +14,9 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, consumer } = result.data;
|
const { consumer } = result.data;
|
||||||
|
|
||||||
const purchaseList = await generate(id, consumer, new Date());
|
const purchaseList = await generate(consumer, new Date());
|
||||||
|
|
||||||
return NextResponse.json(purchaseList);
|
return NextResponse.json(purchaseList);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
import Script from 'next/script';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Synthetic Consumers Data Generator',
|
title: 'Synthetic Consumers Data Generator',
|
||||||
@@ -15,6 +16,11 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang='en'>
|
<html lang='en'>
|
||||||
<body>{children}</body>
|
<body>{children}</body>
|
||||||
|
<Script
|
||||||
|
defer
|
||||||
|
src="https://analytics.frompixels.com/script.js"
|
||||||
|
data-website-id="66a4c395-0e4c-4f66-910a-5ed92436e2cd"
|
||||||
|
/>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { Button } from './Button';
|
|||||||
import { useToast } from '../context/toast/ToastContext';
|
import { useToast } from '../context/toast/ToastContext';
|
||||||
import { Toasts } from './Toast';
|
import { Toasts } from './Toast';
|
||||||
import { LineChart, PersonStanding, Download, Sparkles } from 'lucide-react';
|
import { LineChart, PersonStanding, Download, Sparkles } from 'lucide-react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
export const Content = () => {
|
export const Content = () => {
|
||||||
const [consumerId, setConsumerId] = useState<number>();
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [consumer, setConsumer] = useState<Consumer | null>(null);
|
const [consumer, setConsumer] = useState<Consumer | null>(null);
|
||||||
@@ -37,6 +37,7 @@ export const Content = () => {
|
|||||||
try {
|
try {
|
||||||
downloadJson(consumer, 'consumer.json');
|
downloadJson(consumer, 'consumer.json');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Failed to download consumer data', err);
|
||||||
showToast('Failed to download consumer data');
|
showToast('Failed to download consumer data');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -46,6 +47,7 @@ export const Content = () => {
|
|||||||
try {
|
try {
|
||||||
downloadJson(purchasesResult, 'purchases.json');
|
downloadJson(purchasesResult, 'purchases.json');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Failed to download purchase history', err);
|
||||||
showToast('Failed to download purchase history');
|
showToast('Failed to download purchase history');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -58,24 +60,24 @@ export const Content = () => {
|
|||||||
setPurchasesResult(null);
|
setPurchasesResult(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/consumer', {
|
const { data } = await axios.post(
|
||||||
method: 'POST',
|
'/api/consumer',
|
||||||
|
{},
|
||||||
|
{
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to generate consumer');
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
setConsumerId(data.id);
|
|
||||||
setConsumer(data.consumer);
|
setConsumer(data.consumer);
|
||||||
|
|
||||||
setEditedConsumer(JSON.stringify(data.consumer, null, 2));
|
setEditedConsumer(JSON.stringify(data.consumer, null, 2));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err instanceof Error ? err.message : 'Something went wrong');
|
console.error('Something went wrong', err);
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const errorMessage = err.response?.data?.error || err.message;
|
||||||
|
showToast(errorMessage);
|
||||||
|
} else {
|
||||||
|
showToast('Something went wrong');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -107,29 +109,28 @@ export const Content = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const jsonData = JSON.parse(editedConsumer);
|
const jsonData = JSON.parse(editedConsumer);
|
||||||
const requestData = { id: consumerId, consumer: jsonData };
|
const requestData = { consumer: jsonData };
|
||||||
|
|
||||||
const validationResult = purchasesRequestSchema.safeParse(requestData);
|
const validationResult = purchasesRequestSchema.safeParse(requestData);
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
throw new Error(validationResult.error.issues[0].message);
|
throw new Error(validationResult.error.issues[0].message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch('/api/purchase-list', {
|
const { data } = await axios.post('/api/purchase-list', requestData, {
|
||||||
method: 'POST',
|
headers: { 'Content-Type': 'application/json' }
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(requestData)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json();
|
|
||||||
throw new Error(errorData.error || 'Failed to generate purchases');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
setPurchasesResult(data);
|
setPurchasesResult(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err instanceof Error ? err.message : 'Something went wrong');
|
console.error('Something went wrong', err);
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
const errorMessage = err.response?.data?.error || err.message;
|
||||||
|
showToast(errorMessage);
|
||||||
|
} else if (err instanceof Error) {
|
||||||
|
showToast(err.message);
|
||||||
|
} else {
|
||||||
|
showToast('Something went wrong');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
@@ -168,7 +169,7 @@ export const Content = () => {
|
|||||||
Quick Start
|
Quick Start
|
||||||
</h2>
|
</h2>
|
||||||
<p className='text-sm text-slate-600'>
|
<p className='text-sm text-slate-600'>
|
||||||
Generate synthetic data in minutes
|
Generate synthetic data in seconds
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
|||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
7
next.config.ts
Normal file
7
next.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: 'standalone'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
9510
package-lock.json
generated
Normal file
9510
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -5,7 +5,7 @@
|
|||||||
"author": "riccardo@frompixels.com",
|
"author": "riccardo@frompixels.com",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "prisma generate && next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint && eslint . --fix",
|
"lint": "next lint && eslint . --fix",
|
||||||
"format": "prettier --config .prettierrc '**/*.{ts,tsx,json,md}' --write",
|
"format": "prettier --config .prettierrc '**/*.{ts,tsx,json,md}' --write",
|
||||||
@@ -13,26 +13,22 @@
|
|||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"audit": "audit-ci",
|
"audit": "audit-ci",
|
||||||
"vercel:link": "vercel link",
|
"vercel:link": "vercel link",
|
||||||
"vercel:env": "vercel env pull .env",
|
"vercel:env": "vercel env pull .env"
|
||||||
"prisma:migrate": "npx prisma migrate dev",
|
|
||||||
"prisma:push": "npx prisma db push",
|
|
||||||
"prisma:generate": "npx prisma generate",
|
|
||||||
"prisma:reset": "npx prisma db push --force-reset"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.32.1",
|
"axios": "^1.12.0",
|
||||||
"@prisma/client": "^6.0.1",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"openai": "^4.77.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"next": "14.2.10",
|
"next": "15.4.10",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
"zod": "^3.23.8"
|
"zod": "^4.1.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^18.4.3",
|
"@commitlint/cli": "^18.4.3",
|
||||||
@@ -40,18 +36,17 @@
|
|||||||
"@types/node": "^22.10.1",
|
"@types/node": "^22.10.1",
|
||||||
"@types/react": "^18.3.12",
|
"@types/react": "^18.3.12",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||||
"@typescript-eslint/parser": "^6.12.0",
|
"@typescript-eslint/parser": "^8.46.0",
|
||||||
"audit-ci": "^6.6.1",
|
"audit-ci": "^6.6.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "^15.0.3",
|
"eslint-config-next": "^15.5.4",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^15.1.0",
|
"lint-staged": "^15.1.0",
|
||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"prisma": "^6.0.1",
|
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.3.0"
|
"typescript": "^5.3.0"
|
||||||
@@ -63,5 +58,8 @@
|
|||||||
"*.{json,ts,tsx}": [
|
"*.{json,ts,tsx}": [
|
||||||
"prettier --write --ignore-unknown"
|
"prettier --write --ignore-unknown"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"brace-expansion": "^2.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "consumer" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"letters" TEXT NOT NULL,
|
|
||||||
"year" INTEGER NOT NULL,
|
|
||||||
"zipCode" TEXT NOT NULL,
|
|
||||||
"persona" JSONB NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "consumer_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "purchases" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"value" JSONB NOT NULL,
|
|
||||||
"consumerId" INTEGER NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "purchases_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "purchases" ADD CONSTRAINT "purchases_consumerId_fkey" FOREIGN KEY ("consumerId") REFERENCES "consumer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the column `persona` on the `consumer` table. All the data in the column will be lost.
|
|
||||||
- You are about to drop the column `year` on the `consumer` table. All the data in the column will be lost.
|
|
||||||
- Added the required column `birthday` to the `consumer` table without a default value. This is not possible if the table is not empty.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "consumer" DROP COLUMN "persona",
|
|
||||||
DROP COLUMN "year",
|
|
||||||
ADD COLUMN "birthday" TIMESTAMP(3) NOT NULL,
|
|
||||||
ADD COLUMN "editedValue" JSONB,
|
|
||||||
ADD COLUMN "value" JSONB;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "purchases" ALTER COLUMN "value" DROP NOT NULL;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "consumer" ALTER COLUMN "letters" DROP NOT NULL,
|
|
||||||
ALTER COLUMN "zipCode" DROP NOT NULL,
|
|
||||||
ALTER COLUMN "birthday" DROP NOT NULL;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Please do not edit this file manually
|
|
||||||
# It should be added in your version-control system (i.e. Git)
|
|
||||||
provider = "postgresql"
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
const globalForPrisma = global as unknown as { prisma: PrismaClient };
|
|
||||||
|
|
||||||
export const prisma = globalForPrisma.prisma || new PrismaClient();
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
|
||||||
|
|
||||||
export default prisma;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
|
|
||||||
directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
|
|
||||||
}
|
|
||||||
|
|
||||||
model Consumer {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
letters String?
|
|
||||||
birthday DateTime?
|
|
||||||
zipCode String?
|
|
||||||
value Json?
|
|
||||||
editedValue Json?
|
|
||||||
purchaseLists PurchaseList[]
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@map("consumer")
|
|
||||||
}
|
|
||||||
|
|
||||||
model PurchaseList {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
value Json?
|
|
||||||
consumer Consumer @relation(fields: [consumerId], references: [id])
|
|
||||||
consumerId Int
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@map("purchases")
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 26 KiB |
@@ -19,10 +19,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@app/*": ["./app/*"],
|
"@aiGateway": ["./utils/aiGatewayClient.ts"],
|
||||||
"@components/*": ["./components/*"],
|
"@components/*": ["./components/*"],
|
||||||
"@utils/*": ["./utils/*"],
|
"@utils/*": ["./utils/*"],
|
||||||
"@consumer/*": ["./utils/consumer/*"],
|
"@consumer/*": ["./utils/consumer/*"],
|
||||||
|
"@prisma": ["./prisma/prisma"],
|
||||||
"@purchases/*": ["./utils/purchases/*"]
|
"@purchases/*": ["./utils/purchases/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
120
utils/aiGatewayClient.ts
Normal file
120
utils/aiGatewayClient.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
|
let ovhAI: OpenAI | null = null;
|
||||||
|
|
||||||
|
function getClient(): OpenAI {
|
||||||
|
if (!ovhAI) {
|
||||||
|
ovhAI = new OpenAI({
|
||||||
|
apiKey: process.env.OVHCLOUD_API_KEY,
|
||||||
|
baseURL: 'https://oai.endpoints.kepler.ai.cloud.ovh.net/v1'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ovhAI;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseTool {
|
||||||
|
readonly name: string;
|
||||||
|
readonly input_schema: {
|
||||||
|
readonly type: 'object';
|
||||||
|
readonly properties: Record<string, unknown>;
|
||||||
|
readonly required?: readonly string[];
|
||||||
|
readonly description?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolUseBlock {
|
||||||
|
type: 'tool_use';
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
input: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
|
||||||
|
export async function makeRequest<T extends BaseTool>(
|
||||||
|
prompt: string,
|
||||||
|
toolDef: T
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
|
const requiredFields = toolDef.input_schema.required
|
||||||
|
? [...toolDef.input_schema.required]
|
||||||
|
: Object.keys(toolDef.input_schema.properties);
|
||||||
|
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
console.log(`AI request attempt ${attempt}/${MAX_RETRIES}`);
|
||||||
|
|
||||||
|
const completion = await getClient().chat.completions.create({
|
||||||
|
model: 'Meta-Llama-3_3-70B-Instruct',
|
||||||
|
temperature: 0.7,
|
||||||
|
max_tokens: 16000,
|
||||||
|
tools: [
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: toolDef.name,
|
||||||
|
description: toolDef.input_schema.description || '',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: toolDef.input_schema.properties,
|
||||||
|
required: requiredFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
tool_choice: {
|
||||||
|
type: 'function',
|
||||||
|
function: { name: toolDef.name }
|
||||||
|
},
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: `You are a data generation assistant that creates realistic synthetic data.
|
||||||
|
|
||||||
|
CRITICAL REQUIREMENTS:
|
||||||
|
1. You MUST call the function with ALL required fields populated
|
||||||
|
2. Required top-level fields: ${requiredFields.join(', ')}
|
||||||
|
3. Every nested object and array must be fully populated with realistic data
|
||||||
|
4. Do NOT leave any field as null, undefined, or empty
|
||||||
|
5. Generate diverse, realistic Italian consumer data`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: prompt
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = completion.choices[0]?.message;
|
||||||
|
|
||||||
|
if (!message?.tool_calls || message.tool_calls.length === 0) {
|
||||||
|
throw new Error('No function call found in response');
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolCall = message.tool_calls[0];
|
||||||
|
|
||||||
|
if (toolCall.function.name !== toolDef.name) {
|
||||||
|
throw new Error(
|
||||||
|
`Expected tool ${toolDef.name} but got ${toolCall.function.name}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = JSON.parse(toolCall.function.arguments);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Attempt ${attempt} failed:`, error);
|
||||||
|
lastError = error as Error;
|
||||||
|
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
const delay = 1000 * attempt;
|
||||||
|
console.log(`Retrying in ${delay}ms...`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('All retry attempts failed');
|
||||||
|
throw lastError || new Error('OVH AI Endpoints client error.');
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import 'dotenv/config';
|
|
||||||
import Anthropic from '@anthropic-ai/sdk';
|
|
||||||
|
|
||||||
export interface BaseTool {
|
|
||||||
readonly name: string;
|
|
||||||
readonly input_schema: {
|
|
||||||
readonly type: 'object';
|
|
||||||
readonly properties: Record<string, unknown>;
|
|
||||||
readonly required?: readonly string[];
|
|
||||||
readonly description?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function makeRequest<T extends BaseTool>(prompt: string, tool: T) {
|
|
||||||
if (!process.env.ANTHROPIC_API_KEY) {
|
|
||||||
throw Error('No Anthropic API key found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client.messages.create({
|
|
||||||
model: 'claude-3-5-sonnet-20241022',
|
|
||||||
max_tokens: 8192,
|
|
||||||
temperature: 1,
|
|
||||||
tools: [tool],
|
|
||||||
messages: [{ role: 'user', content: prompt }]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.stop_reason && response.stop_reason !== 'tool_use') {
|
|
||||||
throw Error(JSON.stringify(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.content.length != 2) {
|
|
||||||
throw Error(JSON.stringify(response));
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = response.content as [
|
|
||||||
{ type: string; text: string },
|
|
||||||
{ type: string; input: object }
|
|
||||||
];
|
|
||||||
|
|
||||||
return content[1].input;
|
|
||||||
} catch (error) {
|
|
||||||
throw Error('Anthropic client error.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,14 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { Consumer, consumerSchema } from './types';
|
import { Consumer, consumerSchema } from './types';
|
||||||
import { Tool } from './tool';
|
import { Tool } from './tool';
|
||||||
import { BaseTool, makeRequest } from '../anthropicClient';
|
import { BaseTool, makeRequest } from '@utils/aiGatewayClient';
|
||||||
import { generatePrompt } from './prompt';
|
import { generatePrompt } from './prompt';
|
||||||
import { generateConsumerSeed } from '@utils/generateConsumerSeed';
|
import { generateConsumerSeed } from '@utils/generateConsumerSeed';
|
||||||
import prisma from '../../prisma/prisma';
|
|
||||||
|
|
||||||
export async function generate() {
|
export async function generate() {
|
||||||
const { letters, birthday, zipCode } = generateConsumerSeed();
|
const { letters, birthday, zipCode } = generateConsumerSeed();
|
||||||
|
|
||||||
const newConsumer = await prisma.consumer.create({
|
console.info(`New consumer being generated`);
|
||||||
data: {
|
|
||||||
letters,
|
|
||||||
birthday: birthday.toDate(),
|
|
||||||
zipCode
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info(`New consumer being generated with id ${newConsumer.id}`);
|
|
||||||
|
|
||||||
const prompt = generatePrompt(letters, birthday, zipCode);
|
const prompt = generatePrompt(letters, birthday, zipCode);
|
||||||
|
|
||||||
@@ -32,19 +23,7 @@ export async function generate() {
|
|||||||
|
|
||||||
console.info('Generated consumer by Anthropic', validConsumer.data);
|
console.info('Generated consumer by Anthropic', validConsumer.data);
|
||||||
|
|
||||||
await prisma.consumer.update({
|
|
||||||
where: {
|
|
||||||
id: newConsumer.id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
value: validConsumer.data
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info(`Consumer with id ${newConsumer.id} stored in database.`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: newConsumer.id,
|
|
||||||
consumer: validConsumer.data
|
consumer: validConsumer.data
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const coreSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const routinesSchema = z.object({
|
const routinesSchema = z.object({
|
||||||
weekday: z.record(weekdayActivitySchema),
|
weekday: z.record(z.string(), weekdayActivitySchema),
|
||||||
weekend: z.array(z.string()),
|
weekend: z.array(z.string()),
|
||||||
commute: z.object({
|
commute: z.object({
|
||||||
method: z.string(),
|
method: z.string(),
|
||||||
@@ -111,7 +111,7 @@ const financesSchema = z.object({
|
|||||||
subscriptions: z.array(subscriptionSchema),
|
subscriptions: z.array(subscriptionSchema),
|
||||||
spending_patterns: z.object({
|
spending_patterns: z.object({
|
||||||
impulsive_score: z.number().min(1).max(10),
|
impulsive_score: z.number().min(1).max(10),
|
||||||
categories: z.record(spendingCategorySchema)
|
categories: z.record(z.string(), spendingCategorySchema)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ExerciseActivity, Consumer, SocialActivity } from '../consumer/types';
|
import { ExerciseActivity, Consumer, SocialActivity } from '@consumer/types';
|
||||||
import { getWeekRanges, isDateInRange } from '../dateFunctions';
|
import { getWeekRanges, isDateInRange } from '../dateFunctions';
|
||||||
|
|
||||||
function formatCategories(
|
function formatCategories(
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { PurchaseList, purchaseListSchema } from './types';
|
import { PurchaseList, purchaseListSchema } from './types';
|
||||||
import { Tool } from './tool';
|
import { Tool } from './tool';
|
||||||
import { BaseTool, makeRequest } from '../anthropicClient';
|
import { BaseTool, makeRequest } from '@utils/aiGatewayClient';
|
||||||
import { generatePrompt } from './prompt';
|
import { generatePrompt } from './prompt';
|
||||||
import { Consumer } from '../consumer/types';
|
import { Consumer } from '@consumer/types';
|
||||||
import prisma from '../../prisma/prisma';
|
|
||||||
|
|
||||||
export async function generate(
|
export async function generate(editedConsumer: Consumer, date: Date) {
|
||||||
id: number | undefined,
|
|
||||||
editedConsumer: Consumer,
|
|
||||||
date: Date
|
|
||||||
) {
|
|
||||||
const consumerPrompt = await generatePrompt(
|
const consumerPrompt = await generatePrompt(
|
||||||
editedConsumer,
|
editedConsumer,
|
||||||
parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50'),
|
parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50'),
|
||||||
@@ -17,31 +12,6 @@ export async function generate(
|
|||||||
parseInt(process.env.NUMBER_OF_WEEKS ?? '4')
|
parseInt(process.env.NUMBER_OF_WEEKS ?? '4')
|
||||||
);
|
);
|
||||||
|
|
||||||
const consumer = id
|
|
||||||
? await prisma.consumer.update({
|
|
||||||
where: {
|
|
||||||
id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
editedValue: editedConsumer
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: await prisma.consumer.create({
|
|
||||||
data: {
|
|
||||||
editedValue: editedConsumer
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const newPurchaseList = await prisma.purchaseList.create({
|
|
||||||
data: {
|
|
||||||
consumerId: consumer.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
`Generating purchase list with id ${newPurchaseList.id} for consumer with id ${consumer.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = (await makeRequest(
|
const result = (await makeRequest(
|
||||||
consumerPrompt,
|
consumerPrompt,
|
||||||
@@ -58,21 +28,9 @@ export async function generate(
|
|||||||
(acc, week) => acc + week.purchases.length,
|
(acc, week) => acc + week.purchases.length,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
console.info(
|
|
||||||
`Generated ${totalPurchases} purchases for purchase list with id ${newPurchaseList.id} for consumer wth id ${id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await prisma.purchaseList.update({
|
|
||||||
where: {
|
|
||||||
id: newPurchaseList.id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
value: validPurchases.data
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info(
|
console.info(
|
||||||
`Purchase list with id ${newPurchaseList.id} for consumer with id ${id} stored in database.`
|
`Generated ${totalPurchases} purchases for new purchase list for consumer`
|
||||||
);
|
);
|
||||||
|
|
||||||
return validPurchases.data;
|
return validPurchases.data;
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import prisma from '../prisma/prisma';
|
|
||||||
|
|
||||||
export async function rateLimiter() {
|
|
||||||
if (!process.env.RATE_LIMIT) {
|
|
||||||
throw Error('Rate limit missing.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
||||||
|
|
||||||
const consumersCount = await prisma.consumer.count({
|
|
||||||
where: {
|
|
||||||
createdAt: {
|
|
||||||
gt: yesterday
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const purchaseListsCount = await prisma.purchaseList.count({
|
|
||||||
where: {
|
|
||||||
createdAt: {
|
|
||||||
gt: yesterday
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return consumersCount + purchaseListsCount > parseInt(process.env.RATE_LIMIT);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"functions": {
|
"functions": {
|
||||||
"app/api/**/*": {
|
"app/api/**/*": {
|
||||||
"maxDuration": 90
|
"maxDuration": 60
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"headers": [
|
"headers": [
|
||||||
|
|||||||
Reference in New Issue
Block a user