Fixes CI pipeline failures caused by missing Prisma Client generation and TypeScript type safety issues. Added Prisma generation step to CI pipeline, installed missing type dependencies, and resolved 40+ exactOptionalPropertyTypes violations across service layer. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
162 lines
5.1 KiB
TypeScript
162 lines
5.1 KiB
TypeScript
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: string = context?.currentDate ?? now.toISOString().split("T")[0] ?? "";
|
|
const timeStr: string = context?.currentTime ?? now.toTimeString().slice(0, 5);
|
|
const tzStr: string = 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)) {
|
|
// Dynamic regex for template replacement - key is from trusted source (our code)
|
|
// eslint-disable-next-line security/detect-non-literal-regexp
|
|
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: string[] = [];
|
|
for (const match of matches) {
|
|
const variable = match[1];
|
|
if (variable !== undefined) {
|
|
variables.push(variable);
|
|
}
|
|
}
|
|
|
|
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(): { level: FormalityLevel; description: string }[] {
|
|
return Object.entries(FORMALITY_MODIFIERS).map(([level, description]) => ({
|
|
level: level as FormalityLevel,
|
|
description,
|
|
}));
|
|
}
|
|
}
|