Compare commits

31 Commits
v1.0.0 ... main

Author SHA1 Message Date
490031fbee fix: correct infrastructure path
All checks were successful
Deploy / lint-build-deploy (push) Successful in 2m26s
2026-01-22 17:40:09 +00:00
da83fb5b8a feat: add analytics
Some checks failed
Deploy / lint-build-deploy (push) Has been cancelled
2026-01-22 18:34:55 +01:00
da98e0345d fix: tweak llama usage
All checks were successful
Deploy / lint-build-deploy (push) Successful in 2m28s
2026-01-19 20:36:13 +01:00
359f0ca4c7 fix: use npm install
All checks were successful
Deploy / lint-build-deploy (push) Successful in 3m11s
2026-01-19 20:27:58 +01:00
c58ee71c9c fix: legacy-peer-deps
Some checks failed
Deploy / lint-build-deploy (push) Failing after 6s
2026-01-19 20:26:08 +01:00
04be7a66c0 fix: use lazy loading
Some checks failed
Deploy / lint-build-deploy (push) Failing after 6s
2026-01-19 20:19:54 +01:00
ef333ae7f2 feat: switch to llama on ovh
Some checks failed
Deploy / lint-build-deploy (push) Failing after 7s
2026-01-19 20:06:09 +01:00
dependabot[bot]
4453fb7943 chore(deps): bump next in the npm_and_yarn group across 1 directory (#14)
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 15.4.9 to 15.4.10
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.4.9...v15.4.10)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.4.10
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 22:54:22 +01:00
dependabot[bot]
25f7a72b64 chore(deps): bump next in the npm_and_yarn group across 1 directory (#13)
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 15.4.8 to 15.4.9
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.4.8...v15.4.9)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.4.9
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 22:46:58 +01:00
dependabot[bot]
895e2fc04b chore(deps): bump next in the npm_and_yarn group across 1 directory (#12)
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 15.4.7 to 15.4.8
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.4.7...v15.4.8)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.4.8
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-06 15:11:41 +01:00
dependabot[bot]
26bb716d96 chore(deps): bump js-yaml in the npm_and_yarn group 2025-11-16 20:46:00 +01:00
017e538396 Vercel ai gateway (#10)
* feat: use vercel ai gateway

* fix: correct response handling

* ci: add pipeline
2025-10-11 15:07:54 +02:00
99a04d2a3e fix: correct handling of claude response 2025-09-28 17:16:29 +08:00
3f64b70e18 chore: update claude version 2025-09-27 21:03:01 +08:00
b2c11ca77a Merge pull request #9 from RiccardoSenica/dependabot/npm_and_yarn/npm_and_yarn-16c0b19af7
chore(deps): bump axios from 1.9.0 to 1.12.0 in the npm_and_yarn group across 1 directory
2025-09-14 20:50:13 +08:00
dependabot[bot]
272e4df227 chore(deps): bump axios in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [axios](https://github.com/axios/axios).


Updates `axios` from 1.9.0 to 1.12.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.9.0...v1.12.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-14 12:42:48 +00:00
2a6fb86059 Merge pull request #8 from RiccardoSenica/dependabot/npm_and_yarn/npm_and_yarn-2cb8645c2b
chore(deps): bump next from 15.3.3 to 15.4.7 in the npm_and_yarn group across 1 directory
2025-09-14 20:41:50 +08:00
dependabot[bot]
d8c581bd21 chore(deps): bump next in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 15.3.3 to 15.4.7
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.3.3...v15.4.7)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.4.7
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-30 12:12:02 +00:00
0ca1c7bbf0 Merge pull request #7 from RiccardoSenica/dependabot/npm_and_yarn/npm_and_yarn-e04d5d616f
chore(deps): bump form-data from 4.0.1 to 4.0.4 in the npm_and_yarn group across 1 directory
2025-08-04 20:19:20 +08:00
dependabot[bot]
315044f5ce chore(deps): bump form-data in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [form-data](https://github.com/form-data/form-data).


Updates `form-data` from 4.0.1 to 4.0.4
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.1...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-22 12:09:30 +00:00
579e3ad1c5 chore: add vercel analytics 2025-05-31 00:49:14 +02:00
abad698d25 chore: replace favicon 2025-01-18 22:17:09 +01:00
Riccardo Senica
ef1ce8aa87 Merge pull request #4 from RiccardoSenica/remove-prisma
chore: remove prisma
2025-01-09 07:09:47 +01:00
Riccardo Senica
152088a923 chore: remove prisma 2025-01-09 05:41:55 +00:00
Riccardo Senica
53b9afeafd Merge pull request #3 from RiccardoSenica/dependabot/npm_and_yarn/npm_and_yarn-1ca363a475
chore(deps): bump next from 14.2.15 to 14.2.21 in the npm_and_yarn group across 1 directory
2025-01-08 17:21:44 +01:00
dependabot[bot]
99ce034ca8 chore(deps): bump next in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 14.2.15 to 14.2.21
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.15...v14.2.21)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-04 02:28:52 +00:00
24fd38eeb0 chore: use axios 2024-12-29 20:30:55 +01:00
Riccardo Senica
98b0510dfb Merge pull request #2 from RiccardoSenica/dependabot/npm_and_yarn/npm_and_yarn-3226c314bf
chore(deps): bump next from 14.2.10 to 14.2.15 in the npm_and_yarn group across 1 directory
2024-12-21 14:32:18 +01:00
dependabot[bot]
98691268c0 chore(deps): bump next in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).


Updates `next` from 14.2.10 to 14.2.15
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.10...v14.2.15)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-18 18:46:06 +00:00
b154474075 chore: query optimization 2024-12-08 10:17:25 +01:00
Riccardo Senica
a91c4d56b0 chore: update text 2024-12-07 09:20:40 +01:00
32 changed files with 14617 additions and 6605 deletions

12
.dockerignore Normal file
View File

@@ -0,0 +1,12 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
.git
.gitignore
.next
.env
.env.*
!.env.example
*.md
.husky

View File

@@ -1,13 +1,4 @@
ANTHROPIC_API_KEY=
NEXT_PUBLIC_CONTACT_EMAIL=
POSTGRES_DATABASE=
POSTGRES_HOST=
POSTGRES_PASSWORD=
POSTGRES_PRISMA_URL=
POSTGRES_URL=
POSTGRES_URL_NON_POOLING=
POSTGRES_URL_NO_SSL=
POSTGRES_USER=
PURCHASE_REFLECTION_THRESHOLD=
NUMBER_OF_WEEKS=
RATE_LIMIT=
OVHCLOUD_API_KEY=
PURCHASE_REFLECTION_THRESHOLD=50
NUMBER_OF_WEEKS=4

View 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
View 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
View File

@@ -133,3 +133,4 @@ dist
.DS_Store
.vercel
.env*.local

64
Dockerfile Normal file
View 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"]

View File

@@ -1,6 +1,6 @@
# 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
@@ -51,10 +51,6 @@ yarn dev
- `yarn audit` - Run audit
- `yarn vercel:link` - Link Vercel project
- `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

View File

@@ -1,21 +1,11 @@
import { NextResponse } from 'next/server';
import { generate } from '@utils/consumer/store';
import { rateLimiter } from '@utils/rateLimiter';
export async function POST() {
const rateLimit = await rateLimiter();
if (rateLimit) {
return NextResponse.json(
{ error: 'Rate limit exceeded.' },
{ status: 429 }
);
}
try {
const data = await generate();
return NextResponse.json({
id: data.id,
consumer: data.consumer
});
} catch (error) {

View File

@@ -1,17 +1,8 @@
import { generate } from '@purchases/store';
import { purchasesRequestSchema } from '@purchases/types';
import { rateLimiter } from '@utils/rateLimiter';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const rateLimit = await rateLimiter();
if (rateLimit) {
return NextResponse.json(
{ error: 'Rate limit exceeded.' },
{ status: 429 }
);
}
try {
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);
} catch (error) {

View File

@@ -1,5 +1,6 @@
import type { Metadata } from 'next';
import './globals.css';
import Script from 'next/script';
export const metadata: Metadata = {
title: 'Synthetic Consumers Data Generator',
@@ -15,6 +16,11 @@ export default function RootLayout({
return (
<html lang='en'>
<body>{children}</body>
<Script
defer
src="https://analytics.frompixels.com/script.js"
data-website-id="66a4c395-0e4c-4f66-910a-5ed92436e2cd"
/>
</html>
);
}

View File

@@ -5,9 +5,9 @@ import { Button } from './Button';
import { useToast } from '../context/toast/ToastContext';
import { Toasts } from './Toast';
import { LineChart, PersonStanding, Download, Sparkles } from 'lucide-react';
import axios from 'axios';
export const Content = () => {
const [consumerId, setConsumerId] = useState<number>();
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [consumer, setConsumer] = useState<Consumer | null>(null);
@@ -37,6 +37,7 @@ export const Content = () => {
try {
downloadJson(consumer, 'consumer.json');
} catch (err) {
console.error('Failed to download consumer data', err);
showToast('Failed to download consumer data');
}
};
@@ -46,6 +47,7 @@ export const Content = () => {
try {
downloadJson(purchasesResult, 'purchases.json');
} catch (err) {
console.error('Failed to download purchase history', err);
showToast('Failed to download purchase history');
}
};
@@ -58,24 +60,24 @@ export const Content = () => {
setPurchasesResult(null);
try {
const response = await fetch('/api/consumer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const { data } = await axios.post(
'/api/consumer',
{},
{
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);
setEditedConsumer(JSON.stringify(data.consumer, null, 2));
} 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 {
setLoading(false);
}
@@ -107,29 +109,28 @@ export const Content = () => {
try {
const jsonData = JSON.parse(editedConsumer);
const requestData = { id: consumerId, consumer: jsonData };
const requestData = { consumer: jsonData };
const validationResult = purchasesRequestSchema.safeParse(requestData);
if (!validationResult.success) {
throw new Error(validationResult.error.issues[0].message);
}
const response = await fetch('/api/purchase-list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
const { data } = await axios.post('/api/purchase-list', requestData, {
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to generate purchases');
}
const data = await response.json();
setPurchasesResult(data);
} 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 {
setSubmitting(false);
}
@@ -168,7 +169,7 @@ export const Content = () => {
Quick Start
</h2>
<p className='text-sm text-slate-600'>
Generate synthetic data in minutes
Generate synthetic data in seconds
</p>
</div>
</div>

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"author": "riccardo@frompixels.com",
"scripts": {
"dev": "next dev",
"build": "prisma generate && next build",
"build": "next build",
"start": "next start",
"lint": "next lint && eslint . --fix",
"format": "prettier --config .prettierrc '**/*.{ts,tsx,json,md}' --write",
@@ -13,26 +13,22 @@
"prepare": "husky install",
"audit": "audit-ci",
"vercel:link": "vercel link",
"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"
"vercel:env": "vercel env pull .env"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@prisma/client": "^6.0.1",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"openai": "^4.77.0",
"clsx": "^2.1.1",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"lucide-react": "^0.462.0",
"moment": "^2.30.1",
"next": "14.2.10",
"next": "15.4.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwind-merge": "^2.5.5",
"zod": "^3.23.8"
"zod": "^4.1.8"
},
"devDependencies": {
"@commitlint/cli": "^18.4.3",
@@ -40,18 +36,17 @@
"@types/node": "^22.10.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.46.0",
"audit-ci": "^6.6.1",
"autoprefixer": "^10.4.20",
"eslint": "^8",
"eslint-config-next": "^15.0.3",
"eslint-config-next": "^15.5.4",
"eslint-config-prettier": "^9.0.0",
"husky": "^8.0.3",
"lint-staged": "^15.1.0",
"postcss": "^8.4.49",
"prettier": "^3.1.0",
"prisma": "^6.0.1",
"tailwindcss": "^3.4.15",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.3.0"
@@ -63,5 +58,8 @@
"*.{json,ts,tsx}": [
"prettier --write --ignore-unknown"
]
},
"resolutions": {
"brace-expansion": "^2.0.2"
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"

View File

@@ -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;

View File

@@ -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

View File

@@ -19,10 +19,11 @@
}
],
"paths": {
"@app/*": ["./app/*"],
"@aiGateway": ["./utils/aiGatewayClient.ts"],
"@components/*": ["./components/*"],
"@utils/*": ["./utils/*"],
"@consumer/*": ["./utils/consumer/*"],
"@prisma": ["./prisma/prisma"],
"@purchases/*": ["./utils/purchases/*"]
}
},

120
utils/aiGatewayClient.ts Normal file
View 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.');
}

View File

@@ -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.');
}
}

View File

@@ -1,23 +1,14 @@
import 'dotenv/config';
import { Consumer, consumerSchema } from './types';
import { Tool } from './tool';
import { BaseTool, makeRequest } from '../anthropicClient';
import { BaseTool, makeRequest } from '@utils/aiGatewayClient';
import { generatePrompt } from './prompt';
import { generateConsumerSeed } from '@utils/generateConsumerSeed';
import prisma from '../../prisma/prisma';
export async function generate() {
const { letters, birthday, zipCode } = generateConsumerSeed();
const newConsumer = await prisma.consumer.create({
data: {
letters,
birthday: birthday.toDate(),
zipCode
}
});
console.info(`New consumer being generated with id ${newConsumer.id}`);
console.info(`New consumer being generated`);
const prompt = generatePrompt(letters, birthday, zipCode);
@@ -32,19 +23,7 @@ export async function generate() {
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 {
id: newConsumer.id,
consumer: validConsumer.data
};
} catch (error) {

View File

@@ -91,7 +91,7 @@ const coreSchema = z.object({
});
const routinesSchema = z.object({
weekday: z.record(weekdayActivitySchema),
weekday: z.record(z.string(), weekdayActivitySchema),
weekend: z.array(z.string()),
commute: z.object({
method: z.string(),
@@ -111,7 +111,7 @@ const financesSchema = z.object({
subscriptions: z.array(subscriptionSchema),
spending_patterns: z.object({
impulsive_score: z.number().min(1).max(10),
categories: z.record(spendingCategorySchema)
categories: z.record(z.string(), spendingCategorySchema)
})
});

View File

@@ -1,4 +1,4 @@
import { ExerciseActivity, Consumer, SocialActivity } from '../consumer/types';
import { ExerciseActivity, Consumer, SocialActivity } from '@consumer/types';
import { getWeekRanges, isDateInRange } from '../dateFunctions';
function formatCategories(

View File

@@ -1,15 +1,10 @@
import { PurchaseList, purchaseListSchema } from './types';
import { Tool } from './tool';
import { BaseTool, makeRequest } from '../anthropicClient';
import { BaseTool, makeRequest } from '@utils/aiGatewayClient';
import { generatePrompt } from './prompt';
import { Consumer } from '../consumer/types';
import prisma from '../../prisma/prisma';
import { Consumer } from '@consumer/types';
export async function generate(
id: number | undefined,
editedConsumer: Consumer,
date: Date
) {
export async function generate(editedConsumer: Consumer, date: Date) {
const consumerPrompt = await generatePrompt(
editedConsumer,
parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50'),
@@ -17,31 +12,6 @@ export async function generate(
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 {
const result = (await makeRequest(
consumerPrompt,
@@ -58,21 +28,9 @@ export async function generate(
(acc, week) => acc + week.purchases.length,
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(
`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;

View File

@@ -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);
}

View File

@@ -1,7 +1,7 @@
{
"functions": {
"app/api/**/*": {
"maxDuration": 90
"maxDuration": 60
}
},
"headers": [

10991
yarn.lock

File diff suppressed because it is too large Load Diff