feat(api): implement personalities CRUD API (#537)
All checks were successful
ci/woodpecker/push/api Pipeline was successful
All checks were successful
ci/woodpecker/push/api Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #537.
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { Injectable, NotFoundException, ConflictException, Logger } from "@nestjs/common";
|
||||
import type { FormalityLevel, Personality } from "@prisma/client";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||
import { Personality } from "./entities/personality.entity";
|
||||
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
|
||||
* Service for managing personality/assistant configurations.
|
||||
*
|
||||
* Field mapping:
|
||||
* Prisma `systemPrompt` <-> API/frontend `systemPromptTemplate`
|
||||
* Prisma `isEnabled` <-> API/frontend `isActive`
|
||||
*/
|
||||
@Injectable()
|
||||
export class PersonalitiesService {
|
||||
@@ -12,11 +19,30 @@ export class PersonalitiesService {
|
||||
|
||||
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<Personality> {
|
||||
// Check for duplicate name
|
||||
async create(workspaceId: string, dto: CreatePersonalityDto): Promise<PersonalityResponse> {
|
||||
// Check for duplicate name within workspace
|
||||
const existing = await this.prisma.personality.findFirst({
|
||||
where: { workspaceId, name: dto.name },
|
||||
});
|
||||
@@ -25,7 +51,7 @@ export class PersonalitiesService {
|
||||
throw new ConflictException(`Personality with name "${dto.name}" already exists`);
|
||||
}
|
||||
|
||||
// If creating a default personality, unset other defaults
|
||||
// If creating as default, unset other defaults first
|
||||
if (dto.isDefault) {
|
||||
await this.unsetOtherDefaults(workspaceId);
|
||||
}
|
||||
@@ -34,36 +60,43 @@ export class PersonalitiesService {
|
||||
data: {
|
||||
workspaceId,
|
||||
name: dto.name,
|
||||
displayName: dto.displayName,
|
||||
displayName: dto.name, // use name as displayName since frontend doesn't send displayName separately
|
||||
description: dto.description ?? null,
|
||||
systemPrompt: dto.systemPrompt,
|
||||
temperature: dto.temperature ?? null,
|
||||
maxTokens: dto.maxTokens ?? null,
|
||||
llmProviderInstanceId: dto.llmProviderInstanceId ?? null,
|
||||
tone: dto.tone,
|
||||
formalityLevel: dto.formalityLevel,
|
||||
systemPrompt: dto.systemPromptTemplate,
|
||||
isDefault: dto.isDefault ?? false,
|
||||
isEnabled: dto.isEnabled ?? true,
|
||||
isEnabled: dto.isActive ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`Created personality ${personality.id} for workspace ${workspaceId}`);
|
||||
return personality;
|
||||
return this.toResponse(personality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all personalities for a workspace
|
||||
* Find all personalities for a workspace with optional active filter
|
||||
*/
|
||||
async findAll(workspaceId: string): Promise<Personality[]> {
|
||||
return this.prisma.personality.findMany({
|
||||
where: { workspaceId },
|
||||
async findAll(workspaceId: string, query?: PersonalityQueryDto): Promise<PersonalityResponse[]> {
|
||||
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<Personality> {
|
||||
const personality = await this.prisma.personality.findUnique({
|
||||
async findOne(workspaceId: string, id: string): Promise<PersonalityResponse> {
|
||||
const personality = await this.prisma.personality.findFirst({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
@@ -71,13 +104,13 @@ export class PersonalitiesService {
|
||||
throw new NotFoundException(`Personality with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return personality;
|
||||
return this.toResponse(personality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a personality by name
|
||||
* Find a personality by name slug
|
||||
*/
|
||||
async findByName(workspaceId: string, name: string): Promise<Personality> {
|
||||
async findByName(workspaceId: string, name: string): Promise<PersonalityResponse> {
|
||||
const personality = await this.prisma.personality.findFirst({
|
||||
where: { workspaceId, name },
|
||||
});
|
||||
@@ -86,13 +119,13 @@ export class PersonalitiesService {
|
||||
throw new NotFoundException(`Personality with name "${name}" not found`);
|
||||
}
|
||||
|
||||
return personality;
|
||||
return this.toResponse(personality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the default personality for a workspace
|
||||
* Find the default (and enabled) personality for a workspace
|
||||
*/
|
||||
async findDefault(workspaceId: string): Promise<Personality> {
|
||||
async findDefault(workspaceId: string): Promise<PersonalityResponse> {
|
||||
const personality = await this.prisma.personality.findFirst({
|
||||
where: { workspaceId, isDefault: true, isEnabled: true },
|
||||
});
|
||||
@@ -101,14 +134,18 @@ export class PersonalitiesService {
|
||||
throw new NotFoundException(`No default personality found for workspace ${workspaceId}`);
|
||||
}
|
||||
|
||||
return personality;
|
||||
return this.toResponse(personality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing personality
|
||||
*/
|
||||
async update(workspaceId: string, id: string, dto: UpdatePersonalityDto): Promise<Personality> {
|
||||
// Check existence
|
||||
async update(
|
||||
workspaceId: string,
|
||||
id: string,
|
||||
dto: UpdatePersonalityDto
|
||||
): Promise<PersonalityResponse> {
|
||||
// Verify existence
|
||||
await this.findOne(workspaceId, id);
|
||||
|
||||
// Check for duplicate name if updating name
|
||||
@@ -127,20 +164,43 @@ export class PersonalitiesService {
|
||||
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: dto,
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
this.logger.log(`Updated personality ${id} for workspace ${workspaceId}`);
|
||||
return personality;
|
||||
return this.toResponse(personality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a personality
|
||||
*/
|
||||
async delete(workspaceId: string, id: string): Promise<void> {
|
||||
// Check existence
|
||||
// Verify existence
|
||||
await this.findOne(workspaceId, id);
|
||||
|
||||
await this.prisma.personality.delete({
|
||||
@@ -151,23 +211,22 @@ export class PersonalitiesService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a personality as the default
|
||||
* Set a personality as the default (convenience endpoint)
|
||||
*/
|
||||
async setDefault(workspaceId: string, id: string): Promise<Personality> {
|
||||
// Check existence
|
||||
async setDefault(workspaceId: string, id: string): Promise<PersonalityResponse> {
|
||||
// Verify existence
|
||||
await this.findOne(workspaceId, id);
|
||||
|
||||
// Unset other defaults
|
||||
await this.unsetOtherDefaults(workspaceId, id);
|
||||
|
||||
// Set this one as default
|
||||
const personality = await this.prisma.personality.update({
|
||||
where: { id },
|
||||
data: { isDefault: true },
|
||||
});
|
||||
|
||||
this.logger.log(`Set personality ${id} as default for workspace ${workspaceId}`);
|
||||
return personality;
|
||||
return this.toResponse(personality);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +237,7 @@ export class PersonalitiesService {
|
||||
where: {
|
||||
workspaceId,
|
||||
isDefault: true,
|
||||
...(excludeId && { id: { not: excludeId } }),
|
||||
...(excludeId !== undefined && { id: { not: excludeId } }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user