diff --git a/src/lib/services/ai/sdk/schemas/cardimport.ts b/src/lib/services/ai/sdk/schemas/cardimport.ts new file mode 100644 index 0000000..feec8fb --- /dev/null +++ b/src/lib/services/ai/sdk/schemas/cardimport.ts @@ -0,0 +1,44 @@ +/** + * Card Import Schemas + * + * Zod schemas for SillyTavern character card import operations. + */ + +import { z } from 'zod'; + +/** + * NPC extracted from character card. + * Maps to the character-card-import template output. + */ +export const cardImportNpcSchema = z.object({ + name: z.string().describe("Character's actual name"), + role: z.string().describe("Character's role (ally, mentor, antagonist, love interest, guide, friend)"), + description: z.string().describe("1-2 sentences: who they are and key appearance details"), + personality: z.string().describe("Key personality traits as comma-separated list"), + relationship: z.string().describe("Their relationship to the protagonist"), +}); + +/** + * Result from character-card-import template. + * Cleans SillyTavern cards and converts to Aventura scenario settings. + */ +export const cardImportResultSchema = z.object({ + primaryCharacterName: z.string().describe("The ACTUAL name of the main character that {{char}} refers to"), + settingSeed: z.string().describe("The FULL cleaned text with {{char}} replaced, {{user}} kept as-is"), + npcs: z.array(cardImportNpcSchema).describe("All significant characters from the card"), +}); + +/** + * Result from vault-character-import template. + * Extracts clean character data for the vault. + */ +export const vaultCharacterImportSchema = z.object({ + name: z.string().describe("The character's actual name"), + description: z.string().describe("1-2 paragraphs describing who this character is"), + traits: z.array(z.string()).describe("3-8 personality traits"), + visualDescriptors: z.array(z.string()).describe("Physical appearance details for image generation"), +}); + +export type CardImportNpc = z.infer; +export type CardImportResult = z.infer; +export type VaultCharacterImport = z.infer; diff --git a/src/lib/services/ai/sdk/schemas/lorebook.ts b/src/lib/services/ai/sdk/schemas/lorebook.ts index e805c84..13f5c65 100644 --- a/src/lib/services/ai/sdk/schemas/lorebook.ts +++ b/src/lib/services/ai/sdk/schemas/lorebook.ts @@ -120,3 +120,20 @@ export const finishLoreManagementSchema = z.object({ }); export type FinishLoreManagementSchema = z.infer; + +/** + * Classification result for a single entry. + */ +export const entryClassificationSchema = z.object({ + index: z.number().describe('Index of the entry being classified'), + type: entryTypeSchema.describe('The classified type for this entry'), +}); + +/** + * Result from lorebook-classifier template. + * Array of entry classifications. + */ +export const lorebookClassificationResultSchema = z.array(entryClassificationSchema); + +export type EntryClassification = z.infer; +export type LorebookClassificationResult = z.infer; diff --git a/src/lib/services/characterCardImporter.ts b/src/lib/services/characterCardImporter.ts index 00e1417..482a695 100644 --- a/src/lib/services/characterCardImporter.ts +++ b/src/lib/services/characterCardImporter.ts @@ -3,15 +3,16 @@ * * Imports SillyTavern character cards (V1/V2 JSON format) into Aventura's wizard. * Supports both JSON files and PNG files with embedded character data. - * - * STATUS: PARTIALLY STUBBED - Awaiting SDK migration - * - PNG/JSON parsing: WORKING - * - LLM conversion: STUBBED */ -import type { StoryMode } from '$lib/types'; +import type { StoryMode, VisualDescriptors } from '$lib/types'; import type { Genre, GeneratedCharacter } from '$lib/services/ai/wizard/ScenarioService'; -import { settings } from '$lib/stores/settings.svelte'; +import { promptService, type PromptContext } from '$lib/services/prompts'; +import { generateStructured } from './ai/sdk/generate'; +import { + cardImportResultSchema, + vaultCharacterImportSchema, +} from './ai/sdk/schemas/cardimport'; import { createLogger } from './ai/core/config'; const log = createLogger('CharacterCardImporter'); @@ -262,7 +263,6 @@ function buildCardContext(card: ParsedCard): string { /** * Convert a parsed character card into a scenario setting using LLM. - * @throws Error - LLM conversion not implemented during SDK migration */ export async function convertCardToScenario( jsonString: string, @@ -290,20 +290,51 @@ export async function convertCardToScenario( const preprocessedFirstMessage = normalizeUserMacro(card.firstMessage); const preprocessedAlternateGreetings = card.alternateGreetings.map(g => normalizeUserMacro(g)); - // Return a basic fallback without LLM conversion - const fallbackDescription = normalizeUserMacro( - [card.scenario, card.description].filter(s => s.trim()).join('\n\n') - ); + // Build card content for LLM + const cardContent = buildCardContext(card); + + // Minimal context for prompt rendering + const promptContext: PromptContext = { + mode, + pov: 'second', + tense: 'present', + protagonistName: '', + }; + + const system = promptService.renderPrompt('character-card-import', promptContext); + const prompt = promptService.renderUserPrompt('character-card-import', promptContext, { + genre, + title: cardTitle, + cardContent, + }); + + const result = await generateStructured({ + presetId: 'classification', + schema: cardImportResultSchema, + system, + prompt, + }); + + // Convert LLM result to CardImportResult format + const npcs: GeneratedCharacter[] = result.npcs.map(npc => ({ + name: npc.name, + role: npc.role, + description: npc.description, + relationship: npc.relationship, + traits: npc.personality.split(',').map(t => t.trim()).filter(Boolean), + })); + + log('Card import successful', { primaryCharacter: result.primaryCharacterName, npcCount: npcs.length }); return { - success: false, - settingSeed: fallbackDescription.slice(0, 2000), - npcs: [], - primaryCharacterName: cardTitle, - storyTitle: cardTitle, + success: true, + settingSeed: result.settingSeed, + npcs, + primaryCharacterName: result.primaryCharacterName, + storyTitle: result.primaryCharacterName || cardTitle, firstMessage: preprocessedFirstMessage, alternateGreetings: preprocessedAlternateGreetings, - errors: ['LLM conversion not implemented - awaiting SDK migration. Basic extraction was used as fallback.'], + errors: [], }; } @@ -311,12 +342,11 @@ export interface SanitizedCharacter { name: string; description: string; traits: string[]; - visualDescriptors: import('$lib/types').VisualDescriptors; + visualDescriptors: VisualDescriptors; } /** * Sanitize a character card using LLM to extract clean character data. - * @throws Error - LLM sanitization not implemented during SDK migration */ export async function sanitizeCharacterCard( jsonString: string, @@ -328,13 +358,41 @@ export async function sanitizeCharacterCard( return null; } - log('Sanitization not implemented - returning basic extraction'); + // Build card content for LLM + const cardContent = buildCardContext(card); + + // Minimal context for prompt rendering + const promptContext: PromptContext = { + mode: 'adventure', + pov: 'second', + tense: 'present', + protagonistName: '', + }; + + const system = promptService.renderPrompt('vault-character-import', promptContext); + const prompt = promptService.renderUserPrompt('vault-character-import', promptContext, { + cardContent, + }); + + const result = await generateStructured({ + presetId: 'classification', + schema: vaultCharacterImportSchema, + system, + prompt, + }); + + log('Character sanitization successful', { name: result.name }); + + // Convert array of visual descriptors to VisualDescriptors object + const visualDescriptors: VisualDescriptors = {}; + result.visualDescriptors.forEach((desc, i) => { + visualDescriptors[`descriptor_${i}`] = desc; + }); - // Return basic extraction without LLM return { - name: card.name, - description: card.description || card.personality || '', - traits: [], - visualDescriptors: {}, + name: result.name, + description: result.description, + traits: result.traits, + visualDescriptors, }; } diff --git a/src/lib/services/lorebookImporter.ts b/src/lib/services/lorebookImporter.ts index e1eb088..f7fda09 100644 --- a/src/lib/services/lorebookImporter.ts +++ b/src/lib/services/lorebookImporter.ts @@ -2,15 +2,13 @@ * Lorebook Importer Service * * Imports lorebooks from various formats (primarily SillyTavern) into Aventura's Entry system. - * - * STATUS: PARTIALLY STUBBED - Awaiting SDK migration - * - Parsing and basic type inference: WORKING - * - LLM classification: STUBBED */ import type { Entry, EntryType, EntryInjectionMode, EntryCreator } from '$lib/types'; -import { settings } from '$lib/stores/settings.svelte'; import type { StoryMode } from '$lib/services/prompts'; +import { promptService, type PromptContext } from '$lib/services/prompts'; +import { generateStructured } from './ai/sdk/generate'; +import { lorebookClassificationResultSchema } from './ai/sdk/schemas/lorebook'; import { createLogger } from './ai/core/config'; const log = createLogger('LorebookImporter'); @@ -171,7 +169,7 @@ function inferEntryType(name: string, content: string): EntryType { /** * LLM-based entry type classification. - * @throws Error - LLM classification not implemented during SDK migration + * Classifies entries in batches to avoid token limits. */ export async function classifyEntriesWithLLM( entries: ImportedEntry[], @@ -180,14 +178,67 @@ export async function classifyEntriesWithLLM( ): Promise { if (entries.length === 0) return entries; - log('LLM classification not implemented - using keyword-based inference'); + const BATCH_SIZE = 20; // Process in batches to avoid token limits + const result = [...entries]; + let classified = 0; - // Return entries as-is with keyword-based types (already set during parsing) - if (onProgress) { - onProgress(entries.length, entries.length); + // Minimal context for prompt rendering + const promptContext: PromptContext = { + mode, + pov: 'second', + tense: 'present', + protagonistName: '', + }; + + const system = promptService.renderPrompt('lorebook-classifier', promptContext); + + // Process in batches + for (let i = 0; i < entries.length; i += BATCH_SIZE) { + const batch = entries.slice(i, i + BATCH_SIZE); + + // Build entries JSON for the prompt + const entriesJson = JSON.stringify( + batch.map((entry, batchIndex) => ({ + index: batchIndex, + name: entry.name, + content: entry.description.slice(0, 500), // Limit content length + keywords: entry.keywords.slice(0, 10), + })), + null, + 2 + ); + + const prompt = promptService.renderUserPrompt('lorebook-classifier', promptContext, { + entriesJson, + }); + + const classifications = await generateStructured({ + presetId: 'classification', + schema: lorebookClassificationResultSchema, + system, + prompt, + }); + + // Apply classifications to batch + for (const classification of classifications) { + const globalIndex = i + classification.index; + if (globalIndex < result.length) { + result[globalIndex] = { + ...result[globalIndex], + type: classification.type as EntryType, + }; + } + } + + classified += batch.length; + if (onProgress) { + onProgress(classified, entries.length); + } + + log('Classified batch', { batch: i / BATCH_SIZE + 1, classified, total: entries.length }); } - return entries; + return result; } function determineInjectionMode(entry: SillyTavernEntry): EntryInjectionMode { diff --git a/src/lib/services/prompts/definitions.ts b/src/lib/services/prompts/definitions.ts index 602ffdf..76ad7b3 100644 --- a/src/lib/services/prompts/definitions.ts +++ b/src/lib/services/prompts/definitions.ts @@ -2746,34 +2746,6 @@ Respond with ONLY the translated text, no explanations.`, userContent: `{{content}}`, }; -// ============================================================================ -// AGENTIC RETRY PROMPTS -// ============================================================================ - -/** - * Prompt sent when the agentic retrieval agent doesn't make tool calls. - * Used to encourage the model to continue using tools or finish. - */ -const agenticRetrievalRetryTemplate: PromptTemplate = { - id: 'agentic-retrieval-retry', - name: 'Agentic Retrieval Retry', - category: 'service', - description: 'Prompt sent when agentic retrieval agent stops calling tools', - content: 'Please use the available tools to gather relevant context, or call finish_retrieval when you are done.', -}; - -/** - * Prompt sent when the lore management agent doesn't make tool calls. - * Used to encourage the model to continue using tools or finish. - */ -const loreManagementRetryTemplate: PromptTemplate = { - id: 'lore-management-retry', - name: 'Lore Management Retry', - category: 'service', - description: 'Prompt sent when lore management agent stops calling tools', - content: 'Please use the available tools to make any necessary changes, or call finish_lore_management if you are done reviewing the lorebook.', -}; - // ============================================================================ // COMBINED PROMPT TEMPLATES // ============================================================================ @@ -2796,8 +2768,6 @@ export const PROMPT_TEMPLATES: PromptTemplate[] = [ loreManagementPromptTemplate, interactiveLorebookPromptTemplate, agenticRetrievalPromptTemplate, - agenticRetrievalRetryTemplate, - loreManagementRetryTemplate, characterCardImportPromptTemplate, vaultCharacterImportPromptTemplate, imagePromptAnalysisTemplate,