Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
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 <noreply@anthropic.com>
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,
|
|
}));
|
|
}
|
|
}
|