import { Injectable, NotFoundException, ConflictException, Logger } from "@nestjs/common"; import type { FormalityLevel, Personality } from "@prisma/client"; import { PrismaService } from "../prisma/prisma.service"; import type { CreatePersonalityDto } from "./dto/create-personality.dto"; import type { UpdatePersonalityDto } from "./dto/update-personality.dto"; import type { PersonalityQueryDto } from "./dto/personality-query.dto"; import type { PersonalityResponse } from "./entities/personality.entity"; /** * Service for managing personality/assistant configurations. * * Field mapping: * Prisma `systemPrompt` <-> API/frontend `systemPromptTemplate` * Prisma `isEnabled` <-> API/frontend `isActive` */ @Injectable() export class PersonalitiesService { private readonly logger = new Logger(PersonalitiesService.name); constructor(private readonly prisma: PrismaService) {} /** * Map a Prisma Personality record to the API response shape. */ private toResponse(personality: Personality): PersonalityResponse { return { id: personality.id, workspaceId: personality.workspaceId, name: personality.name, description: personality.description, tone: personality.tone, formalityLevel: personality.formalityLevel, systemPromptTemplate: personality.systemPrompt, isDefault: personality.isDefault, isActive: personality.isEnabled, createdAt: personality.createdAt, updatedAt: personality.updatedAt, }; } /** * Create a new personality */ async create(workspaceId: string, dto: CreatePersonalityDto): Promise { // Check for duplicate name within workspace const existing = await this.prisma.personality.findFirst({ where: { workspaceId, name: dto.name }, }); if (existing) { throw new ConflictException(`Personality with name "${dto.name}" already exists`); } // If creating as default, unset other defaults first if (dto.isDefault) { await this.unsetOtherDefaults(workspaceId); } const personality = await this.prisma.personality.create({ data: { workspaceId, name: dto.name, displayName: dto.name, // use name as displayName since frontend doesn't send displayName separately description: dto.description ?? null, tone: dto.tone, formalityLevel: dto.formalityLevel, systemPrompt: dto.systemPromptTemplate, isDefault: dto.isDefault ?? false, isEnabled: dto.isActive ?? true, }, }); this.logger.log(`Created personality ${personality.id} for workspace ${workspaceId}`); return this.toResponse(personality); } /** * Find all personalities for a workspace with optional active filter */ async findAll(workspaceId: string, query?: PersonalityQueryDto): Promise { const where: { workspaceId: string; isEnabled?: boolean } = { workspaceId }; if (query?.isActive !== undefined) { where.isEnabled = query.isActive; } const personalities = await this.prisma.personality.findMany({ where, orderBy: [{ isDefault: "desc" }, { name: "asc" }], }); return personalities.map((p) => this.toResponse(p)); } /** * Find a specific personality by ID */ async findOne(workspaceId: string, id: string): Promise { const personality = await this.prisma.personality.findFirst({ where: { id, workspaceId }, }); if (!personality) { throw new NotFoundException(`Personality with ID ${id} not found`); } return this.toResponse(personality); } /** * Find a personality by name slug */ async findByName(workspaceId: string, name: string): Promise { const personality = await this.prisma.personality.findFirst({ where: { workspaceId, name }, }); if (!personality) { throw new NotFoundException(`Personality with name "${name}" not found`); } return this.toResponse(personality); } /** * Find the default (and enabled) personality for a workspace */ async findDefault(workspaceId: string): Promise { const personality = await this.prisma.personality.findFirst({ where: { workspaceId, isDefault: true, isEnabled: true }, }); if (!personality) { throw new NotFoundException(`No default personality found for workspace ${workspaceId}`); } return this.toResponse(personality); } /** * Update an existing personality */ async update( workspaceId: string, id: string, dto: UpdatePersonalityDto ): Promise { // Verify existence await this.findOne(workspaceId, id); // Check for duplicate name if updating name if (dto.name) { const existing = await this.prisma.personality.findFirst({ where: { workspaceId, name: dto.name, id: { not: id } }, }); if (existing) { throw new ConflictException(`Personality with name "${dto.name}" already exists`); } } // If setting as default, unset other defaults if (dto.isDefault === true) { await this.unsetOtherDefaults(workspaceId, id); } // Build update data with field mapping const updateData: { name?: string; displayName?: string; description?: string; tone?: string; formalityLevel?: FormalityLevel; systemPrompt?: string; isDefault?: boolean; isEnabled?: boolean; } = {}; if (dto.name !== undefined) { updateData.name = dto.name; updateData.displayName = dto.name; } if (dto.description !== undefined) updateData.description = dto.description; if (dto.tone !== undefined) updateData.tone = dto.tone; if (dto.formalityLevel !== undefined) updateData.formalityLevel = dto.formalityLevel; if (dto.systemPromptTemplate !== undefined) updateData.systemPrompt = dto.systemPromptTemplate; if (dto.isDefault !== undefined) updateData.isDefault = dto.isDefault; if (dto.isActive !== undefined) updateData.isEnabled = dto.isActive; const personality = await this.prisma.personality.update({ where: { id }, data: updateData, }); this.logger.log(`Updated personality ${id} for workspace ${workspaceId}`); return this.toResponse(personality); } /** * Delete a personality */ async delete(workspaceId: string, id: string): Promise { // Verify existence await this.findOne(workspaceId, id); await this.prisma.personality.delete({ where: { id }, }); this.logger.log(`Deleted personality ${id} from workspace ${workspaceId}`); } /** * Set a personality as the default (convenience endpoint) */ async setDefault(workspaceId: string, id: string): Promise { // Verify existence await this.findOne(workspaceId, id); // Unset other defaults await this.unsetOtherDefaults(workspaceId, id); const personality = await this.prisma.personality.update({ where: { id }, data: { isDefault: true }, }); this.logger.log(`Set personality ${id} as default for workspace ${workspaceId}`); return this.toResponse(personality); } /** * Unset the default flag on all other personalities in the workspace */ private async unsetOtherDefaults(workspaceId: string, excludeId?: string): Promise { const currentDefault = await this.prisma.personality.findFirst({ where: { workspaceId, isDefault: true, ...(excludeId !== undefined && { id: { not: excludeId } }), }, }); if (currentDefault) { await this.prisma.personality.update({ where: { id: currentDefault.id }, data: { isDefault: false }, }); } } }