feat: general improvements
This commit is contained in:
@@ -1,4 +1,3 @@
|
|||||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/persona?schema=public&connect_timeout=300"
|
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/persona?schema=public&connect_timeout=300"
|
||||||
PERSONA_PROMPT=
|
|
||||||
PURCHASE_REFLECTION_THRESHOLD=50
|
PURCHASE_REFLECTION_THRESHOLD=50
|
||||||
ANTHROPIC_API_KEY=
|
ANTHROPIC_API_KEY=
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -11,9 +11,7 @@ A TypeScript application that leverages the Anthropic Claude API to generate rea
|
|||||||
- Financial patterns and spending habits
|
- Financial patterns and spending habits
|
||||||
- Contextual behaviors and upcoming events
|
- Contextual behaviors and upcoming events
|
||||||
- Create realistic weekly purchase histories that match persona profiles
|
- Create realistic weekly purchase histories that match persona profiles
|
||||||
- Store generated data in:
|
- Store generated data in JSON files for easy data portability
|
||||||
- PostgreSQL database for structured querying
|
|
||||||
- JSON files for easy data portability
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
@@ -29,13 +27,7 @@ yarn install
|
|||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Initialize the database:
|
3. Build and start the application:
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Build and start the application:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn build
|
yarn build
|
||||||
@@ -56,8 +48,6 @@ yarn dev
|
|||||||
- `yarn lint` - Run ESLint with automatic fixes
|
- `yarn lint` - Run ESLint with automatic fixes
|
||||||
- `yarn format` - Format code using Prettier
|
- `yarn format` - Format code using Prettier
|
||||||
- `yarn typecheck` - Check TypeScript types
|
- `yarn typecheck` - Check TypeScript types
|
||||||
- `yarn generate` - Generate Prisma client
|
|
||||||
- `yarn migrate` - Run database migrations
|
|
||||||
|
|
||||||
## ⚠️ Disclaimer
|
## ⚠️ Disclaimer
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
container_name: personadb
|
|
||||||
image: postgres:15
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: persona
|
|
||||||
ports:
|
|
||||||
- "5433:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "purchases-personas",
|
"name": "purchases-personas",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Generate realistic fictional personas and their weekly purchase behaviors using AI",
|
"description": "Generate realistic fictional personas and their weekly purchase behaviors using AI",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "prisma generate && node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "nodemon src/index.ts",
|
"dev": "nodemon src/index.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
@@ -16,10 +16,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.32.1",
|
"@anthropic-ai/sdk": "^0.32.1",
|
||||||
"@prisma/client": "^5.22.0",
|
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1"
|
"express": "^4.21.1",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^18.4.3",
|
"@commitlint/cli": "^18.4.3",
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
"lint-staged": "^15.1.0",
|
"lint-staged": "^15.1.0",
|
||||||
"nodemon": "^3.0.2",
|
"nodemon": "^3.0.2",
|
||||||
"prettier": "^3.1.0",
|
"prettier": "^3.1.0",
|
||||||
"prisma": "^5.8.0",
|
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.0"
|
"typescript": "^5.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "users" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"age" INTEGER NOT NULL,
|
|
||||||
"persona" JSONB NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "items" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"amount" DOUBLE PRECISION NOT NULL,
|
|
||||||
"datetime" TIMESTAMP(3) NOT NULL,
|
|
||||||
"location" TEXT NOT NULL,
|
|
||||||
"notes" TEXT,
|
|
||||||
"userId" INTEGER NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "items_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "items" ADD CONSTRAINT "items_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "items" ADD COLUMN "reflections" JSONB;
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "items" ALTER COLUMN "reflections" SET DEFAULT 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,36 +0,0 @@
|
|||||||
generator client {
|
|
||||||
provider = "prisma-client-js"
|
|
||||||
}
|
|
||||||
|
|
||||||
datasource db {
|
|
||||||
provider = "postgresql"
|
|
||||||
url = env("DATABASE_URL")
|
|
||||||
}
|
|
||||||
|
|
||||||
model User {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
name String
|
|
||||||
age Int
|
|
||||||
persona Json
|
|
||||||
items Item[]
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@map("users")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Item {
|
|
||||||
id Int @id @default(autoincrement())
|
|
||||||
name String
|
|
||||||
amount Float
|
|
||||||
datetime DateTime
|
|
||||||
location String
|
|
||||||
notes String?
|
|
||||||
reflections Json? @default(dbgenerated("null"))
|
|
||||||
user User @relation(fields: [userId], references: [id])
|
|
||||||
userId Int
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
|
||||||
@@map("items")
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,15 @@ import { generate as generatePurchases } from './purchase/store';
|
|||||||
|
|
||||||
const personaPromise = generatePersona();
|
const personaPromise = generatePersona();
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const numWeeks = 4;
|
||||||
|
|
||||||
console.log(`Generating persona...`);
|
console.log(`Generating persona...`);
|
||||||
|
|
||||||
personaPromise.then(id => {
|
personaPromise.then(id => {
|
||||||
console.log(`Generating purchases for id ${id}...`);
|
console.log(`Generating purchases for id ${id}...`);
|
||||||
|
|
||||||
const purchasesPromise = generatePurchases(id);
|
const purchasesPromise = generatePurchases(id, date, numWeeks);
|
||||||
|
|
||||||
purchasesPromise.then(() => {
|
purchasesPromise.then(() => {
|
||||||
console.log('Complete');
|
console.log('Complete');
|
||||||
|
|||||||
74
src/persona/prompt.ts
Normal file
74
src/persona/prompt.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { generatePersonaSeed } from '../utils/generatePersonaSeed';
|
||||||
|
|
||||||
|
export function generatePrompt(): string {
|
||||||
|
const seed = generatePersonaSeed();
|
||||||
|
const [letters, year, postalCode] = seed.split(':');
|
||||||
|
|
||||||
|
return `You are tasked with creating a detailed persona of an Italian individual based on the following seed information:
|
||||||
|
|
||||||
|
<persona_seed>
|
||||||
|
<name_letters>${letters}</name_letters>
|
||||||
|
<birth_year>${year}</birth_year>
|
||||||
|
<postal_code>${postalCode}</postal_code>
|
||||||
|
</persona_seed>
|
||||||
|
|
||||||
|
Your goal is to generate a realistic and diverse persona that reflects the complexity of Italian society. Follow these steps to create the persona:
|
||||||
|
|
||||||
|
1. Analyze the demographic data:
|
||||||
|
- Create a full name that includes ALL the letters provided in <name_letters>, though it may contain additional letters.
|
||||||
|
- Consider the implications of the birth year and postal code on the person's background and lifestyle.
|
||||||
|
|
||||||
|
2. Generate core demographics:
|
||||||
|
- Name and age (derived from the seed information)
|
||||||
|
- Occupation (including title, level, approximate income, location, and schedule)
|
||||||
|
- Home situation (residence type, ownership status, location)
|
||||||
|
- Household (relationship status, family members, pets if any)
|
||||||
|
|
||||||
|
3. Describe daily patterns:
|
||||||
|
- Detailed weekday schedule with approximate times and locations
|
||||||
|
- Typical weekend activities
|
||||||
|
- Commute details (transportation method, route, regular stops if applicable)
|
||||||
|
|
||||||
|
4. Define preferences and behaviors:
|
||||||
|
- Financial management style
|
||||||
|
- Brand relationships (with loyalty scores from 1 to 10)
|
||||||
|
- Preferred payment methods
|
||||||
|
|
||||||
|
5. Outline a financial profile:
|
||||||
|
- Fixed monthly expenses:
|
||||||
|
* Housing: rent/mortgage payments
|
||||||
|
* Utilities: electricity, gas, water, waste management
|
||||||
|
* Internet and phone services
|
||||||
|
* Insurance payments (home, car, health, life)
|
||||||
|
* Property taxes (if applicable)
|
||||||
|
- Regular subscriptions:
|
||||||
|
* Digital services (streaming, apps, etc.)
|
||||||
|
* Memberships (gym, clubs, etc.)
|
||||||
|
* Regular services (cleaning, maintenance, etc.)
|
||||||
|
- Category-specific spending patterns
|
||||||
|
- Impulse buying tendency (score from 1 to 10)
|
||||||
|
|
||||||
|
6. Describe regular activities:
|
||||||
|
- Exercise routines or lack thereof
|
||||||
|
- Social activities
|
||||||
|
|
||||||
|
7. Add personal context:
|
||||||
|
- Key stress triggers
|
||||||
|
- Reward behaviors
|
||||||
|
- Upcoming significant events
|
||||||
|
|
||||||
|
Throughout this process, consider the following:
|
||||||
|
- Ensure a diverse representation of technological aptitudes, not focusing solely on tech-savvy individuals.
|
||||||
|
- Use inclusive language and avoid stereotypes or discriminatory assumptions.
|
||||||
|
- Align numerical scores (1-10) and monetary values (in EUR) with:
|
||||||
|
a) Regional economic indicators
|
||||||
|
b) Generational trends
|
||||||
|
c) Professional sector norms
|
||||||
|
d) Local cost of living
|
||||||
|
|
||||||
|
Before providing the final persona, wrap your analysis in <persona_creation_process> tags. For each major section:
|
||||||
|
1. Break down the postal code implications on the person's background and lifestyle.
|
||||||
|
2. Consider multiple options for each aspect (at least 2-3 choices).
|
||||||
|
3. Explain your reasoning for the final choice.
|
||||||
|
This will help ensure a thorough and well-reasoned persona creation.`;
|
||||||
|
}
|
||||||
@@ -1,45 +1,33 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { prisma } from '../utils/prismaClient';
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { Persona } from './types';
|
import { Persona, personaSchema } from './types';
|
||||||
import { Tool } from './tool';
|
import { Tool } from './tool';
|
||||||
import { BaseTool, makeRequest } from '../utils/anthropicClient';
|
import { BaseTool, makeRequest } from '../utils/anthropicClient';
|
||||||
import { createFolderIfNotExists } from '../utils/createFolder';
|
import { createFolderIfNotExists } from '../utils/createFolder';
|
||||||
import { generatePrompt } from '../utils/generatePersonaSeed';
|
import { generatePrompt } from './prompt';
|
||||||
|
import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
export async function generate() {
|
export async function generate() {
|
||||||
if (!process.env.PERSONA_PROMPT) {
|
|
||||||
throw Error('Persona prompt missing.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompt = `${generatePrompt()} ${process.env.PERSONA_PROMPT}`;
|
const prompt = `${generatePrompt()} ${process.env.PERSONA_PROMPT}`;
|
||||||
|
|
||||||
const result = (await makeRequest(prompt, Tool as BaseTool)) as Persona;
|
const result = (await makeRequest(prompt, Tool as BaseTool)) as Persona;
|
||||||
|
|
||||||
const id = await saveToDb(result);
|
const validPersona = personaSchema.safeParse(result);
|
||||||
|
|
||||||
await saveToJson(result, id);
|
if (validPersona.error) {
|
||||||
|
throw Error(`Invalid Persona generated: ${validPersona.error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Persona name: ${result.core.name}`);
|
const id = randomUUID();
|
||||||
|
|
||||||
|
await saveToJson(validPersona.data, id);
|
||||||
|
|
||||||
|
console.log(`Persona name: ${validPersona.data.core.name}`);
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveToDb(persona: Persona) {
|
export async function saveToJson(persona: Persona, id: string) {
|
||||||
const result = await prisma.user.create({
|
|
||||||
data: {
|
|
||||||
name: persona.core.name,
|
|
||||||
age: persona.core.age,
|
|
||||||
persona: JSON.stringify(persona)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Persona '${result.name}' inserted in DB with id ${result.id}`);
|
|
||||||
|
|
||||||
return result.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveToJson(persona: Persona, id: number) {
|
|
||||||
await createFolderIfNotExists(`personas/${id}/`);
|
await createFolderIfNotExists(`personas/${id}/`);
|
||||||
|
|
||||||
const jsonName = `personas/${id}/${id}-persona.json`;
|
const jsonName = `personas/${id}/${id}-persona.json`;
|
||||||
|
|||||||
@@ -226,13 +226,13 @@ export const Tool = {
|
|||||||
properties: {
|
properties: {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
type: 'array' as const,
|
type: 'array' as const,
|
||||||
description: 'Regular subscriptions',
|
description: 'Regular subscriptions and fixed expenses',
|
||||||
items: {
|
items: {
|
||||||
type: 'object' as const,
|
type: 'object' as const,
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
description: 'Subscription name'
|
description: 'Subscription or expense name'
|
||||||
},
|
},
|
||||||
amount: {
|
amount: {
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
@@ -246,8 +246,40 @@ export const Tool = {
|
|||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
description: 'Next payment date',
|
description: 'Next payment date',
|
||||||
format: 'date'
|
format: 'date'
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: 'string' as const,
|
||||||
|
description: 'Expense category',
|
||||||
|
enum: [
|
||||||
|
'housing',
|
||||||
|
'utilities',
|
||||||
|
'insurance',
|
||||||
|
'services',
|
||||||
|
'memberships',
|
||||||
|
'digital',
|
||||||
|
'taxes',
|
||||||
|
'other'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
is_fixed_expense: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
description:
|
||||||
|
'Whether this is a fixed expense (utilities, rent) or optional subscription'
|
||||||
|
},
|
||||||
|
auto_payment: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
description: 'Whether payment is automated'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
required: [
|
||||||
|
'name',
|
||||||
|
'amount',
|
||||||
|
'frequency',
|
||||||
|
'next_due_date',
|
||||||
|
'category',
|
||||||
|
'is_fixed_expense',
|
||||||
|
'auto_payment'
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
spending_patterns: {
|
spending_patterns: {
|
||||||
|
|||||||
@@ -1,118 +1,138 @@
|
|||||||
interface Pet {
|
import { z } from 'zod';
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FrequencyObject {
|
const petSchema = z.object({
|
||||||
name: string;
|
type: z.string(),
|
||||||
frequency: number;
|
name: z.string()
|
||||||
}
|
});
|
||||||
|
|
||||||
interface SubscriptionBill {
|
const regularStopSchema = z.object({
|
||||||
name: string;
|
location: z.string(),
|
||||||
amount: number;
|
purpose: z.string(),
|
||||||
date: string | Date;
|
frequency: z.string()
|
||||||
}
|
});
|
||||||
|
|
||||||
interface ActivityObject {
|
const weekdayActivitySchema = z.object({
|
||||||
name: string;
|
activity: z.string(),
|
||||||
frequency: number;
|
location: z.string(),
|
||||||
schedule?: string[];
|
duration_minutes: z.number()
|
||||||
}
|
});
|
||||||
|
|
||||||
interface BrandLoyalty {
|
const brandPreferenceSchema = z.object({
|
||||||
name: string;
|
name: z.string(),
|
||||||
loyaltyScore: number;
|
loyalty_score: z.number().min(1).max(10)
|
||||||
}
|
});
|
||||||
|
|
||||||
interface Event {
|
const subscriptionSchema = z.object({
|
||||||
name: string;
|
name: z.string(),
|
||||||
date: string | Date;
|
amount: z.number(),
|
||||||
details?: string;
|
frequency: z.string(),
|
||||||
}
|
next_due_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||||
|
category: z.enum([
|
||||||
|
'housing',
|
||||||
|
'utilities',
|
||||||
|
'insurance',
|
||||||
|
'services',
|
||||||
|
'memberships',
|
||||||
|
'digital',
|
||||||
|
'taxes',
|
||||||
|
'other'
|
||||||
|
]),
|
||||||
|
is_fixed_expense: z.boolean(),
|
||||||
|
auto_payment: z.boolean()
|
||||||
|
});
|
||||||
|
|
||||||
interface TimelineActivity {
|
const spendingCategorySchema = z.object({
|
||||||
activity: string;
|
preference_score: z.number().min(1).max(10),
|
||||||
duration: string;
|
monthly_budget: z.number()
|
||||||
location?: string;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
interface RegularStop {
|
const exerciseActivitySchema = z.object({
|
||||||
location: string;
|
activity: z.string(),
|
||||||
purpose: string;
|
frequency: z.string(),
|
||||||
frequency: string;
|
duration_minutes: z.number()
|
||||||
}
|
});
|
||||||
|
|
||||||
interface SpendingCategories {
|
export type ExerciseActivity = z.infer<typeof exerciseActivitySchema>;
|
||||||
[category: string]: {
|
|
||||||
preference: number;
|
|
||||||
frequency: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Persona {
|
const socialActivitySchema = z.object({
|
||||||
core: {
|
activity: z.string(),
|
||||||
age: number;
|
frequency: z.string()
|
||||||
name: string;
|
});
|
||||||
occupation: {
|
|
||||||
title: string;
|
export type SocialActivity = z.infer<typeof socialActivitySchema>;
|
||||||
level: string;
|
|
||||||
income: number;
|
const upcomingEventSchema = z.object({
|
||||||
location: string;
|
name: z.string(),
|
||||||
schedule: string[];
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||||
};
|
importance: z.number().min(1).max(10)
|
||||||
home: {
|
});
|
||||||
type: string;
|
|
||||||
ownership: string;
|
const coreSchema = z.object({
|
||||||
location: string;
|
age: z.number(),
|
||||||
commute_distance_km: number;
|
name: z.string(),
|
||||||
};
|
occupation: z.object({
|
||||||
household: {
|
title: z.string(),
|
||||||
status: string;
|
level: z.string(),
|
||||||
members: string[];
|
income: z.number(),
|
||||||
pets: Pet[];
|
location: z.string(),
|
||||||
};
|
schedule: z.array(z.string())
|
||||||
};
|
}),
|
||||||
routines: {
|
home: z.object({
|
||||||
weekday: {
|
type: z.string(),
|
||||||
[hour: string]: TimelineActivity;
|
ownership: z.string(),
|
||||||
};
|
location: z.string(),
|
||||||
weekend: ActivityObject[];
|
commute_distance_km: z.number()
|
||||||
commute: {
|
}),
|
||||||
method: string;
|
household: z.object({
|
||||||
route: string[];
|
status: z.string(),
|
||||||
regular_stops: RegularStop[];
|
members: z.array(z.string()),
|
||||||
};
|
pets: z.array(petSchema)
|
||||||
};
|
})
|
||||||
preferences: {
|
});
|
||||||
diet: string[];
|
|
||||||
brands: BrandLoyalty[];
|
const routinesSchema = z.object({
|
||||||
price_sensitivity: number;
|
weekday: z.record(weekdayActivitySchema),
|
||||||
payment_methods: string[];
|
weekend: z.array(z.string()),
|
||||||
shopping: {
|
commute: z.object({
|
||||||
grocery_stores: FrequencyObject[];
|
method: z.string(),
|
||||||
coffee_shops: FrequencyObject[];
|
route: z.array(z.string()),
|
||||||
restaurants: FrequencyObject[];
|
regular_stops: z.array(regularStopSchema)
|
||||||
retail: FrequencyObject[];
|
})
|
||||||
};
|
});
|
||||||
};
|
|
||||||
finances: {
|
const preferencesSchema = z.object({
|
||||||
subscriptions: SubscriptionBill[];
|
diet: z.array(z.string()),
|
||||||
regular_bills: SubscriptionBill[];
|
brands: z.array(brandPreferenceSchema),
|
||||||
spending_patterns: {
|
price_sensitivity: z.number().min(1).max(10),
|
||||||
impulsive_score: number;
|
payment_methods: z.array(z.string())
|
||||||
categories: SpendingCategories;
|
});
|
||||||
};
|
|
||||||
};
|
const financesSchema = z.object({
|
||||||
habits: {
|
subscriptions: z.array(subscriptionSchema),
|
||||||
exercise: ActivityObject[];
|
spending_patterns: z.object({
|
||||||
social: ActivityObject[];
|
impulsive_score: z.number().min(1).max(10),
|
||||||
entertainment: ActivityObject[];
|
categories: z.record(spendingCategorySchema)
|
||||||
vices: ActivityObject[];
|
})
|
||||||
};
|
});
|
||||||
context: {
|
|
||||||
stress_triggers: string[];
|
const habitsSchema = z.object({
|
||||||
reward_behaviors: string[];
|
exercise: z.array(exerciseActivitySchema),
|
||||||
upcoming_events: Event[];
|
social: z.array(socialActivitySchema)
|
||||||
recent_changes: string[];
|
});
|
||||||
};
|
|
||||||
}
|
const contextSchema = z.object({
|
||||||
|
stress_triggers: z.array(z.string()),
|
||||||
|
reward_behaviors: z.array(z.string()),
|
||||||
|
upcoming_events: z.array(upcomingEventSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const personaSchema = z.object({
|
||||||
|
core: coreSchema,
|
||||||
|
routines: routinesSchema,
|
||||||
|
preferences: preferencesSchema,
|
||||||
|
finances: financesSchema,
|
||||||
|
habits: habitsSchema,
|
||||||
|
context: contextSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Persona = z.infer<typeof personaSchema>;
|
||||||
|
|||||||
296
src/purchase/prompt.ts
Normal file
296
src/purchase/prompt.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import { ExerciseActivity, Persona, SocialActivity } from '../persona/types';
|
||||||
|
import { getWeekRanges, isDateInRange } from '../utils/dateFunctions';
|
||||||
|
|
||||||
|
function formatCategories(
|
||||||
|
categories?: Record<
|
||||||
|
string,
|
||||||
|
{ preference_score: number; monthly_budget: number }
|
||||||
|
>
|
||||||
|
): string {
|
||||||
|
if (!categories || Object.keys(categories).length === 0) {
|
||||||
|
return '(No spending categories defined)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(categories)
|
||||||
|
.map(([category, data]) => {
|
||||||
|
const weeklyBudget = Math.round(data.monthly_budget / 4.33);
|
||||||
|
const preferenceLevel =
|
||||||
|
data.preference_score >= 8
|
||||||
|
? 'high'
|
||||||
|
: data.preference_score >= 5
|
||||||
|
? 'moderate'
|
||||||
|
: 'low';
|
||||||
|
return `- ${category}: €${weeklyBudget}/week (${preferenceLevel} preference)`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHabits(habits: {
|
||||||
|
exercise: ExerciseActivity[];
|
||||||
|
social: SocialActivity[];
|
||||||
|
}): string {
|
||||||
|
const sections: string[] = [];
|
||||||
|
|
||||||
|
if (habits.exercise?.length) {
|
||||||
|
sections.push(
|
||||||
|
'Activity Pattern:\n' +
|
||||||
|
habits.exercise
|
||||||
|
.map(
|
||||||
|
ex =>
|
||||||
|
`- ${ex.frequency} ${ex.activity} sessions (${ex.duration_minutes}min)`
|
||||||
|
)
|
||||||
|
.join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (habits.social?.length) {
|
||||||
|
sections.push(
|
||||||
|
'Social Pattern:\n' +
|
||||||
|
habits.social
|
||||||
|
.map(soc => `- ${soc.frequency} ${soc.activity}`)
|
||||||
|
.join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatContext(context: {
|
||||||
|
stress_triggers: string[];
|
||||||
|
reward_behaviors: string[];
|
||||||
|
upcoming_events: Array<{ name: string; date: string; importance: number }>;
|
||||||
|
}): string {
|
||||||
|
const sections: string[] = [];
|
||||||
|
|
||||||
|
if (context.upcoming_events?.length) {
|
||||||
|
sections.push(
|
||||||
|
'Key Events:\n' +
|
||||||
|
context.upcoming_events
|
||||||
|
.map(event => {
|
||||||
|
const impact =
|
||||||
|
event.importance >= 8
|
||||||
|
? 'significant'
|
||||||
|
: event.importance >= 5
|
||||||
|
? 'moderate'
|
||||||
|
: 'minor';
|
||||||
|
return `- ${event.name} (${event.date}) - Priority: ${event.importance}/10\n Expected spending impact: ${impact}`;
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.stress_triggers?.length) {
|
||||||
|
sections.push(
|
||||||
|
'Stress Factors:\n' +
|
||||||
|
context.stress_triggers.map(trigger => `- ${trigger}`).join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.reward_behaviors?.length) {
|
||||||
|
sections.push(
|
||||||
|
'Reward Activities:\n' +
|
||||||
|
context.reward_behaviors.map(behavior => `- ${behavior}`).join('\n')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPersonaCore(core: Persona['core']): string {
|
||||||
|
const sections = [
|
||||||
|
`${core.name} is a ${core.age}-year-old ${core.occupation.title} at ${core.occupation.location}`,
|
||||||
|
`Living: ${core.household.status}, ${core.home.ownership} ${core.home.type} in ${core.home.location}`,
|
||||||
|
formatHousehold(core.household)
|
||||||
|
];
|
||||||
|
|
||||||
|
return sections.filter(Boolean).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHousehold(household: Persona['core']['household']): string {
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
if (household.members.length) {
|
||||||
|
sections.push(`Household Members: ${household.members.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (household.pets.length) {
|
||||||
|
sections.push(
|
||||||
|
`Pets: ${household.pets.map(pet => `${pet.name} (${pet.type})`).join(', ')}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDailySchedule(
|
||||||
|
weekday: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
activity: string;
|
||||||
|
location: string;
|
||||||
|
duration_minutes: number;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
): string {
|
||||||
|
return Object.entries(weekday)
|
||||||
|
.map(
|
||||||
|
([time, details]) =>
|
||||||
|
`- ${time}: ${details.activity} (${details.location})`
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCommute(commute: Persona['routines']['commute']): string {
|
||||||
|
return `${commute.method} (${commute.route.join(' → ')})
|
||||||
|
Regular stops: ${commute.regular_stops
|
||||||
|
.map(stop => `${stop.frequency} ${stop.purpose} at ${stop.location}`)
|
||||||
|
.join(', ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPurchasingStyle(impulsiveScore: number): string {
|
||||||
|
if (impulsiveScore <= 3) return 'Highly planned, rarely impulsive';
|
||||||
|
if (impulsiveScore <= 5)
|
||||||
|
return 'Mostly planned, occasional impulse purchases';
|
||||||
|
if (impulsiveScore <= 7) return 'Balance of planned and impulse purchases';
|
||||||
|
return 'Frequently makes impulse purchases';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSubscriptions(
|
||||||
|
subscriptions: Persona['finances']['subscriptions']
|
||||||
|
): string {
|
||||||
|
return subscriptions
|
||||||
|
.map(
|
||||||
|
sub =>
|
||||||
|
`- ${sub.name}: €${sub.amount} (${sub.frequency})${sub.auto_payment ? ' [automatic]' : ''}`
|
||||||
|
)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBrands(brands: Persona['preferences']['brands']): string {
|
||||||
|
return brands
|
||||||
|
.map(brand => `${brand.name} (loyalty: ${brand.loyalty_score}/10)`)
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWeekSection(
|
||||||
|
range: { start: Date; end: Date },
|
||||||
|
weekNum: number,
|
||||||
|
persona: Persona
|
||||||
|
): string {
|
||||||
|
return `=== WEEK ${weekNum}: ${range.start.toISOString().split('T')[0]} to ${range.end.toISOString().split('T')[0]} ===
|
||||||
|
|
||||||
|
Context:
|
||||||
|
${formatWeekContext(range, persona)}
|
||||||
|
${formatPurchaseOpportunities(persona)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatWeekContext(
|
||||||
|
range: { start: Date; end: Date },
|
||||||
|
persona: Persona
|
||||||
|
): string {
|
||||||
|
const contexts = [];
|
||||||
|
|
||||||
|
if (range.start.getDate() <= 5) {
|
||||||
|
contexts.push('Post-salary period');
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = persona.context.upcoming_events.filter(event =>
|
||||||
|
isDateInRange(new Date(event.date), range.start, range.end)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (events.length) {
|
||||||
|
contexts.push(`Events: ${events.map(e => e.name).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return contexts.map(c => `- ${c}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPurchaseOpportunities(persona: Persona): string {
|
||||||
|
const opportunities = [];
|
||||||
|
|
||||||
|
if (persona.routines.commute.regular_stops.length) {
|
||||||
|
opportunities.push('Regular purchase points:');
|
||||||
|
persona.routines.commute.regular_stops.forEach(stop => {
|
||||||
|
opportunities.push(
|
||||||
|
`- ${stop.frequency} ${stop.purpose} at ${stop.location}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persona.habits.exercise.length) {
|
||||||
|
opportunities.push('Activity-related purchases:');
|
||||||
|
persona.habits.exercise.forEach(ex => {
|
||||||
|
opportunities.push(`- ${ex.frequency} ${ex.activity} sessions`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persona.habits.social.length) {
|
||||||
|
opportunities.push('Social spending occasions:');
|
||||||
|
persona.habits.social.forEach(soc => {
|
||||||
|
opportunities.push(`- ${soc.frequency} ${soc.activity}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return opportunities.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generatePrompt(
|
||||||
|
persona: Persona,
|
||||||
|
reflectionThreshold: number,
|
||||||
|
targetDate: Date,
|
||||||
|
numWeeks: number
|
||||||
|
): Promise<string> {
|
||||||
|
const weekRanges = getWeekRanges(targetDate, numWeeks);
|
||||||
|
|
||||||
|
return `PERSONA PROFILE:
|
||||||
|
${formatPersonaCore(persona.core)}
|
||||||
|
|
||||||
|
Daily Schedule:
|
||||||
|
${formatDailySchedule(persona.routines.weekday)}
|
||||||
|
Commute: ${formatCommute(persona.routines.commute)}
|
||||||
|
|
||||||
|
Weekend Activities:
|
||||||
|
${persona.routines.weekend.map(activity => `- ${activity}`).join('\n')}
|
||||||
|
|
||||||
|
${formatHabits(persona.habits)}
|
||||||
|
|
||||||
|
Financial Profile:
|
||||||
|
- Income: €${persona.core.occupation.income.toLocaleString()}/year
|
||||||
|
- Payment Methods: ${persona.preferences.payment_methods.join(', ')}
|
||||||
|
- Price Sensitivity: ${persona.preferences.price_sensitivity}/10
|
||||||
|
- Purchasing Style: ${formatPurchasingStyle(persona.finances.spending_patterns.impulsive_score)}
|
||||||
|
|
||||||
|
Monthly Fixed Expenses:
|
||||||
|
${formatSubscriptions(persona.finances.subscriptions)}
|
||||||
|
|
||||||
|
Spending Categories:
|
||||||
|
${formatCategories(persona.finances.spending_patterns.categories)}
|
||||||
|
|
||||||
|
Brand Preferences:
|
||||||
|
${formatBrands(persona.preferences.brands)}
|
||||||
|
|
||||||
|
Dietary Preferences:
|
||||||
|
${persona.preferences.diet.join(', ')}
|
||||||
|
|
||||||
|
${formatContext(persona.context)}
|
||||||
|
|
||||||
|
PURCHASE GENERATION GUIDELINES:
|
||||||
|
|
||||||
|
Generate ${numWeeks} weeks of purchases for ${persona.core.name}:
|
||||||
|
|
||||||
|
${weekRanges
|
||||||
|
.map((range, i) => formatWeekSection(range, i + 1, persona))
|
||||||
|
.join('\n\n')}
|
||||||
|
|
||||||
|
Purchase Format:
|
||||||
|
[DATE] [TIME] | [LOCATION] | [ITEMS] | €[AMOUNT] | [CATEGORY] | [PLANNED/IMPULSE]
|
||||||
|
|
||||||
|
Reflection Guidelines (for purchases over €${reflectionThreshold}):
|
||||||
|
- Consider purchase significance and context
|
||||||
|
- Reference lifestyle impact and usage patterns
|
||||||
|
- Include relation to events or stress factors
|
||||||
|
- Account for social context where relevant
|
||||||
|
|
||||||
|
Reflection Format:
|
||||||
|
[DATE] | Mood: [MOOD] | [REFLECTION] | Satisfaction: [SCORE]/10`;
|
||||||
|
}
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
import { Persona } from '../persona/types';
|
|
||||||
|
|
||||||
function formatFrequencyList(
|
|
||||||
items?: Array<{ name: string; frequency: number }>
|
|
||||||
): string {
|
|
||||||
if (!items?.length) return '(No data available)';
|
|
||||||
return items
|
|
||||||
.sort((a, b) => b.frequency - a.frequency)
|
|
||||||
.map(item => `- ${item.name} (${item.frequency}x per month)`)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatCategories(categories?: {
|
|
||||||
[key: string]: { preference: number; frequency: number };
|
|
||||||
}): string {
|
|
||||||
if (!categories || Object.keys(categories).length === 0)
|
|
||||||
return '(No categories defined)';
|
|
||||||
return Object.entries(categories)
|
|
||||||
.map(([category, data]) => {
|
|
||||||
const weeklyFrequency = Math.round(data.frequency * 4.33); // Monthly to weekly
|
|
||||||
return `- ${category}: ${weeklyFrequency}x per week (preference: ${data.preference}/10)`;
|
|
||||||
})
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatActivities(
|
|
||||||
activities?: Array<{ name: string; frequency: number; schedule?: string[] }>
|
|
||||||
): string {
|
|
||||||
if (!activities?.length) return '(No activities listed)';
|
|
||||||
return activities
|
|
||||||
.map(
|
|
||||||
act =>
|
|
||||||
`- ${act.name}: ${act.frequency}x per ${act.schedule ? act.schedule.join(', ') : 'week'}`
|
|
||||||
)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatList(items?: string[]): string {
|
|
||||||
if (!items?.length) return '(None listed)';
|
|
||||||
return items.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generatePurchasePrompt(
|
|
||||||
persona: Persona,
|
|
||||||
reflectionThreshold: number
|
|
||||||
): Promise<string> {
|
|
||||||
try {
|
|
||||||
const sections: string[] = [];
|
|
||||||
|
|
||||||
sections.push(`PERSONAL PROFILE:
|
|
||||||
Name: ${persona.core.name || 'Unknown'}
|
|
||||||
Age: ${persona.core.age || 'Unknown'}
|
|
||||||
Occupation: ${persona.core.occupation?.title || 'Unknown'}${
|
|
||||||
persona.core.occupation?.level
|
|
||||||
? ` (${persona.core.occupation.level})`
|
|
||||||
: ''
|
|
||||||
}
|
|
||||||
Income: ${persona.core.occupation?.income ? `$${persona.core.occupation.income.toLocaleString()}/year` : 'Unknown'}
|
|
||||||
Location: ${persona.core.home?.location || 'Unknown'}
|
|
||||||
Household: ${persona.core.household?.status || 'Unknown'}${
|
|
||||||
persona.core.household?.pets?.length
|
|
||||||
? `\nPets: ${persona.core.household.pets
|
|
||||||
.map(pet => `${pet.type || 'pet'} named ${pet.name}`)
|
|
||||||
.join(', ')}`
|
|
||||||
: ''
|
|
||||||
}`);
|
|
||||||
|
|
||||||
if (persona.core.occupation?.schedule?.length) {
|
|
||||||
sections.push(
|
|
||||||
`WORK SCHEDULE:\n${persona.core.occupation.schedule.join('\n')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persona.preferences?.shopping) {
|
|
||||||
sections.push(`REGULAR SHOPPING PATTERNS:
|
|
||||||
${persona.preferences.shopping.grocery_stores?.length ? `Grocery Stores:\n${formatFrequencyList(persona.preferences.shopping.grocery_stores)}` : ''}
|
|
||||||
${persona.preferences.shopping.coffee_shops?.length ? `\nCoffee Shops:\n${formatFrequencyList(persona.preferences.shopping.coffee_shops)}` : ''}
|
|
||||||
${persona.preferences.shopping.restaurants?.length ? `\nRestaurants:\n${formatFrequencyList(persona.preferences.shopping.restaurants)}` : ''}
|
|
||||||
${persona.preferences.shopping.retail?.length ? `\nRetail:\n${formatFrequencyList(persona.preferences.shopping.retail)}` : ''}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persona.finances?.spending_patterns?.categories) {
|
|
||||||
sections.push(
|
|
||||||
`SPENDING CATEGORIES & FREQUENCY:\n${formatCategories(persona.finances.spending_patterns.categories)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persona.preferences || persona.finances?.spending_patterns) {
|
|
||||||
sections.push(`PAYMENT PREFERENCES:
|
|
||||||
${persona.preferences?.payment_methods ? `- Methods: ${formatList(persona.preferences.payment_methods)}` : ''}
|
|
||||||
${persona.preferences?.price_sensitivity ? `- Price Sensitivity: ${persona.preferences.price_sensitivity}/10` : ''}
|
|
||||||
${persona.finances?.spending_patterns?.impulsive_score ? `- Impulsiveness Score: ${persona.finances.spending_patterns.impulsive_score}/10` : ''}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persona.routines?.commute?.regular_stops?.length) {
|
|
||||||
sections.push(`REGULAR ROUTINES:
|
|
||||||
Commute Stops:
|
|
||||||
${persona.routines.commute.regular_stops
|
|
||||||
.map(
|
|
||||||
stop => `- ${stop.frequency} visits to ${stop.location} for ${stop.purpose}`
|
|
||||||
)
|
|
||||||
.join('\n')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persona.preferences) {
|
|
||||||
const preferencesSection = [`PREFERENCES:`];
|
|
||||||
if (persona.preferences.diet?.length) {
|
|
||||||
preferencesSection.push(
|
|
||||||
`- Diet: ${formatList(persona.preferences.diet)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (persona.preferences.brands?.length) {
|
|
||||||
preferencesSection.push(
|
|
||||||
`- Favorite Brands: ${persona.preferences.brands
|
|
||||||
.map(b => `${b.name} (loyalty: ${b.loyaltyScore}/10)`)
|
|
||||||
.join(', ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
sections.push(preferencesSection.join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persona.habits) {
|
|
||||||
const activitiesSection = [`REGULAR ACTIVITIES:`];
|
|
||||||
if (persona.habits.exercise?.length) {
|
|
||||||
activitiesSection.push(
|
|
||||||
`Exercise:\n${formatActivities(persona.habits.exercise)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (persona.habits.social?.length) {
|
|
||||||
activitiesSection.push(
|
|
||||||
`\nSocial:\n${formatActivities(persona.habits.social)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (persona.habits.entertainment?.length) {
|
|
||||||
activitiesSection.push(
|
|
||||||
`\nEntertainment:\n${formatActivities(persona.habits.entertainment)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
sections.push(activitiesSection.join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persona.context) {
|
|
||||||
const contextSection = [`CONTEXT:`];
|
|
||||||
if (persona.context.upcoming_events?.length) {
|
|
||||||
contextSection.push(
|
|
||||||
`Upcoming Events:\n${persona.context.upcoming_events
|
|
||||||
.map(
|
|
||||||
event =>
|
|
||||||
`- ${event.name} on ${event.date}${event.details ? `: ${event.details}` : ''}`
|
|
||||||
)
|
|
||||||
.join('\n')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (persona.context.stress_triggers?.length) {
|
|
||||||
contextSection.push(
|
|
||||||
`\nStress Triggers: ${formatList(persona.context.stress_triggers)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (persona.context.reward_behaviors?.length) {
|
|
||||||
contextSection.push(
|
|
||||||
`\nReward Behaviors: ${formatList(persona.context.reward_behaviors)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
sections.push(contextSection.join('\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (persona.finances?.subscriptions?.length) {
|
|
||||||
sections.push(`EXISTING SUBSCRIPTIONS (exclude from weekly purchases):
|
|
||||||
${persona.finances.subscriptions.map(sub => `- ${sub.name}: $${sub.amount} (due: ${sub.date})`).join('\n')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
sections.push(`Please generate a detailed list of purchases for one week, including:
|
|
||||||
1. Date and time of purchase
|
|
||||||
2. Store/vendor name
|
|
||||||
3. Items purchased
|
|
||||||
4. Amount spent
|
|
||||||
5. Category of spending
|
|
||||||
6. Whether it was planned or impulse purchase
|
|
||||||
|
|
||||||
Consider:
|
|
||||||
- Regular commute stops and routines
|
|
||||||
- Exercise and social activities
|
|
||||||
- Dietary preferences and restrictions
|
|
||||||
- Brand loyalties and preferred stores
|
|
||||||
- Work schedule and regular activities
|
|
||||||
- Price sensitivity and impulsiveness score
|
|
||||||
- Upcoming events and potential related purchases
|
|
||||||
${persona.context?.recent_changes?.length ? `- Recent lifestyle changes: ${formatList(persona.context.recent_changes)}` : ''}
|
|
||||||
|
|
||||||
Format each purchase as:
|
|
||||||
[DATE] [TIME] | [STORE] | [ITEMS] | $[AMOUNT] | [CATEGORY] | [PLANNED/IMPULSE]
|
|
||||||
|
|
||||||
|
|
||||||
For purchases over €${reflectionThreshold}, randomly include 0-5 reflections following these rules:
|
|
||||||
|
|
||||||
1. Each reflection must occur AFTER the purchase date
|
|
||||||
2. Reflections must be in chronological order
|
|
||||||
3. First reflection should typically be within a week of purchase
|
|
||||||
4. Space out subsequent reflections realistically (e.g., weeks or months apart)
|
|
||||||
5. No reflection should be dated after ${new Date().toISOString()}
|
|
||||||
|
|
||||||
Each reflection must include:
|
|
||||||
1. A date when the reflection was made
|
|
||||||
2. A personal comment that makes sense for that point in time
|
|
||||||
3. A satisfaction score from 1-10 (10 being extremely satisfied, 1 being completely regretful)
|
|
||||||
4. The persona's mood or context when making the reflection
|
|
||||||
|
|
||||||
Consider how reflection timing affects content:
|
|
||||||
- Immediate reflections (1-7 days): Initial impressions, emotional responses
|
|
||||||
- Short-term reflections (1-4 weeks): Early usage experience, discovering features/issues
|
|
||||||
- Medium-term reflections (1-3 months): More balanced assessment, practical value
|
|
||||||
- Long-term reflections (3+ months): Durability, long-term value, retrospective thoughts
|
|
||||||
|
|
||||||
Factor these into reflections:
|
|
||||||
- How the persona's view typically evolves over time
|
|
||||||
- Seasonal or contextual factors (e.g., using winter clothes in summer)
|
|
||||||
- Financial impact becoming more/less significant over time
|
|
||||||
- Product durability or performance changes
|
|
||||||
- Changes in the persona's life circumstances
|
|
||||||
- Whether the novelty wears off or appreciation grows
|
|
||||||
|
|
||||||
Format each reflection as:
|
|
||||||
[REFLECTION_DATE] | Mood: [MOOD] | [COMMENT] | Satisfaction: [SCORE]/10
|
|
||||||
|
|
||||||
Example of a purchase with reflections:
|
|
||||||
2024-01-15 12:30 | Nike Store | Running Shoes XC90 | $180 | Clothing | PLANNED
|
|
||||||
Reflections:
|
|
||||||
2024-01-16 | Mood: Excited | "First run with these was amazing - the cushioning is perfect for my style" | Satisfaction: 9/10
|
|
||||||
2024-01-30 | Mood: Focused | "After two weeks of regular runs, they're holding up great and no knee pain" | Satisfaction: 8/10
|
|
||||||
2024-03-10 | Mood: Practical | "Three months in, still performing well but showing some wear on the sides" | Satisfaction: 8/10
|
|
||||||
|
|
||||||
Generate purchases that align with the persona's lifestyle, income level, and spending patterns.`);
|
|
||||||
|
|
||||||
return sections.filter(section => section.trim().length > 0).join('\n\n');
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(JSON.stringify(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,28 @@
|
|||||||
import { prisma } from '../utils/prismaClient';
|
|
||||||
import fs, { readFileSync } from 'fs';
|
import fs, { readFileSync } from 'fs';
|
||||||
import { PurchaseList, Reflection } from './types';
|
import { PurchaseList, purchaseListSchema } from './types';
|
||||||
import { Tool } from './tool';
|
import { Tool } from './tool';
|
||||||
import { BaseTool, makeRequest } from '../utils/anthropicClient';
|
import { BaseTool, makeRequest } from '../utils/anthropicClient';
|
||||||
import { createFolderIfNotExists } from '../utils/createFolder';
|
import { createFolderIfNotExists } from '../utils/createFolder';
|
||||||
import { generatePurchasePrompt } from './promptGenerator';
|
import { generatePrompt } from './prompt';
|
||||||
import { Persona } from '../persona/types';
|
import { Persona } from '../persona/types';
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
|
|
||||||
export async function generate(personaId: number) {
|
export async function generate(
|
||||||
|
personaId: string,
|
||||||
|
date: Date,
|
||||||
|
numWeeks: number
|
||||||
|
) {
|
||||||
|
try {
|
||||||
const jsonFile = readFileSync(
|
const jsonFile = readFileSync(
|
||||||
`personas/${personaId}/${personaId}-persona.json`,
|
`personas/${personaId}/${personaId}-persona.json`,
|
||||||
'utf-8'
|
'utf-8'
|
||||||
);
|
);
|
||||||
|
|
||||||
const persona: Persona = JSON.parse(jsonFile);
|
const persona: Persona = JSON.parse(jsonFile);
|
||||||
|
|
||||||
const personaPrompt = await generatePurchasePrompt(
|
const personaPrompt = await generatePrompt(
|
||||||
persona,
|
persona,
|
||||||
parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50')
|
parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50'),
|
||||||
|
date,
|
||||||
|
numWeeks
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = (await makeRequest(
|
const result = (await makeRequest(
|
||||||
@@ -26,49 +30,53 @@ export async function generate(personaId: number) {
|
|||||||
Tool as BaseTool
|
Tool as BaseTool
|
||||||
)) as PurchaseList;
|
)) as PurchaseList;
|
||||||
|
|
||||||
await saveToDb(personaId, result);
|
const validPurchases = purchaseListSchema.safeParse(result);
|
||||||
|
|
||||||
await saveToJson(result, personaId);
|
if (validPurchases.error) {
|
||||||
|
throw Error(`Invalid purchases: ${validPurchases.error.message}`);
|
||||||
console.log(`Generated ${result.items.length} purchases`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reflectionToJson(reflection: Reflection): Prisma.JsonObject {
|
await saveToJson(validPurchases.data, personaId);
|
||||||
return {
|
|
||||||
comment: reflection.comment,
|
const totalPurchases = validPurchases.data.weeks.reduce(
|
||||||
satisfactionScore: reflection.satisfactionScore,
|
(acc, week) => acc + week.purchases.length,
|
||||||
date: reflection.date,
|
0
|
||||||
mood: reflection.mood || null
|
);
|
||||||
};
|
console.log(
|
||||||
|
`Generated ${totalPurchases} purchases for ${persona.core.name}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return validPurchases.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating purchases:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveToDb(personaId: number, purchases: PurchaseList) {
|
export async function saveToJson(purchaseList: PurchaseList, id: string) {
|
||||||
const result = await prisma.item.createMany({
|
|
||||||
data: purchases.items.map(
|
|
||||||
purchase =>
|
|
||||||
({
|
|
||||||
userId: personaId,
|
|
||||||
name: purchase.name,
|
|
||||||
amount: purchase.amount,
|
|
||||||
datetime: purchase.datetime,
|
|
||||||
location: purchase.location,
|
|
||||||
notes: purchase.notes,
|
|
||||||
reflections: purchase.reflections
|
|
||||||
? purchase.reflections.map(reflectionToJson)
|
|
||||||
: Prisma.JsonNull
|
|
||||||
}) satisfies Prisma.ItemCreateManyInput
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Inserted ${result.count} purchases for persona ${personaId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveToJson(purchaseList: PurchaseList, id: number) {
|
|
||||||
await createFolderIfNotExists(`personas/${id}/`);
|
await createFolderIfNotExists(`personas/${id}/`);
|
||||||
|
|
||||||
const jsonName = `personas/${id}/${id}-purchases.json`;
|
const jsonName = `personas/${id}/${id}-purchases.json`;
|
||||||
|
|
||||||
await fs.promises.writeFile(jsonName, JSON.stringify(purchaseList), 'utf8');
|
await fs.promises.writeFile(
|
||||||
|
jsonName,
|
||||||
|
JSON.stringify(purchaseList, null, 2),
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
console.log(`Saved ${purchaseList.items.length} purchases as ${jsonName}`);
|
const purchaseStats = purchaseList.weeks.reduce(
|
||||||
|
(stats, week) => {
|
||||||
|
return {
|
||||||
|
total: stats.total + week.purchases.length,
|
||||||
|
planned: stats.planned + week.purchases.filter(p => p.isPlanned).length,
|
||||||
|
withReflections:
|
||||||
|
stats.withReflections +
|
||||||
|
week.purchases.filter(p => p.reflections?.length).length
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ total: 0, planned: 0, withReflections: 0 }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Saved ${purchaseStats.total} purchases (${purchaseStats.planned} planned, ${purchaseStats.withReflections} with reflections) as ${jsonName}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
export const Tool = {
|
export const Tool = {
|
||||||
name: 'PurchaseSchema' as const,
|
name: 'PurchasesSchema' as const,
|
||||||
input_schema: {
|
input_schema: {
|
||||||
type: 'object' as const,
|
type: 'object' as const,
|
||||||
properties: {
|
properties: {
|
||||||
|
weeks: {
|
||||||
|
type: 'array' as const,
|
||||||
items: {
|
items: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
weekNumber: {
|
||||||
|
type: 'number' as const,
|
||||||
|
description: 'Sequential week number starting from 1'
|
||||||
|
},
|
||||||
|
startDate: {
|
||||||
|
type: 'string' as const,
|
||||||
|
description: 'Start date of the week in ISO 8601 format',
|
||||||
|
format: 'date'
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
type: 'string' as const,
|
||||||
|
description: 'End date of the week in ISO 8601 format',
|
||||||
|
format: 'date'
|
||||||
|
},
|
||||||
|
purchases: {
|
||||||
type: 'array' as const,
|
type: 'array' as const,
|
||||||
items: {
|
items: {
|
||||||
type: 'object' as const,
|
type: 'object' as const,
|
||||||
properties: {
|
properties: {
|
||||||
name: {
|
name: {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
description: 'Name of the purchased item'
|
description: 'Name of the purchased item or service'
|
||||||
},
|
},
|
||||||
amount: {
|
amount: {
|
||||||
type: 'number' as const,
|
type: 'number' as const,
|
||||||
@@ -25,14 +44,22 @@ export const Tool = {
|
|||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
description: 'Purchase location'
|
description: 'Purchase location'
|
||||||
},
|
},
|
||||||
notes: {
|
category: {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
description: 'Additional purchase details (optional)'
|
description:
|
||||||
|
'Spending category (must match persona preferences)'
|
||||||
|
},
|
||||||
|
isPlanned: {
|
||||||
|
type: 'boolean' as const,
|
||||||
|
description: 'Whether the purchase was planned or impulse'
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
type: 'string' as const,
|
||||||
|
description: 'Optional context about purchase circumstances'
|
||||||
},
|
},
|
||||||
reflections: {
|
reflections: {
|
||||||
type: 'array' as const,
|
type: 'array' as const,
|
||||||
description:
|
description: 'Reflections for purchases over threshold',
|
||||||
'Array of reflections on purchases over threshold amount',
|
|
||||||
items: {
|
items: {
|
||||||
type: 'object' as const,
|
type: 'object' as const,
|
||||||
properties: {
|
properties: {
|
||||||
@@ -48,22 +75,67 @@ export const Tool = {
|
|||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
description: 'Date of the reflection in ISO 8601 format',
|
description: 'Reflection date in ISO 8601 format',
|
||||||
format: 'date-time'
|
format: 'date-time'
|
||||||
},
|
},
|
||||||
mood: {
|
mood: {
|
||||||
type: 'string' as const,
|
type: 'string' as const,
|
||||||
description: 'Optional context about mood during reflection'
|
description: 'Mood during reflection'
|
||||||
|
},
|
||||||
|
relatedTo: {
|
||||||
|
type: 'string' as const,
|
||||||
|
description: 'Optional related event or context'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ['comment', 'satisfactionScore', 'date'] as const
|
required: [
|
||||||
|
'comment',
|
||||||
|
'satisfactionScore',
|
||||||
|
'date',
|
||||||
|
'mood'
|
||||||
|
] as const
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ['name', 'amount', 'datetime', 'location'] as const
|
required: [
|
||||||
|
'name',
|
||||||
|
'amount',
|
||||||
|
'datetime',
|
||||||
|
'location',
|
||||||
|
'category',
|
||||||
|
'isPlanned'
|
||||||
|
] as const
|
||||||
|
},
|
||||||
|
minItems: 12,
|
||||||
|
maxItems: 20,
|
||||||
|
description: 'List of purchases for this week'
|
||||||
|
},
|
||||||
|
weekContext: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
events: {
|
||||||
|
type: 'array' as const,
|
||||||
|
items: {
|
||||||
|
type: 'string' as const
|
||||||
|
},
|
||||||
|
description: 'Notable events during this week'
|
||||||
|
},
|
||||||
|
stressLevel: {
|
||||||
|
type: 'number' as const,
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 10,
|
||||||
|
description: 'Overall stress level for the week'
|
||||||
|
},
|
||||||
|
notes: {
|
||||||
|
type: 'string' as const,
|
||||||
|
description: 'Additional context about the week'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ['items'] as const
|
required: ['weekNumber', 'startDate', 'endDate', 'purchases'] as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['weeks'] as const
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,26 +1,80 @@
|
|||||||
export interface Reflection {
|
import { z } from 'zod';
|
||||||
comment: string;
|
|
||||||
satisfactionScore: number;
|
|
||||||
date: string;
|
|
||||||
mood?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Purchase {
|
const isoDateTimeString = z.string().refine(
|
||||||
name: string;
|
value => {
|
||||||
amount: number;
|
try {
|
||||||
datetime: string;
|
return !isNaN(new Date(value).getTime());
|
||||||
location: string;
|
} catch {
|
||||||
notes?: string;
|
return false;
|
||||||
reflections?: Reflection[];
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{ message: 'Invalid ISO 8601 datetime format' }
|
||||||
|
);
|
||||||
|
|
||||||
export interface PurchaseList {
|
const reflectionSchema = z.object({
|
||||||
items: Purchase[];
|
comment: z.string().min(1, 'Comment is required'),
|
||||||
}
|
satisfactionScore: z.number().min(1).max(10),
|
||||||
|
date: isoDateTimeString,
|
||||||
|
mood: z.string().nullable().optional(),
|
||||||
|
relatedTo: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
export interface ReflectionJson {
|
const weekContextSchema = z.object({
|
||||||
comment: string;
|
events: z.array(z.string()).optional(),
|
||||||
satisfactionScore: number;
|
stressLevel: z.number().min(1).max(10).optional(),
|
||||||
date: string;
|
notes: z.string().optional()
|
||||||
mood: string | null;
|
});
|
||||||
|
|
||||||
|
const purchaseSchema = z.object({
|
||||||
|
name: z.string().min(1, 'Name is required'),
|
||||||
|
amount: z.number().positive('Amount must be positive'),
|
||||||
|
datetime: isoDateTimeString,
|
||||||
|
location: z.string().min(1, 'Location is required'),
|
||||||
|
category: z.string().min(1, 'Category is required'),
|
||||||
|
isPlanned: z.boolean(),
|
||||||
|
context: z.string().optional(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
reflections: z.array(reflectionSchema).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const weekSchema = z
|
||||||
|
.object({
|
||||||
|
weekNumber: z.number().positive().int(),
|
||||||
|
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format'),
|
||||||
|
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format'),
|
||||||
|
purchases: z
|
||||||
|
.array(purchaseSchema)
|
||||||
|
.min(12, 'Minimum 12 purchases required per week')
|
||||||
|
.max(20, 'Maximum 20 purchases allowed per week'),
|
||||||
|
weekContext: weekContextSchema.optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
data => {
|
||||||
|
const start = new Date(data.startDate);
|
||||||
|
const end = new Date(data.endDate);
|
||||||
|
return start <= end;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'End date must be after or equal to start date',
|
||||||
|
path: ['endDate']
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const purchaseListSchema = z
|
||||||
|
.object({
|
||||||
|
weeks: z.array(weekSchema).min(1, 'At least one week is required')
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
data => {
|
||||||
|
const weekNumbers = data.weeks
|
||||||
|
.map(w => w.weekNumber)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
return weekNumbers.every((num, idx) => num === idx + 1);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'Week numbers must be sequential starting from 1',
|
||||||
|
path: ['weeks']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type PurchaseList = z.infer<typeof purchaseListSchema>;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export async function makeRequest<T extends BaseTool>(prompt: string, tool: T) {
|
|||||||
apiKey: process.env.ANTHROPIC_API_KEY
|
apiKey: process.env.ANTHROPIC_API_KEY
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await anthropic.messages.create({
|
const response = await anthropic.messages.create({
|
||||||
model: 'claude-3-5-sonnet-20241022',
|
model: 'claude-3-5-sonnet-20241022',
|
||||||
max_tokens: 8192,
|
max_tokens: 8192,
|
||||||
@@ -42,4 +43,7 @@ export async function makeRequest<T extends BaseTool>(prompt: string, tool: T) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return content[1].input;
|
return content[1].input;
|
||||||
|
} catch (error) {
|
||||||
|
throw Error('Anthropic client error.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/utils/dateFunctions.ts
Normal file
33
src/utils/dateFunctions.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export function getWeekRanges(
|
||||||
|
targetDate: Date,
|
||||||
|
numWeeks: number
|
||||||
|
): Array<{ start: Date; end: Date }> {
|
||||||
|
if (numWeeks < 1 || numWeeks > 8) {
|
||||||
|
throw new Error('Number of weeks must be between 1 and 8');
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranges = [];
|
||||||
|
const firstDay = new Date(targetDate);
|
||||||
|
firstDay.setUTCHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const dayOfWeek = firstDay.getUTCDay();
|
||||||
|
const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
||||||
|
firstDay.setUTCDate(firstDay.getUTCDate() + diff);
|
||||||
|
|
||||||
|
for (let i = 0; i < numWeeks; i++) {
|
||||||
|
const weekStart = new Date(firstDay);
|
||||||
|
weekStart.setUTCDate(weekStart.getUTCDate() + i * 7);
|
||||||
|
|
||||||
|
const weekEnd = new Date(weekStart);
|
||||||
|
weekEnd.setUTCDate(weekEnd.getUTCDate() + 6);
|
||||||
|
weekEnd.setUTCHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
ranges.push({ start: weekStart, end: weekEnd });
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDateInRange(date: Date, start: Date, end: Date): boolean {
|
||||||
|
return date >= start && date <= end;
|
||||||
|
}
|
||||||
@@ -102,20 +102,10 @@ function formatPersonaSeed(
|
|||||||
return `${letters}:${year}:${postalCode}`;
|
return `${letters}:${year}:${postalCode}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function generatePersonaSeed(): string {
|
export function generatePersonaSeed(): string {
|
||||||
const letters = generateLetters();
|
const letters = generateLetters();
|
||||||
const birthYear = generateBirthYear();
|
const birthYear = generateBirthYear();
|
||||||
const postalCode = generateRandomCAP();
|
const postalCode = generateRandomCAP();
|
||||||
|
|
||||||
return formatPersonaSeed(letters, birthYear, postalCode);
|
return formatPersonaSeed(letters, birthYear, postalCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generatePrompt(): string {
|
|
||||||
const seed = generatePersonaSeed();
|
|
||||||
const [letters, year, postalCode] = seed.split(':');
|
|
||||||
|
|
||||||
return `Using the Italian persona seed ${seed}, create a detailed persona of an Italian individual where:
|
|
||||||
- The letters "${letters}" MUST ALL be included in the person's full name (first name + last name), though the name can contain additional letters
|
|
||||||
- The person was born in ${year}
|
|
||||||
- They live in an area with postal code (CAP) ${postalCode}.`;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
|
|
||||||
export const prisma = new PrismaClient();
|
|
||||||
87
yarn.lock
87
yarn.lock
@@ -402,64 +402,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@prisma/client@npm:^5.22.0":
|
|
||||||
version: 5.22.0
|
|
||||||
resolution: "@prisma/client@npm:5.22.0"
|
|
||||||
peerDependencies:
|
|
||||||
prisma: "*"
|
|
||||||
peerDependenciesMeta:
|
|
||||||
prisma:
|
|
||||||
optional: true
|
|
||||||
checksum: 10/991d7410ded2d6c824303ac222ae94df4f1bf42240dda97c710cfee3a3bc59fef65c73e1ee55177e38d12f4d7dfc0a047748f98b4e3026dddd65f462684cd2d1
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@prisma/debug@npm:5.22.0":
|
|
||||||
version: 5.22.0
|
|
||||||
resolution: "@prisma/debug@npm:5.22.0"
|
|
||||||
checksum: 10/e4c425fde57f83c1a3df44d3f9088684a3e1d764022d2378f981209e57b7f421bc773d7f4fc23daa3936aa3e3772fa99fef81d2f2ec9673269f06efe822e3b53
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@prisma/engines-version@npm:5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2":
|
|
||||||
version: 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2
|
|
||||||
resolution: "@prisma/engines-version@npm:5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
|
|
||||||
checksum: 10/e9e3563d75482591ca482dfc26afaf8a727796f48bfcc9b550652ca9c66176f5841944d00f8a4cb60a7fd58b117d8e28b1c6b84fc7316123738f7290c91c291d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@prisma/engines@npm:5.22.0":
|
|
||||||
version: 5.22.0
|
|
||||||
resolution: "@prisma/engines@npm:5.22.0"
|
|
||||||
dependencies:
|
|
||||||
"@prisma/debug": "npm:5.22.0"
|
|
||||||
"@prisma/engines-version": "npm:5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
|
|
||||||
"@prisma/fetch-engine": "npm:5.22.0"
|
|
||||||
"@prisma/get-platform": "npm:5.22.0"
|
|
||||||
checksum: 10/887381d8be0acc713159ef70cdc5c4b2ca5af6f3a5d2f6ae73b63c6cf31b74e5c41a9c2dbccb02b1a49bbda19a3d0a75cd1bcc86d76e98991c3a3978e61b622b
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@prisma/fetch-engine@npm:5.22.0":
|
|
||||||
version: 5.22.0
|
|
||||||
resolution: "@prisma/fetch-engine@npm:5.22.0"
|
|
||||||
dependencies:
|
|
||||||
"@prisma/debug": "npm:5.22.0"
|
|
||||||
"@prisma/engines-version": "npm:5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2"
|
|
||||||
"@prisma/get-platform": "npm:5.22.0"
|
|
||||||
checksum: 10/512ce722e582a72429c90d438a63c730b230faa1235300215ab6cd1c5a0dfb2aa63704a66ee72c57222d9c094477ea198b1c154b9779dcab78ebf36e09f79161
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@prisma/get-platform@npm:5.22.0":
|
|
||||||
version: 5.22.0
|
|
||||||
resolution: "@prisma/get-platform@npm:5.22.0"
|
|
||||||
dependencies:
|
|
||||||
"@prisma/debug": "npm:5.22.0"
|
|
||||||
checksum: 10/15db9f38933a59a1b0f3a1d6284a50261803677516d6eedec2f7caaa1767c1c6e94e6465aca9239766b1cc363002476da21f7eeab55cc5329937d6d6d57a5f67
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@tsconfig/node10@npm:^1.0.7":
|
"@tsconfig/node10@npm:^1.0.7":
|
||||||
version: 1.0.11
|
version: 1.0.11
|
||||||
resolution: "@tsconfig/node10@npm:1.0.11"
|
resolution: "@tsconfig/node10@npm:1.0.11"
|
||||||
@@ -2085,7 +2027,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"fsevents@npm:2.3.3, fsevents@npm:~2.3.2":
|
"fsevents@npm:~2.3.2":
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
resolution: "fsevents@npm:2.3.3"
|
resolution: "fsevents@npm:2.3.3"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2095,7 +2037,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>":
|
"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin<compat/fsevents>":
|
||||||
version: 2.3.3
|
version: 2.3.3
|
||||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3664,21 +3606,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"prisma@npm:^5.8.0":
|
|
||||||
version: 5.22.0
|
|
||||||
resolution: "prisma@npm:5.22.0"
|
|
||||||
dependencies:
|
|
||||||
"@prisma/engines": "npm:5.22.0"
|
|
||||||
fsevents: "npm:2.3.3"
|
|
||||||
dependenciesMeta:
|
|
||||||
fsevents:
|
|
||||||
optional: true
|
|
||||||
bin:
|
|
||||||
prisma: build/index.js
|
|
||||||
checksum: 10/7d7a47491b6b5fb6fe23358af4ab36c1b85e837bf439c6994cfb20e98627dd15e7bc9e1be40f9620170fc587e6d0b5c6b871f44e5a380b15b7b684eb89e21037
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0":
|
"proc-log@npm:^4.1.0, proc-log@npm:^4.2.0":
|
||||||
version: 4.2.0
|
version: 4.2.0
|
||||||
resolution: "proc-log@npm:4.2.0"
|
resolution: "proc-log@npm:4.2.0"
|
||||||
@@ -3727,7 +3654,6 @@ __metadata:
|
|||||||
"@anthropic-ai/sdk": "npm:^0.32.1"
|
"@anthropic-ai/sdk": "npm:^0.32.1"
|
||||||
"@commitlint/cli": "npm:^18.4.3"
|
"@commitlint/cli": "npm:^18.4.3"
|
||||||
"@commitlint/config-conventional": "npm:^18.4.3"
|
"@commitlint/config-conventional": "npm:^18.4.3"
|
||||||
"@prisma/client": "npm:^5.22.0"
|
|
||||||
"@types/express": "npm:^4.17.21"
|
"@types/express": "npm:^4.17.21"
|
||||||
"@types/node": "npm:^20.10.0"
|
"@types/node": "npm:^20.10.0"
|
||||||
"@typescript-eslint/eslint-plugin": "npm:^6.12.0"
|
"@typescript-eslint/eslint-plugin": "npm:^6.12.0"
|
||||||
@@ -3742,9 +3668,9 @@ __metadata:
|
|||||||
lint-staged: "npm:^15.1.0"
|
lint-staged: "npm:^15.1.0"
|
||||||
nodemon: "npm:^3.0.2"
|
nodemon: "npm:^3.0.2"
|
||||||
prettier: "npm:^3.1.0"
|
prettier: "npm:^3.1.0"
|
||||||
prisma: "npm:^5.8.0"
|
|
||||||
ts-node: "npm:^10.9.2"
|
ts-node: "npm:^10.9.2"
|
||||||
typescript: "npm:^5.3.0"
|
typescript: "npm:^5.3.0"
|
||||||
|
zod: "npm:^3.23.8"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -4829,3 +4755,10 @@ __metadata:
|
|||||||
checksum: 10/f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
|
checksum: 10/f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"zod@npm:^3.23.8":
|
||||||
|
version: 3.23.8
|
||||||
|
resolution: "zod@npm:3.23.8"
|
||||||
|
checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|||||||
Reference in New Issue
Block a user