diff --git a/README.md b/README.md index 64a86f5..99a9f35 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The project is framework-first rather than product-first. The current core libra - provider-agnostic text, structured-output, image, conversation-memory, and special-date adapter contracts - ready-made xAI Grok text, structured-output, and image adapters - one IdentityDB memory space per persona -- persona initialization from personality, history, values, preferences, and relationships +- persona initialization from a long freeform persona seed string, with optional supplemental structured hints - LLM-generated biography ingestion into IdentityDB fact drafts - optional profile image generation through an image adapter - schedule generation for day/week/month scopes with optional external special-date context diff --git a/docs/plans/2026-05-11-boxbrain-foundation.md b/docs/plans/2026-05-11-boxbrain-foundation.md index 85a153a..6212e4d 100644 --- a/docs/plans/2026-05-11-boxbrain-foundation.md +++ b/docs/plans/2026-05-11-boxbrain-foundation.md @@ -14,7 +14,7 @@ BoxBrain models one persona as a durable memory space plus a runtime harness: -- `initializePersona(input)` creates a persona, asks an LLM adapter to generate a detailed life story from personality/history/values/likes/dislikes/relationships, asks another extraction path to split that story into IdentityDB facts, stores every fact in the persona's IdentityDB space, and optionally generates a profile image. +- `initializePersona(input)` creates a persona, prefers a long freeform persona seed string as the caller input, asks an LLM adapter to generate a detailed life story from that seed plus any supplemental structured hints, asks another extraction path to split that story into IdentityDB facts, stores every fact in the persona's IdentityDB space, and optionally generates a profile image. - `generateSchedule(input)` creates a month/week/day schedule for a persona around a date, stores schedule facts keyed by time topics, and derives contact availability windows from the schedule. - `setAvailability(input)` explicitly sets or updates contact availability: `online`, `do_not_disturb`, or `offline`. - `sendMessage(input)` handles a user text turn by loading mandatory memories, delegating contextual memory search to an LLM, then asking the persona LLM to emit one or more short DM messages through a tool-like output contract. @@ -145,7 +145,7 @@ BoxBrain models one persona as a durable memory space plus a runtime harness: **Behavior to test first:** - Initialization creates a stable persona object with `id`, `spaceName`, and profile fields. -- Biography adapter is called with personality, history, values, preferences, relationships, and current date context. +- Biography adapter is called with the freeform `seedText` when provided, plus any supplemental structured hints and current date context. - Fact splitter adapter output is stored in the persona's space. - Profile image adapter is not called unless requested. - Profile image result is returned and stored as a fact when requested. diff --git a/src/persona.ts b/src/persona.ts index 8651e9d..596b2ee 100644 --- a/src/persona.ts +++ b/src/persona.ts @@ -14,7 +14,8 @@ export interface InitializePersonaInput { id?: string | undefined; displayName: string; spaceName?: string | undefined; - personality: string; + seedText?: string | undefined; + personality?: string | undefined; history?: string | undefined; values?: string[] | undefined; likes?: string[] | undefined; @@ -74,6 +75,8 @@ const PERSONA_FACT_EXTRACTION_SCHEMA = { } as const; export async function initializePersona(db: IdentityDB, input: InitializePersonaInput): Promise { + assertPersonaInitializationInput(input); + const id = input.id ?? createPersonaId(input.displayName); const spaceName = input.spaceName ?? `persona:${id}`; const existingSpace = await db.getSpaceByName(spaceName); @@ -175,10 +178,11 @@ export async function initializePersona(db: IdentityDB, input: InitializePersona function buildBiographyPrompt(input: InitializePersonaInput): string { return [ 'Create a concrete, detailed life biography for a synthetic persona from birth to now.', - 'The biography must include personality, history, values, preferences, dislikes, and relationships when provided.', + biographySourceInstruction(input), `Current date: ${input.currentDate ?? new Date().toISOString().slice(0, 10)}`, `Display name: ${input.displayName}`, - `Personality: ${input.personality}`, + freeformSeedLine(input.seedText), + optionalLine('Personality', input.personality), optionalLine('History', input.history), listLine('Values', input.values), listLine('Likes', input.likes), @@ -255,6 +259,36 @@ function assertPersonaFactExtractionResult(value: PersonaFactExtractionResult): return value; } +function assertPersonaInitializationInput(input: InitializePersonaInput): void { + const hasSeedText = typeof input.seedText === 'string' && input.seedText.trim().length > 0; + const hasStructuredSeed = [ + input.personality, + input.history, + ...(input.values ?? []), + ...(input.likes ?? []), + ...(input.dislikes ?? []), + ...((input.relationships ?? []).map((relationship) => relationship.name)), + ].some((value) => typeof value === 'string' && value.trim().length > 0); + + if (!hasSeedText && !hasStructuredSeed) { + throw new Error( + 'initializePersona requires either seedText or at least one structured persona hint such as personality, history, values, likes, dislikes, or relationships.', + ); + } +} + +function biographySourceInstruction(input: InitializePersonaInput): string { + if (input.seedText && input.seedText.trim().length > 0) { + return 'Use the freeform persona seed as the primary source and infer personality, history, values, likes, dislikes, and relationships from it without contradicting the provided details.'; + } + + return 'The biography must include personality, history, values, preferences, dislikes, and relationships when provided.'; +} + +function freeformSeedLine(seedText: string | undefined): string | undefined { + return seedText && seedText.trim().length > 0 ? `Freeform persona seed:\n${seedText}` : undefined; +} + function optionalLine(label: string, value: string | undefined): string | undefined { return value ? `${label}: ${value}` : undefined; } diff --git a/tests/persona.test.ts b/tests/persona.test.ts index 9f9224f..619f326 100644 --- a/tests/persona.test.ts +++ b/tests/persona.test.ts @@ -53,6 +53,32 @@ describe('initializePersona', () => { expect(facts.map((fact) => fact.statement)).toContain('Minji grew up near the sea.'); }); + it('accepts a single freeform persona seed string and forwards it to the biography generation step', async () => { + const db = await createDb(); + const prompts: string[] = []; + const structured = createStructuredAdapter(prompts); + const seedText = + 'Mina is 29, grew up in Busan, moved to Seoul for product design work, values loyalty and quiet consistency, loves indie music and late-night walks, dislikes loud restaurants, and is very close to her older brother Jisoo.'; + + const persona = await initializePersona(db, { + id: 'mina', + displayName: 'Mina', + seedText, + currentDate: '2026-05-11', + structuredModel: structured, + }); + + expect(persona).toMatchObject({ + id: 'mina', + displayName: 'Mina', + spaceName: 'persona:mina', + }); + expect(prompts[0]).toContain('Freeform persona seed'); + expect(prompts[0]).toContain(seedText); + expect(prompts[0]).toContain('infer personality, history, values, likes, dislikes, and relationships'); + expect(prompts[0]).toContain('Current date: 2026-05-11'); + }); + it('does not call the image adapter unless a profile image is requested', async () => { const db = await createDb(); const structured = createStructuredAdapter([]);