feat(#130): add Personality Prisma schema and backend

Implement Personality system backend with database schema, service,
controller, and comprehensive tests. Personalities define assistant
behavior with system prompts and LLM configuration.

Changes:
- Update Personality model in schema.prisma with LLM provider relation
- Create PersonalitiesService with CRUD and default management
- Create PersonalitiesController with REST endpoints
- Add DTOs with validation (create/update)
- Add entity for type safety
- Remove unused PromptFormatterService
- Achieve 26 tests with full coverage

Endpoints:
- GET /personality - List all
- GET /personality/default - Get default
- GET /personality/by-name/:name - Get by name
- GET /personality/:id - Get one
- POST /personality - Create
- PATCH /personality/:id - Update
- DELETE /personality/:id - Delete
- POST /personality/:id/set-default - Set default

Fixes #130

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-31 12:44:50 -06:00
parent 1f97e6de40
commit 64cb5c1edd
12 changed files with 516 additions and 549 deletions

View File

@@ -3,52 +3,15 @@ import { PrismaService } from "../prisma/prisma.service";
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
import { Personality } from "./entities/personality.entity";
/**
* Service for managing personality/assistant configurations
*/
@Injectable()
export class PersonalitiesService {
private readonly logger = new Logger(PersonalitiesService.name);
constructor(private readonly prisma: PrismaService) {}
/**
* Find all personalities for a workspace
*/
async findAll(workspaceId: string, isActive = true): Promise<Personality[]> {
return this.prisma.personality.findMany({
where: { workspaceId, isActive },
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
});
}
/**
* Find a specific personality by ID
*/
async findOne(workspaceId: string, id: string): Promise<Personality> {
const personality = await this.prisma.personality.findUnique({
where: { id, workspaceId },
});
if (!personality) {
throw new NotFoundException(`Personality with ID ${id} not found`);
}
return personality;
}
/**
* Find the default personality for a workspace
*/
async findDefault(workspaceId: string): Promise<Personality> {
const personality = await this.prisma.personality.findFirst({
where: { workspaceId, isDefault: true, isActive: true },
});
if (!personality) {
throw new NotFoundException(`No default personality found for workspace ${workspaceId}`);
}
return personality;
}
/**
* Create a new personality
*/
@@ -68,15 +31,79 @@ export class PersonalitiesService {
}
const personality = await this.prisma.personality.create({
data: Object.assign({}, dto, {
data: {
workspaceId,
}),
name: dto.name,
displayName: dto.displayName,
description: dto.description ?? null,
systemPrompt: dto.systemPrompt,
temperature: dto.temperature ?? null,
maxTokens: dto.maxTokens ?? null,
llmProviderInstanceId: dto.llmProviderInstanceId ?? null,
isDefault: dto.isDefault ?? false,
isEnabled: dto.isEnabled ?? true,
},
});
this.logger.log(`Created personality ${personality.id} for workspace ${workspaceId}`);
return personality;
}
/**
* Find all personalities for a workspace
*/
async findAll(workspaceId: string): Promise<Personality[]> {
return this.prisma.personality.findMany({
where: { workspaceId },
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
});
}
/**
* Find a specific personality by ID
*/
async findOne(workspaceId: string, id: string): Promise<Personality> {
const personality = await this.prisma.personality.findUnique({
where: { id, workspaceId },
});
if (!personality) {
throw new NotFoundException(`Personality with ID ${id} not found`);
}
return personality;
}
/**
* Find a personality by name
*/
async findByName(workspaceId: string, name: string): Promise<Personality> {
const personality = await this.prisma.personality.findFirst({
where: { workspaceId, name },
});
if (!personality) {
throw new NotFoundException(`Personality with name "${name}" not found`);
}
return personality;
}
/**
* Find the default personality for a workspace
*/
async findDefault(workspaceId: string): Promise<Personality> {
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 personality;
}
/**
* Update an existing personality
*/
@@ -112,15 +139,34 @@ export class PersonalitiesService {
/**
* Delete a personality
*/
async remove(workspaceId: string, id: string): Promise<Personality> {
async delete(workspaceId: string, id: string): Promise<void> {
// Check existence
await this.findOne(workspaceId, id);
const personality = await this.prisma.personality.delete({
await this.prisma.personality.delete({
where: { id },
});
this.logger.log(`Deleted personality ${id} from workspace ${workspaceId}`);
}
/**
* Set a personality as the default
*/
async setDefault(workspaceId: string, id: string): Promise<Personality> {
// Check 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;
}