feat(#82): add prompt formatter service to personality module
- Add PromptFormatterService for formatting system prompts based on personality - Support context variable interpolation (userName, workspaceName, etc.) - Add formality level modifiers (VERY_CASUAL to VERY_FORMAL) - Add template validation for custom variables - Add preview endpoint for formatted prompts - Fix UpdatePersonalityDto to avoid @nestjs/mapped-types dependency - Update PersonalitiesController with new endpoints - Add comprehensive tests (33 passing tests) Closes #82
This commit is contained in:
137
apps/api/src/personalities/services/prompt-formatter.service.ts
Normal file
137
apps/api/src/personalities/services/prompt-formatter.service.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { FormalityLevel } from "@prisma/client";
|
||||
import { Personality } from "../entities/personality.entity";
|
||||
|
||||
export interface PromptContext {
|
||||
userName?: string;
|
||||
workspaceName?: string;
|
||||
currentDate?: string;
|
||||
currentTime?: string;
|
||||
timezone?: string;
|
||||
custom?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface FormattedPrompt {
|
||||
systemPrompt: string;
|
||||
metadata: {
|
||||
personalityId: string;
|
||||
personalityName: string;
|
||||
tone: string;
|
||||
formalityLevel: FormalityLevel;
|
||||
formattedAt: Date;
|
||||
};
|
||||
}
|
||||
|
||||
const FORMALITY_MODIFIERS: Record<FormalityLevel, string> = {
|
||||
VERY_CASUAL: "Be extremely relaxed and friendly. Use casual language, contractions, and even emojis when appropriate.",
|
||||
CASUAL: "Be friendly and approachable. Use conversational language and a warm tone.",
|
||||
NEUTRAL: "Be professional yet approachable. Balance formality with friendliness.",
|
||||
FORMAL: "Be professional and respectful. Use proper grammar and formal language.",
|
||||
VERY_FORMAL: "Be highly professional and formal. Use precise language and maintain a respectful, business-like demeanor.",
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PromptFormatterService {
|
||||
formatPrompt(personality: Personality, context?: PromptContext): FormattedPrompt {
|
||||
let prompt = personality.systemPromptTemplate;
|
||||
prompt = this.interpolateVariables(prompt, context);
|
||||
|
||||
if (!prompt.toLowerCase().includes("formality") && !prompt.toLowerCase().includes(personality.formalityLevel.toLowerCase())) {
|
||||
const modifier = FORMALITY_MODIFIERS[personality.formalityLevel];
|
||||
prompt = `${prompt}\n\n${modifier}`;
|
||||
}
|
||||
|
||||
if (!prompt.toLowerCase().includes(personality.tone.toLowerCase())) {
|
||||
prompt = `${prompt}\n\nMaintain a ${personality.tone} tone throughout the conversation.`;
|
||||
}
|
||||
|
||||
return {
|
||||
systemPrompt: prompt.trim(),
|
||||
metadata: {
|
||||
personalityId: personality.id,
|
||||
personalityName: personality.name,
|
||||
tone: personality.tone,
|
||||
formalityLevel: personality.formalityLevel,
|
||||
formattedAt: new Date(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
buildSystemPrompt(
|
||||
personality: Personality,
|
||||
context?: PromptContext,
|
||||
options?: { includeDateTime?: boolean; additionalInstructions?: string },
|
||||
): string {
|
||||
const { systemPrompt } = this.formatPrompt(personality, context);
|
||||
const parts: string[] = [systemPrompt];
|
||||
|
||||
if (options?.includeDateTime === true) {
|
||||
const now = new Date();
|
||||
const dateStr = context?.currentDate ?? now.toISOString().split("T")[0];
|
||||
const timeStr = context?.currentTime ?? now.toTimeString().slice(0, 5);
|
||||
const tzStr = context?.timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
parts.push(`Current date: ${dateStr}, Time: ${timeStr} (${tzStr})`);
|
||||
}
|
||||
|
||||
if (options?.additionalInstructions !== undefined && options.additionalInstructions.length > 0) {
|
||||
parts.push(options.additionalInstructions);
|
||||
}
|
||||
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
|
||||
private interpolateVariables(template: string, context?: PromptContext): string {
|
||||
if (context === undefined) {
|
||||
return template;
|
||||
}
|
||||
|
||||
let result = template;
|
||||
|
||||
if (context.userName !== undefined) {
|
||||
result = result.replace(/\{\{userName\}\}/g, context.userName);
|
||||
}
|
||||
if (context.workspaceName !== undefined) {
|
||||
result = result.replace(/\{\{workspaceName\}\}/g, context.workspaceName);
|
||||
}
|
||||
if (context.currentDate !== undefined) {
|
||||
result = result.replace(/\{\{currentDate\}\}/g, context.currentDate);
|
||||
}
|
||||
if (context.currentTime !== undefined) {
|
||||
result = result.replace(/\{\{currentTime\}\}/g, context.currentTime);
|
||||
}
|
||||
if (context.timezone !== undefined) {
|
||||
result = result.replace(/\{\{timezone\}\}/g, context.timezone);
|
||||
}
|
||||
|
||||
if (context.custom !== undefined) {
|
||||
for (const [key, value] of Object.entries(context.custom)) {
|
||||
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
||||
result = result.replace(regex, value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
validateTemplate(template: string): { valid: boolean; missingVariables: string[] } {
|
||||
const variablePattern = /\{\{(\w+)\}\}/g;
|
||||
const matches = template.matchAll(variablePattern);
|
||||
const variables = Array.from(matches, (m) => m[1]);
|
||||
|
||||
const allowedVariables = new Set(["userName", "workspaceName", "currentDate", "currentTime", "timezone"]);
|
||||
|
||||
const unknownVariables = variables.filter((v) => !allowedVariables.has(v) && !v.startsWith("custom_"));
|
||||
|
||||
return {
|
||||
valid: unknownVariables.length === 0,
|
||||
missingVariables: unknownVariables,
|
||||
};
|
||||
}
|
||||
|
||||
getFormalityLevels(): Array<{ level: FormalityLevel; description: string }> {
|
||||
return Object.entries(FORMALITY_MODIFIERS).map(([level, description]) => ({
|
||||
level: level as FormalityLevel,
|
||||
description,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user