feat: general improvements

This commit is contained in:
2024-11-30 16:01:33 +01:00
parent 8e7bfd2048
commit b248ee80ee
24 changed files with 870 additions and 705 deletions

View File

@@ -1,4 +1,3 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5433/persona?schema=public&connect_timeout=300"
PERSONA_PROMPT=
PURCHASE_REFLECTION_THRESHOLD=50
ANTHROPIC_API_KEY=

View File

@@ -11,9 +11,7 @@ A TypeScript application that leverages the Anthropic Claude API to generate rea
- Financial patterns and spending habits
- Contextual behaviors and upcoming events
- Create realistic weekly purchase histories that match persona profiles
- Store generated data in:
- PostgreSQL database for structured querying
- JSON files for easy data portability
- Store generated data in JSON files for easy data portability
## 🚀 Getting Started
@@ -29,13 +27,7 @@ yarn install
cp .env.example .env
```
3. Initialize the database:
```bash
yarn migrate
```
4. Build and start the application:
3. Build and start the application:
```bash
yarn build
@@ -56,8 +48,6 @@ yarn dev
- `yarn lint` - Run ESLint with automatic fixes
- `yarn format` - Format code using Prettier
- `yarn typecheck` - Check TypeScript types
- `yarn generate` - Generate Prisma client
- `yarn migrate` - Run database migrations
## ⚠️ Disclaimer

View File

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

View File

@@ -1,9 +1,9 @@
{
"name": "purchases-personas",
"version": "1.0.0",
"version": "1.1.0",
"description": "Generate realistic fictional personas and their weekly purchase behaviors using AI",
"scripts": {
"start": "prisma generate && node dist/index.js",
"start": "node dist/index.js",
"dev": "nodemon src/index.ts",
"build": "tsc",
"lint": "eslint . --fix",
@@ -16,10 +16,10 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@prisma/client": "^5.22.0",
"crypto": "^1.0.1",
"dotenv": "^16.4.5",
"express": "^4.21.1"
"express": "^4.21.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@commitlint/cli": "^18.4.3",
@@ -35,7 +35,6 @@
"lint-staged": "^15.1.0",
"nodemon": "^3.0.2",
"prettier": "^3.1.0",
"prisma": "^5.8.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.0"
}

View File

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

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "items" ADD COLUMN "reflections" JSONB;

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "items" ALTER COLUMN "reflections" SET DEFAULT 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,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")
}

View File

@@ -4,12 +4,15 @@ import { generate as generatePurchases } from './purchase/store';
const personaPromise = generatePersona();
const date = new Date();
const numWeeks = 4;
console.log(`Generating persona...`);
personaPromise.then(id => {
console.log(`Generating purchases for id ${id}...`);
const purchasesPromise = generatePurchases(id);
const purchasesPromise = generatePurchases(id, date, numWeeks);
purchasesPromise.then(() => {
console.log('Complete');

74
src/persona/prompt.ts Normal file
View 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.`;
}

View File

@@ -1,45 +1,33 @@
import 'dotenv/config';
import { prisma } from '../utils/prismaClient';
import fs from 'fs';
import { Persona } from './types';
import { Persona, personaSchema } from './types';
import { Tool } from './tool';
import { BaseTool, makeRequest } from '../utils/anthropicClient';
import { createFolderIfNotExists } from '../utils/createFolder';
import { generatePrompt } from '../utils/generatePersonaSeed';
import { generatePrompt } from './prompt';
import { randomUUID } from 'crypto';
export async function generate() {
if (!process.env.PERSONA_PROMPT) {
throw Error('Persona prompt missing.');
}
const prompt = `${generatePrompt()} ${process.env.PERSONA_PROMPT}`;
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;
}
export async function saveToDb(persona: Persona) {
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) {
export async function saveToJson(persona: Persona, id: string) {
await createFolderIfNotExists(`personas/${id}/`);
const jsonName = `personas/${id}/${id}-persona.json`;

View File

@@ -226,13 +226,13 @@ export const Tool = {
properties: {
subscriptions: {
type: 'array' as const,
description: 'Regular subscriptions',
description: 'Regular subscriptions and fixed expenses',
items: {
type: 'object' as const,
properties: {
name: {
type: 'string' as const,
description: 'Subscription name'
description: 'Subscription or expense name'
},
amount: {
type: 'number' as const,
@@ -246,8 +246,40 @@ export const Tool = {
type: 'string' as const,
description: 'Next payment 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: {

View File

@@ -1,118 +1,138 @@
interface Pet {
[key: string]: unknown;
}
import { z } from 'zod';
interface FrequencyObject {
name: string;
frequency: number;
}
const petSchema = z.object({
type: z.string(),
name: z.string()
});
interface SubscriptionBill {
name: string;
amount: number;
date: string | Date;
}
const regularStopSchema = z.object({
location: z.string(),
purpose: z.string(),
frequency: z.string()
});
interface ActivityObject {
name: string;
frequency: number;
schedule?: string[];
}
const weekdayActivitySchema = z.object({
activity: z.string(),
location: z.string(),
duration_minutes: z.number()
});
interface BrandLoyalty {
name: string;
loyaltyScore: number;
}
const brandPreferenceSchema = z.object({
name: z.string(),
loyalty_score: z.number().min(1).max(10)
});
interface Event {
name: string;
date: string | Date;
details?: string;
}
const subscriptionSchema = z.object({
name: z.string(),
amount: z.number(),
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 {
activity: string;
duration: string;
location?: string;
}
const spendingCategorySchema = z.object({
preference_score: z.number().min(1).max(10),
monthly_budget: z.number()
});
interface RegularStop {
location: string;
purpose: string;
frequency: string;
}
const exerciseActivitySchema = z.object({
activity: z.string(),
frequency: z.string(),
duration_minutes: z.number()
});
interface SpendingCategories {
[category: string]: {
preference: number;
frequency: number;
};
}
export type ExerciseActivity = z.infer<typeof exerciseActivitySchema>;
export interface Persona {
core: {
age: number;
name: string;
occupation: {
title: string;
level: string;
income: number;
location: string;
schedule: string[];
};
home: {
type: string;
ownership: string;
location: string;
commute_distance_km: number;
};
household: {
status: string;
members: string[];
pets: Pet[];
};
};
routines: {
weekday: {
[hour: string]: TimelineActivity;
};
weekend: ActivityObject[];
commute: {
method: string;
route: string[];
regular_stops: RegularStop[];
};
};
preferences: {
diet: string[];
brands: BrandLoyalty[];
price_sensitivity: number;
payment_methods: string[];
shopping: {
grocery_stores: FrequencyObject[];
coffee_shops: FrequencyObject[];
restaurants: FrequencyObject[];
retail: FrequencyObject[];
};
};
finances: {
subscriptions: SubscriptionBill[];
regular_bills: SubscriptionBill[];
spending_patterns: {
impulsive_score: number;
categories: SpendingCategories;
};
};
habits: {
exercise: ActivityObject[];
social: ActivityObject[];
entertainment: ActivityObject[];
vices: ActivityObject[];
};
context: {
stress_triggers: string[];
reward_behaviors: string[];
upcoming_events: Event[];
recent_changes: string[];
};
}
const socialActivitySchema = z.object({
activity: z.string(),
frequency: z.string()
});
export type SocialActivity = z.infer<typeof socialActivitySchema>;
const upcomingEventSchema = z.object({
name: z.string(),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
importance: z.number().min(1).max(10)
});
const coreSchema = z.object({
age: z.number(),
name: z.string(),
occupation: z.object({
title: z.string(),
level: z.string(),
income: z.number(),
location: z.string(),
schedule: z.array(z.string())
}),
home: z.object({
type: z.string(),
ownership: z.string(),
location: z.string(),
commute_distance_km: z.number()
}),
household: z.object({
status: z.string(),
members: z.array(z.string()),
pets: z.array(petSchema)
})
});
const routinesSchema = z.object({
weekday: z.record(weekdayActivitySchema),
weekend: z.array(z.string()),
commute: z.object({
method: z.string(),
route: z.array(z.string()),
regular_stops: z.array(regularStopSchema)
})
});
const preferencesSchema = z.object({
diet: z.array(z.string()),
brands: z.array(brandPreferenceSchema),
price_sensitivity: z.number().min(1).max(10),
payment_methods: z.array(z.string())
});
const financesSchema = z.object({
subscriptions: z.array(subscriptionSchema),
spending_patterns: z.object({
impulsive_score: z.number().min(1).max(10),
categories: z.record(spendingCategorySchema)
})
});
const habitsSchema = z.object({
exercise: z.array(exerciseActivitySchema),
social: z.array(socialActivitySchema)
});
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
View 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`;
}

View File

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

View File

@@ -1,24 +1,28 @@
import { prisma } from '../utils/prismaClient';
import fs, { readFileSync } from 'fs';
import { PurchaseList, Reflection } from './types';
import { PurchaseList, purchaseListSchema } from './types';
import { Tool } from './tool';
import { BaseTool, makeRequest } from '../utils/anthropicClient';
import { createFolderIfNotExists } from '../utils/createFolder';
import { generatePurchasePrompt } from './promptGenerator';
import { generatePrompt } from './prompt';
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(
`personas/${personaId}/${personaId}-persona.json`,
'utf-8'
);
const persona: Persona = JSON.parse(jsonFile);
const personaPrompt = await generatePurchasePrompt(
const personaPrompt = await generatePrompt(
persona,
parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50')
parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50'),
date,
numWeeks
);
const result = (await makeRequest(
@@ -26,49 +30,53 @@ export async function generate(personaId: number) {
Tool as BaseTool
)) as PurchaseList;
await saveToDb(personaId, result);
const validPurchases = purchaseListSchema.safeParse(result);
await saveToJson(result, personaId);
console.log(`Generated ${result.items.length} purchases`);
if (validPurchases.error) {
throw Error(`Invalid purchases: ${validPurchases.error.message}`);
}
function reflectionToJson(reflection: Reflection): Prisma.JsonObject {
return {
comment: reflection.comment,
satisfactionScore: reflection.satisfactionScore,
date: reflection.date,
mood: reflection.mood || null
};
await saveToJson(validPurchases.data, personaId);
const totalPurchases = validPurchases.data.weeks.reduce(
(acc, week) => acc + week.purchases.length,
0
);
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) {
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) {
export async function saveToJson(purchaseList: PurchaseList, id: string) {
await createFolderIfNotExists(`personas/${id}/`);
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}`
);
}

View File

@@ -1,16 +1,35 @@
export const Tool = {
name: 'PurchaseSchema' as const,
name: 'PurchasesSchema' as const,
input_schema: {
type: 'object' as const,
properties: {
weeks: {
type: 'array' as const,
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,
items: {
type: 'object' as const,
properties: {
name: {
type: 'string' as const,
description: 'Name of the purchased item'
description: 'Name of the purchased item or service'
},
amount: {
type: 'number' as const,
@@ -25,14 +44,22 @@ export const Tool = {
type: 'string' as const,
description: 'Purchase location'
},
notes: {
category: {
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: {
type: 'array' as const,
description:
'Array of reflections on purchases over threshold amount',
description: 'Reflections for purchases over threshold',
items: {
type: 'object' as const,
properties: {
@@ -48,22 +75,67 @@ export const Tool = {
},
date: {
type: 'string' as const,
description: 'Date of the reflection in ISO 8601 format',
description: 'Reflection date in ISO 8601 format',
format: 'date-time'
},
mood: {
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;

View File

@@ -1,26 +1,80 @@
export interface Reflection {
comment: string;
satisfactionScore: number;
date: string;
mood?: string | null;
}
import { z } from 'zod';
export interface Purchase {
name: string;
amount: number;
datetime: string;
location: string;
notes?: string;
reflections?: Reflection[];
const isoDateTimeString = z.string().refine(
value => {
try {
return !isNaN(new Date(value).getTime());
} catch {
return false;
}
},
{ message: 'Invalid ISO 8601 datetime format' }
);
export interface PurchaseList {
items: Purchase[];
}
const reflectionSchema = z.object({
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 {
comment: string;
satisfactionScore: number;
date: string;
mood: string | null;
const weekContextSchema = z.object({
events: z.array(z.string()).optional(),
stressLevel: z.number().min(1).max(10).optional(),
notes: z.string().optional()
});
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>;

View File

@@ -20,6 +20,7 @@ export async function makeRequest<T extends BaseTool>(prompt: string, tool: T) {
apiKey: process.env.ANTHROPIC_API_KEY
});
try {
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 8192,
@@ -42,4 +43,7 @@ export async function makeRequest<T extends BaseTool>(prompt: string, tool: T) {
];
return content[1].input;
} catch (error) {
throw Error('Anthropic client error.');
}
}

View 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;
}

View File

@@ -102,20 +102,10 @@ function formatPersonaSeed(
return `${letters}:${year}:${postalCode}`;
}
function generatePersonaSeed(): string {
export function generatePersonaSeed(): string {
const letters = generateLetters();
const birthYear = generateBirthYear();
const postalCode = generateRandomCAP();
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}.`;
}

View File

@@ -1,3 +0,0 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

View File

@@ -402,64 +402,6 @@ __metadata:
languageName: node
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":
version: 1.0.11
resolution: "@tsconfig/node10@npm:1.0.11"
@@ -2085,7 +2027,7 @@ __metadata:
languageName: node
linkType: hard
"fsevents@npm:2.3.3, fsevents@npm:~2.3.2":
"fsevents@npm:~2.3.2":
version: 2.3.3
resolution: "fsevents@npm:2.3.3"
dependencies:
@@ -2095,7 +2037,7 @@ __metadata:
languageName: node
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
resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin<compat/fsevents>::version=2.3.3&hash=df0bf1"
dependencies:
@@ -3664,21 +3606,6 @@ __metadata:
languageName: node
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":
version: 4.2.0
resolution: "proc-log@npm:4.2.0"
@@ -3727,7 +3654,6 @@ __metadata:
"@anthropic-ai/sdk": "npm:^0.32.1"
"@commitlint/cli": "npm:^18.4.3"
"@commitlint/config-conventional": "npm:^18.4.3"
"@prisma/client": "npm:^5.22.0"
"@types/express": "npm:^4.17.21"
"@types/node": "npm:^20.10.0"
"@typescript-eslint/eslint-plugin": "npm:^6.12.0"
@@ -3742,9 +3668,9 @@ __metadata:
lint-staged: "npm:^15.1.0"
nodemon: "npm:^3.0.2"
prettier: "npm:^3.1.0"
prisma: "npm:^5.8.0"
ts-node: "npm:^10.9.2"
typescript: "npm:^5.3.0"
zod: "npm:^3.23.8"
languageName: unknown
linkType: soft
@@ -4829,3 +4755,10 @@ __metadata:
checksum: 10/f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700
languageName: node
linkType: hard
"zod@npm:^3.23.8":
version: 3.23.8
resolution: "zod@npm:3.23.8"
checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1
languageName: node
linkType: hard