From b248ee80ee01ba558f97b78f2948480be3fb8d2c Mon Sep 17 00:00:00 2001 From: Riccardo Senica Date: Sat, 30 Nov 2024 16:01:33 +0100 Subject: [PATCH] feat: general improvements --- .env.example | 1 - README.md | 14 +- docker-compose.yml | 17 - package.json | 9 +- .../20241123210705_init/migration.sql | 29 -- .../migration.sql | 2 - .../migration.sql | 2 - prisma/migrations/migration_lock.toml | 3 - prisma/schema.prisma | 36 --- src/index.ts | 5 +- src/persona/prompt.ts | 74 +++++ src/persona/store.ts | 38 +-- src/persona/tool.ts | 38 ++- src/persona/types.ts | 238 +++++++------- src/purchase/prompt.ts | 296 ++++++++++++++++++ src/purchase/promptGenerator.ts | 238 -------------- src/purchase/store.ts | 118 +++---- src/purchase/tool.ts | 138 ++++++-- src/purchase/types.ts | 100 ++++-- src/utils/anthropicClient.ts | 44 +-- src/utils/dateFunctions.ts | 33 ++ src/utils/generatePersonaSeed.ts | 12 +- src/utils/prismaClient.ts | 3 - yarn.lock | 87 +---- 24 files changed, 870 insertions(+), 705 deletions(-) delete mode 100644 docker-compose.yml delete mode 100644 prisma/migrations/20241123210705_init/migration.sql delete mode 100644 prisma/migrations/20241124140321_add_purchase_reflections/migration.sql delete mode 100644 prisma/migrations/20241124141114_purchase_reflections_default_null/migration.sql delete mode 100644 prisma/migrations/migration_lock.toml delete mode 100644 prisma/schema.prisma create mode 100644 src/persona/prompt.ts create mode 100644 src/purchase/prompt.ts delete mode 100644 src/purchase/promptGenerator.ts create mode 100644 src/utils/dateFunctions.ts delete mode 100644 src/utils/prismaClient.ts diff --git a/.env.example b/.env.example index f5c12de..2242f36 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/README.md b/README.md index ccf1303..b0f9a2b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index ec5110c..0000000 --- a/docker-compose.yml +++ /dev/null @@ -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: diff --git a/package.json b/package.json index 4c03add..34a781c 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/prisma/migrations/20241123210705_init/migration.sql b/prisma/migrations/20241123210705_init/migration.sql deleted file mode 100644 index e2d3196..0000000 --- a/prisma/migrations/20241123210705_init/migration.sql +++ /dev/null @@ -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; diff --git a/prisma/migrations/20241124140321_add_purchase_reflections/migration.sql b/prisma/migrations/20241124140321_add_purchase_reflections/migration.sql deleted file mode 100644 index 4f0b823..0000000 --- a/prisma/migrations/20241124140321_add_purchase_reflections/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "items" ADD COLUMN "reflections" JSONB; diff --git a/prisma/migrations/20241124141114_purchase_reflections_default_null/migration.sql b/prisma/migrations/20241124141114_purchase_reflections_default_null/migration.sql deleted file mode 100644 index 56e4899..0000000 --- a/prisma/migrations/20241124141114_purchase_reflections_default_null/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "items" ALTER COLUMN "reflections" SET DEFAULT null; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/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" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 0e4a918..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -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") -} diff --git a/src/index.ts b/src/index.ts index 68544ed..233668a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'); diff --git a/src/persona/prompt.ts b/src/persona/prompt.ts new file mode 100644 index 0000000..b0263da --- /dev/null +++ b/src/persona/prompt.ts @@ -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: + + + ${letters} + ${year} + ${postalCode} + + + 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 , 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 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.`; +} diff --git a/src/persona/store.ts b/src/persona/store.ts index 6034a88..f628864 100644 --- a/src/persona/store.ts +++ b/src/persona/store.ts @@ -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`; diff --git a/src/persona/tool.ts b/src/persona/tool.ts index 216dfef..7ad06d6 100644 --- a/src/persona/tool.ts +++ b/src/persona/tool.ts @@ -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: { diff --git a/src/persona/types.ts b/src/persona/types.ts index f566980..dbf3bd5 100644 --- a/src/persona/types.ts +++ b/src/persona/types.ts @@ -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; -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; + +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; diff --git a/src/purchase/prompt.ts b/src/purchase/prompt.ts new file mode 100644 index 0000000..7c200f8 --- /dev/null +++ b/src/purchase/prompt.ts @@ -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 { + 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`; +} diff --git a/src/purchase/promptGenerator.ts b/src/purchase/promptGenerator.ts deleted file mode 100644 index 408e317..0000000 --- a/src/purchase/promptGenerator.ts +++ /dev/null @@ -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 { - 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)); - } -} diff --git a/src/purchase/store.ts b/src/purchase/store.ts index c5f59bb..66d6c51 100644 --- a/src/purchase/store.ts +++ b/src/purchase/store.ts @@ -1,74 +1,82 @@ -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) { - const jsonFile = readFileSync( - `personas/${personaId}/${personaId}-persona.json`, - 'utf-8' - ); +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 persona: Persona = JSON.parse(jsonFile); + const personaPrompt = await generatePrompt( + persona, + parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50'), + date, + numWeeks + ); - const personaPrompt = await generatePurchasePrompt( - persona, - parseInt(process.env.PURCHASE_REFLECTION_THRESHOLD ?? '50') - ); + const result = (await makeRequest( + personaPrompt, + Tool as BaseTool + )) as PurchaseList; - const result = (await makeRequest( - personaPrompt, - Tool as BaseTool - )) as PurchaseList; + const validPurchases = purchaseListSchema.safeParse(result); - await saveToDb(personaId, result); + if (validPurchases.error) { + throw Error(`Invalid purchases: ${validPurchases.error.message}`); + } - await saveToJson(result, personaId); + await saveToJson(validPurchases.data, personaId); - console.log(`Generated ${result.items.length} purchases`); + 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; + } } -function reflectionToJson(reflection: Reflection): Prisma.JsonObject { - return { - comment: reflection.comment, - satisfactionScore: reflection.satisfactionScore, - date: reflection.date, - mood: reflection.mood || null - }; -} - -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}` + ); } diff --git a/src/purchase/tool.ts b/src/purchase/tool.ts index 6b35a15..2eb6013 100644 --- a/src/purchase/tool.ts +++ b/src/purchase/tool.ts @@ -1,69 +1,141 @@ export const Tool = { - name: 'PurchaseSchema' as const, + name: 'PurchasesSchema' as const, input_schema: { type: 'object' as const, properties: { - items: { + weeks: { type: 'array' as const, items: { type: 'object' as const, properties: { - name: { - type: 'string' as const, - description: 'Name of the purchased item' - }, - amount: { + weekNumber: { type: 'number' as const, - description: 'Purchase amount in EUR' + description: 'Sequential week number starting from 1' }, - datetime: { + startDate: { type: 'string' as const, - description: 'Purchase date and time in ISO 8601 format', - format: 'date-time' + description: 'Start date of the week in ISO 8601 format', + format: 'date' }, - location: { + endDate: { type: 'string' as const, - description: 'Purchase location' + description: 'End date of the week in ISO 8601 format', + format: 'date' }, - notes: { - type: 'string' as const, - description: 'Additional purchase details (optional)' - }, - reflections: { + purchases: { type: 'array' as const, - description: - 'Array of reflections on purchases over threshold amount', items: { type: 'object' as const, properties: { - comment: { + name: { type: 'string' as const, - description: 'Reflective comment about the purchase' + description: 'Name of the purchased item or service' }, - satisfactionScore: { + amount: { type: 'number' as const, - description: 'Purchase satisfaction score (1-10)', - minimum: 1, - maximum: 10 + description: 'Purchase amount in EUR' }, - date: { + datetime: { type: 'string' as const, - description: 'Date of the reflection in ISO 8601 format', + description: 'Purchase date and time in ISO 8601 format', format: 'date-time' }, - mood: { + location: { type: 'string' as const, - description: 'Optional context about mood during reflection' + description: 'Purchase location' + }, + category: { + type: 'string' as const, + 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: 'Reflections for purchases over threshold', + items: { + type: 'object' as const, + properties: { + comment: { + type: 'string' as const, + description: 'Reflective comment about the purchase' + }, + satisfactionScore: { + type: 'number' as const, + description: 'Purchase satisfaction score (1-10)', + minimum: 1, + maximum: 10 + }, + date: { + type: 'string' as const, + description: 'Reflection date in ISO 8601 format', + format: 'date-time' + }, + mood: { + type: 'string' as const, + description: 'Mood during reflection' + }, + relatedTo: { + type: 'string' as const, + description: 'Optional related event or context' + } + }, + required: [ + 'comment', + 'satisfactionScore', + 'date', + 'mood' + ] as const + } } }, - required: ['comment', 'satisfactionScore', 'date'] 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: ['name', 'amount', 'datetime', 'location'] as const + required: ['weekNumber', 'startDate', 'endDate', 'purchases'] as const } } }, - required: ['items'] as const + required: ['weeks'] as const } } as const; diff --git a/src/purchase/types.ts b/src/purchase/types.ts index 1196de2..899f90a 100644 --- a/src/purchase/types.ts +++ b/src/purchase/types.ts @@ -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; diff --git a/src/utils/anthropicClient.ts b/src/utils/anthropicClient.ts index 5e63bef..5afbb48 100644 --- a/src/utils/anthropicClient.ts +++ b/src/utils/anthropicClient.ts @@ -20,26 +20,30 @@ export async function makeRequest(prompt: string, tool: T) { apiKey: process.env.ANTHROPIC_API_KEY }); - const response = await anthropic.messages.create({ - model: 'claude-3-5-sonnet-20241022', - max_tokens: 8192, - temperature: 1, - tools: [tool], - messages: [{ role: 'user', content: prompt }] - }); + try { + const response = await anthropic.messages.create({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 8192, + temperature: 1, + tools: [tool], + messages: [{ role: 'user', content: prompt }] + }); - if (response.stop_reason && response.stop_reason !== 'tool_use') { - throw Error(JSON.stringify(response)); + if (response.stop_reason && response.stop_reason !== 'tool_use') { + throw Error(JSON.stringify(response)); + } + + if (response.content.length != 2) { + throw Error(JSON.stringify(response)); + } + + const content = response.content as [ + { type: string; text: string }, + { type: string; input: object } + ]; + + return content[1].input; + } catch (error) { + throw Error('Anthropic client error.'); } - - if (response.content.length != 2) { - throw Error(JSON.stringify(response)); - } - - const content = response.content as [ - { type: string; text: string }, - { type: string; input: object } - ]; - - return content[1].input; } diff --git a/src/utils/dateFunctions.ts b/src/utils/dateFunctions.ts new file mode 100644 index 0000000..e5f5d69 --- /dev/null +++ b/src/utils/dateFunctions.ts @@ -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; +} diff --git a/src/utils/generatePersonaSeed.ts b/src/utils/generatePersonaSeed.ts index 464cb12..c1b7cb9 100644 --- a/src/utils/generatePersonaSeed.ts +++ b/src/utils/generatePersonaSeed.ts @@ -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}.`; -} diff --git a/src/utils/prismaClient.ts b/src/utils/prismaClient.ts deleted file mode 100644 index 9b6c4ce..0000000 --- a/src/utils/prismaClient.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { PrismaClient } from '@prisma/client'; - -export const prisma = new PrismaClient(); diff --git a/yarn.lock b/yarn.lock index 1c3bdb4..6473b84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::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