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:
@@ -921,28 +921,35 @@ model Personality {
|
|||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
// Identity
|
// Identity
|
||||||
name String
|
name String // unique identifier slug
|
||||||
|
displayName String @map("display_name")
|
||||||
description String? @db.Text
|
description String? @db.Text
|
||||||
|
|
||||||
// Personality traits
|
// System prompt
|
||||||
tone String
|
systemPrompt String @map("system_prompt") @db.Text
|
||||||
formalityLevel FormalityLevel @map("formality_level")
|
|
||||||
|
|
||||||
// System prompt template
|
// LLM configuration
|
||||||
systemPromptTemplate String @map("system_prompt_template") @db.Text
|
temperature Float? // null = use provider default
|
||||||
|
maxTokens Int? @map("max_tokens") // null = use provider default
|
||||||
|
llmProviderInstanceId String? @map("llm_provider_instance_id") @db.Uuid
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
isDefault Boolean @default(false) @map("is_default")
|
isDefault Boolean @default(false) @map("is_default")
|
||||||
isActive Boolean @default(true) @map("is_active")
|
isEnabled Boolean @default(true) @map("is_enabled")
|
||||||
|
|
||||||
// Audit
|
// Audit
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
||||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
llmProviderInstance LlmProviderInstance? @relation("PersonalityLlmProvider", fields: [llmProviderInstanceId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
@@unique([id, workspaceId])
|
@@unique([id, workspaceId])
|
||||||
|
@@unique([workspaceId, name])
|
||||||
@@index([workspaceId])
|
@@index([workspaceId])
|
||||||
@@index([workspaceId, isDefault])
|
@@index([workspaceId, isDefault])
|
||||||
@@index([workspaceId, isActive])
|
@@index([workspaceId, isEnabled])
|
||||||
|
@@index([llmProviderInstanceId])
|
||||||
@@map("personalities")
|
@@map("personalities")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,7 +969,8 @@ model LlmProviderInstance {
|
|||||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
user User? @relation("UserLlmProviders", fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation("UserLlmProviders", fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
personalities Personality[] @relation("PersonalityLlmProvider")
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([providerType])
|
@@index([providerType])
|
||||||
|
|||||||
@@ -1,37 +1,53 @@
|
|||||||
import { IsString, IsIn, IsOptional, IsBoolean, MinLength, MaxLength } from "class-validator";
|
import {
|
||||||
|
IsString,
|
||||||
export const FORMALITY_LEVELS = [
|
IsOptional,
|
||||||
"VERY_CASUAL",
|
IsBoolean,
|
||||||
"CASUAL",
|
IsNumber,
|
||||||
"NEUTRAL",
|
IsInt,
|
||||||
"FORMAL",
|
IsUUID,
|
||||||
"VERY_FORMAL",
|
MinLength,
|
||||||
] as const;
|
MaxLength,
|
||||||
|
Min,
|
||||||
export type FormalityLevel = (typeof FORMALITY_LEVELS)[number];
|
Max,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for creating a new personality/assistant configuration
|
||||||
|
*/
|
||||||
export class CreatePersonalityDto {
|
export class CreatePersonalityDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(1)
|
@MinLength(1)
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
name!: string;
|
name!: string; // unique identifier slug
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(500)
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(1)
|
@MinLength(1)
|
||||||
@MaxLength(50)
|
@MaxLength(200)
|
||||||
tone!: string;
|
displayName!: string; // human-readable name
|
||||||
|
|
||||||
@IsIn(FORMALITY_LEVELS)
|
@IsOptional()
|
||||||
formalityLevel!: FormalityLevel;
|
@IsString()
|
||||||
|
@MaxLength(1000)
|
||||||
|
description?: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(10)
|
@MinLength(10)
|
||||||
systemPromptTemplate!: string;
|
systemPrompt!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@Max(2)
|
||||||
|
temperature?: number; // null = use provider default
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
maxTokens?: number; // null = use provider default
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID("4")
|
||||||
|
llmProviderInstanceId?: string; // FK to LlmProviderInstance
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@@ -39,5 +55,5 @@ export class CreatePersonalityDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isActive?: boolean;
|
isEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,56 @@
|
|||||||
import { IsString, IsOptional, IsBoolean, MinLength, MaxLength, IsIn } from "class-validator";
|
import {
|
||||||
import { FORMALITY_LEVELS, FormalityLevel } from "./create-personality.dto";
|
IsString,
|
||||||
|
IsOptional,
|
||||||
|
IsBoolean,
|
||||||
|
IsNumber,
|
||||||
|
IsInt,
|
||||||
|
IsUUID,
|
||||||
|
MinLength,
|
||||||
|
MaxLength,
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for updating an existing personality/assistant configuration
|
||||||
|
*/
|
||||||
export class UpdatePersonalityDto {
|
export class UpdatePersonalityDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(1)
|
@MinLength(1)
|
||||||
@MaxLength(100)
|
@MaxLength(100)
|
||||||
name?: string;
|
name?: string; // unique identifier slug
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(500)
|
|
||||||
description?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(1)
|
@MinLength(1)
|
||||||
@MaxLength(50)
|
@MaxLength(200)
|
||||||
tone?: string;
|
displayName?: string; // human-readable name
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn(FORMALITY_LEVELS)
|
@IsString()
|
||||||
formalityLevel?: FormalityLevel;
|
@MaxLength(1000)
|
||||||
|
description?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(10)
|
@MinLength(10)
|
||||||
systemPromptTemplate?: string;
|
systemPrompt?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(0)
|
||||||
|
@Max(2)
|
||||||
|
temperature?: number; // null = use provider default
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
maxTokens?: number; // null = use provider default
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID("4")
|
||||||
|
llmProviderInstanceId?: string; // FK to LlmProviderInstance
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@@ -34,5 +58,5 @@ export class UpdatePersonalityDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isActive?: boolean;
|
isEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import type { Personality as PrismaPersonality, FormalityLevel } from "@prisma/client";
|
import type { Personality as PrismaPersonality } from "@prisma/client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Personality entity representing an assistant configuration
|
||||||
|
*/
|
||||||
export class Personality implements PrismaPersonality {
|
export class Personality implements PrismaPersonality {
|
||||||
id!: string;
|
id!: string;
|
||||||
workspaceId!: string;
|
workspaceId!: string;
|
||||||
name!: string;
|
name!: string; // unique identifier slug
|
||||||
|
displayName!: string; // human-readable name
|
||||||
description!: string | null;
|
description!: string | null;
|
||||||
tone!: string;
|
systemPrompt!: string;
|
||||||
formalityLevel!: FormalityLevel;
|
temperature!: number | null; // null = use provider default
|
||||||
systemPromptTemplate!: string;
|
maxTokens!: number | null; // null = use provider default
|
||||||
|
llmProviderInstanceId!: string | null; // FK to LlmProviderInstance
|
||||||
isDefault!: boolean;
|
isDefault!: boolean;
|
||||||
isActive!: boolean;
|
isEnabled!: boolean;
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,51 +2,57 @@ import { describe, it, expect, beforeEach, vi } from "vitest";
|
|||||||
import { Test, TestingModule } from "@nestjs/testing";
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { PersonalitiesController } from "./personalities.controller";
|
import { PersonalitiesController } from "./personalities.controller";
|
||||||
import { PersonalitiesService } from "./personalities.service";
|
import { PersonalitiesService } from "./personalities.service";
|
||||||
import { PromptFormatterService } from "./services/prompt-formatter.service";
|
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||||
|
|
||||||
describe("PersonalitiesController", () => {
|
describe("PersonalitiesController", () => {
|
||||||
let controller: PersonalitiesController;
|
let controller: PersonalitiesController;
|
||||||
let service: PersonalitiesService;
|
let service: PersonalitiesService;
|
||||||
let promptFormatter: PromptFormatterService;
|
|
||||||
|
|
||||||
const mockWorkspaceId = "workspace-123";
|
const mockWorkspaceId = "workspace-123";
|
||||||
|
const mockUserId = "user-123";
|
||||||
const mockPersonalityId = "personality-123";
|
const mockPersonalityId = "personality-123";
|
||||||
const mockRequest = { user: { id: "user-123" }, workspaceId: mockWorkspaceId };
|
|
||||||
|
|
||||||
const mockPersonality = {
|
const mockPersonality = {
|
||||||
id: mockPersonalityId,
|
id: mockPersonalityId,
|
||||||
workspaceId: mockWorkspaceId,
|
workspaceId: mockWorkspaceId,
|
||||||
name: "Professional",
|
name: "professional-assistant",
|
||||||
tone: "professional",
|
displayName: "Professional Assistant",
|
||||||
formalityLevel: "FORMAL",
|
description: "A professional communication assistant",
|
||||||
systemPromptTemplate: "You are a professional assistant.",
|
systemPrompt: "You are a professional assistant who helps with tasks.",
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2000,
|
||||||
|
llmProviderInstanceId: "provider-123",
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
isActive: true,
|
isEnabled: true,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockPersonalitiesService = {
|
const mockRequest = {
|
||||||
findAll: vi.fn(),
|
user: { id: mockUserId },
|
||||||
findOne: vi.fn(),
|
workspaceId: mockWorkspaceId,
|
||||||
findDefault: vi.fn(),
|
|
||||||
create: vi.fn(),
|
|
||||||
update: vi.fn(),
|
|
||||||
remove: vi.fn(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockPromptFormatterService = {
|
const mockPersonalitiesService = {
|
||||||
formatPrompt: vi.fn(),
|
create: vi.fn(),
|
||||||
getFormalityLevels: vi.fn(),
|
findAll: vi.fn(),
|
||||||
|
findOne: vi.fn(),
|
||||||
|
findByName: vi.fn(),
|
||||||
|
findDefault: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
delete: vi.fn(),
|
||||||
|
setDefault: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [PersonalitiesController],
|
controllers: [PersonalitiesController],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: PersonalitiesService, useValue: mockPersonalitiesService },
|
{
|
||||||
{ provide: PromptFormatterService, useValue: mockPromptFormatterService },
|
provide: PersonalitiesService,
|
||||||
|
useValue: mockPersonalitiesService,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideGuard(AuthGuard)
|
.overrideGuard(AuthGuard)
|
||||||
@@ -55,85 +61,119 @@ describe("PersonalitiesController", () => {
|
|||||||
|
|
||||||
controller = module.get<PersonalitiesController>(PersonalitiesController);
|
controller = module.get<PersonalitiesController>(PersonalitiesController);
|
||||||
service = module.get<PersonalitiesService>(PersonalitiesService);
|
service = module.get<PersonalitiesService>(PersonalitiesService);
|
||||||
promptFormatter = module.get<PromptFormatterService>(PromptFormatterService);
|
|
||||||
|
// Reset mocks
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("findAll", () => {
|
describe("findAll", () => {
|
||||||
it("should return all personalities", async () => {
|
it("should return all personalities", async () => {
|
||||||
mockPersonalitiesService.findAll.mockResolvedValue([mockPersonality]);
|
const mockPersonalities = [mockPersonality];
|
||||||
|
mockPersonalitiesService.findAll.mockResolvedValue(mockPersonalities);
|
||||||
|
|
||||||
const result = await controller.findAll(mockRequest);
|
const result = await controller.findAll(mockRequest);
|
||||||
expect(result).toEqual([mockPersonality]);
|
|
||||||
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, undefined);
|
expect(result).toEqual(mockPersonalities);
|
||||||
|
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("findOne", () => {
|
describe("findOne", () => {
|
||||||
it("should return a personality by id", async () => {
|
it("should return a personality by id", async () => {
|
||||||
mockPersonalitiesService.findOne.mockResolvedValue(mockPersonality);
|
mockPersonalitiesService.findOne.mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
const result = await controller.findOne(mockRequest, mockPersonalityId);
|
const result = await controller.findOne(mockRequest, mockPersonalityId);
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonality);
|
expect(result).toEqual(mockPersonality);
|
||||||
|
expect(service.findOne).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findByName", () => {
|
||||||
|
it("should return a personality by name", async () => {
|
||||||
|
mockPersonalitiesService.findByName.mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
|
const result = await controller.findByName(mockRequest, "professional-assistant");
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPersonality);
|
||||||
|
expect(service.findByName).toHaveBeenCalledWith(mockWorkspaceId, "professional-assistant");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("findDefault", () => {
|
describe("findDefault", () => {
|
||||||
it("should return the default personality", async () => {
|
it("should return the default personality", async () => {
|
||||||
mockPersonalitiesService.findDefault.mockResolvedValue(mockPersonality);
|
mockPersonalitiesService.findDefault.mockResolvedValue(mockPersonality);
|
||||||
const result = await controller.findDefault(mockRequest);
|
|
||||||
expect(result).toEqual(mockPersonality);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getFormalityLevels", () => {
|
const result = await controller.findDefault(mockRequest);
|
||||||
it("should return formality levels", () => {
|
|
||||||
const levels = [{ level: "FORMAL", description: "Professional" }];
|
expect(result).toEqual(mockPersonality);
|
||||||
mockPromptFormatterService.getFormalityLevels.mockReturnValue(levels);
|
expect(service.findDefault).toHaveBeenCalledWith(mockWorkspaceId);
|
||||||
const result = controller.getFormalityLevels();
|
|
||||||
expect(result).toEqual(levels);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
it("should create a new personality", async () => {
|
it("should create a new personality", async () => {
|
||||||
const createDto = {
|
const createDto: CreatePersonalityDto = {
|
||||||
name: "Casual",
|
name: "casual-helper",
|
||||||
tone: "casual",
|
displayName: "Casual Helper",
|
||||||
formalityLevel: "CASUAL" as const,
|
description: "A casual helper",
|
||||||
systemPromptTemplate: "You are a casual assistant.",
|
systemPrompt: "You are a casual assistant.",
|
||||||
|
temperature: 0.8,
|
||||||
|
maxTokens: 1500,
|
||||||
};
|
};
|
||||||
mockPersonalitiesService.create.mockResolvedValue({ ...mockPersonality, ...createDto });
|
|
||||||
await controller.create(mockRequest, createDto);
|
mockPersonalitiesService.create.mockResolvedValue({
|
||||||
|
...mockPersonality,
|
||||||
|
...createDto,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await controller.create(mockRequest, createDto);
|
||||||
|
|
||||||
|
expect(result).toMatchObject(createDto);
|
||||||
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
|
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
it("should update a personality", async () => {
|
it("should update a personality", async () => {
|
||||||
const updateDto = { description: "Updated" };
|
const updateDto: UpdatePersonalityDto = {
|
||||||
mockPersonalitiesService.update.mockResolvedValue({ ...mockPersonality, ...updateDto });
|
description: "Updated description",
|
||||||
await controller.update(mockRequest, mockPersonalityId, updateDto);
|
temperature: 0.9,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockPersonalitiesService.update.mockResolvedValue({
|
||||||
|
...mockPersonality,
|
||||||
|
...updateDto,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await controller.update(mockRequest, mockPersonalityId, updateDto);
|
||||||
|
|
||||||
|
expect(result).toMatchObject(updateDto);
|
||||||
expect(service.update).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId, updateDto);
|
expect(service.update).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId, updateDto);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("remove", () => {
|
describe("delete", () => {
|
||||||
it("should delete a personality", async () => {
|
it("should delete a personality", async () => {
|
||||||
mockPersonalitiesService.remove.mockResolvedValue(mockPersonality);
|
mockPersonalitiesService.delete.mockResolvedValue(undefined);
|
||||||
await controller.remove(mockRequest, mockPersonalityId);
|
|
||||||
expect(service.remove).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
await controller.delete(mockRequest, mockPersonalityId);
|
||||||
|
|
||||||
|
expect(service.delete).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("previewPrompt", () => {
|
describe("setDefault", () => {
|
||||||
it("should return formatted system prompt", async () => {
|
it("should set a personality as default", async () => {
|
||||||
const context = { userName: "John" };
|
mockPersonalitiesService.setDefault.mockResolvedValue({
|
||||||
mockPersonalitiesService.findOne.mockResolvedValue(mockPersonality);
|
...mockPersonality,
|
||||||
mockPromptFormatterService.formatPrompt.mockReturnValue({
|
isDefault: true,
|
||||||
systemPrompt: "Formatted prompt",
|
|
||||||
metadata: {},
|
|
||||||
});
|
});
|
||||||
const result = await controller.previewPrompt(mockRequest, mockPersonalityId, context);
|
|
||||||
expect(result.systemPrompt).toBe("Formatted prompt");
|
const result = await controller.setDefault(mockRequest, mockPersonalityId);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ isDefault: true });
|
||||||
|
expect(service.setDefault).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,20 +2,17 @@ import {
|
|||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Patch,
|
||||||
Delete,
|
Delete,
|
||||||
Body,
|
Body,
|
||||||
Param,
|
Param,
|
||||||
Query,
|
|
||||||
UseGuards,
|
UseGuards,
|
||||||
Req,
|
Req,
|
||||||
ParseBoolPipe,
|
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||||
import { PersonalitiesService } from "./personalities.service";
|
import { PersonalitiesService } from "./personalities.service";
|
||||||
import { PromptFormatterService, PromptContext } from "./services/prompt-formatter.service";
|
|
||||||
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||||
import { Personality } from "./entities/personality.entity";
|
import { Personality } from "./entities/personality.entity";
|
||||||
|
|
||||||
@@ -24,37 +21,52 @@ interface AuthenticatedRequest {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller("personalities")
|
/**
|
||||||
|
* Controller for managing personality/assistant configurations
|
||||||
|
*/
|
||||||
|
@Controller("personality")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
export class PersonalitiesController {
|
export class PersonalitiesController {
|
||||||
constructor(
|
constructor(private readonly personalitiesService: PersonalitiesService) {}
|
||||||
private readonly personalitiesService: PersonalitiesService,
|
|
||||||
private readonly promptFormatter: PromptFormatterService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all personalities for the workspace
|
||||||
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(
|
async findAll(@Req() req: AuthenticatedRequest): Promise<Personality[]> {
|
||||||
@Req() req: AuthenticatedRequest,
|
return this.personalitiesService.findAll(req.workspaceId);
|
||||||
@Query("isActive", new ParseBoolPipe({ optional: true })) isActive?: boolean
|
|
||||||
): Promise<Personality[]> {
|
|
||||||
return this.personalitiesService.findAll(req.workspaceId, isActive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default personality for the workspace
|
||||||
|
*/
|
||||||
@Get("default")
|
@Get("default")
|
||||||
async findDefault(@Req() req: AuthenticatedRequest): Promise<Personality> {
|
async findDefault(@Req() req: AuthenticatedRequest): Promise<Personality> {
|
||||||
return this.personalitiesService.findDefault(req.workspaceId);
|
return this.personalitiesService.findDefault(req.workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("formality-levels")
|
/**
|
||||||
getFormalityLevels(): { level: string; description: string }[] {
|
* Get a personality by its unique name
|
||||||
return this.promptFormatter.getFormalityLevels();
|
*/
|
||||||
|
@Get("by-name/:name")
|
||||||
|
async findByName(
|
||||||
|
@Req() req: AuthenticatedRequest,
|
||||||
|
@Param("name") name: string
|
||||||
|
): Promise<Personality> {
|
||||||
|
return this.personalitiesService.findByName(req.workspaceId, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a personality by ID
|
||||||
|
*/
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
async findOne(@Req() req: AuthenticatedRequest, @Param("id") id: string): Promise<Personality> {
|
async findOne(@Req() req: AuthenticatedRequest, @Param("id") id: string): Promise<Personality> {
|
||||||
return this.personalitiesService.findOne(req.workspaceId, id);
|
return this.personalitiesService.findOne(req.workspaceId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new personality
|
||||||
|
*/
|
||||||
@Post()
|
@Post()
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
async create(
|
async create(
|
||||||
@@ -64,7 +76,10 @@ export class PersonalitiesController {
|
|||||||
return this.personalitiesService.create(req.workspaceId, dto);
|
return this.personalitiesService.create(req.workspaceId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(":id")
|
/**
|
||||||
|
* Update a personality
|
||||||
|
*/
|
||||||
|
@Patch(":id")
|
||||||
async update(
|
async update(
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
@Param("id") id: string,
|
@Param("id") id: string,
|
||||||
@@ -73,20 +88,23 @@ export class PersonalitiesController {
|
|||||||
return this.personalitiesService.update(req.workspaceId, id, dto);
|
return this.personalitiesService.update(req.workspaceId, id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a personality
|
||||||
|
*/
|
||||||
@Delete(":id")
|
@Delete(":id")
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
async remove(@Req() req: AuthenticatedRequest, @Param("id") id: string): Promise<Personality> {
|
async delete(@Req() req: AuthenticatedRequest, @Param("id") id: string): Promise<void> {
|
||||||
return this.personalitiesService.remove(req.workspaceId, id);
|
return this.personalitiesService.delete(req.workspaceId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(":id/preview")
|
/**
|
||||||
async previewPrompt(
|
* Set a personality as the default
|
||||||
|
*/
|
||||||
|
@Post(":id/set-default")
|
||||||
|
async setDefault(
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
@Param("id") id: string,
|
@Param("id") id: string
|
||||||
@Body() context?: PromptContext
|
): Promise<Personality> {
|
||||||
): Promise<{ systemPrompt: string }> {
|
return this.personalitiesService.setDefault(req.workspaceId, id);
|
||||||
const personality = await this.personalitiesService.findOne(req.workspaceId, id);
|
|
||||||
const { systemPrompt } = this.promptFormatter.formatPrompt(personality, context);
|
|
||||||
return { systemPrompt };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,11 @@ import { PrismaModule } from "../prisma/prisma.module";
|
|||||||
import { AuthModule } from "../auth/auth.module";
|
import { AuthModule } from "../auth/auth.module";
|
||||||
import { PersonalitiesService } from "./personalities.service";
|
import { PersonalitiesService } from "./personalities.service";
|
||||||
import { PersonalitiesController } from "./personalities.controller";
|
import { PersonalitiesController } from "./personalities.controller";
|
||||||
import { PromptFormatterService } from "./services/prompt-formatter.service";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, AuthModule],
|
imports: [PrismaModule, AuthModule],
|
||||||
controllers: [PersonalitiesController],
|
controllers: [PersonalitiesController],
|
||||||
providers: [PersonalitiesService, PromptFormatterService],
|
providers: [PersonalitiesService],
|
||||||
exports: [PersonalitiesService, PromptFormatterService],
|
exports: [PersonalitiesService],
|
||||||
})
|
})
|
||||||
export class PersonalitiesModule {}
|
export class PersonalitiesModule {}
|
||||||
|
|||||||
@@ -10,19 +10,21 @@ describe("PersonalitiesService", () => {
|
|||||||
let prisma: PrismaService;
|
let prisma: PrismaService;
|
||||||
|
|
||||||
const mockWorkspaceId = "workspace-123";
|
const mockWorkspaceId = "workspace-123";
|
||||||
const mockUserId = "user-123";
|
|
||||||
const mockPersonalityId = "personality-123";
|
const mockPersonalityId = "personality-123";
|
||||||
|
const mockProviderId = "provider-123";
|
||||||
|
|
||||||
const mockPersonality = {
|
const mockPersonality = {
|
||||||
id: mockPersonalityId,
|
id: mockPersonalityId,
|
||||||
workspaceId: mockWorkspaceId,
|
workspaceId: mockWorkspaceId,
|
||||||
name: "Professional",
|
name: "professional-assistant",
|
||||||
description: "Professional communication style",
|
displayName: "Professional Assistant",
|
||||||
tone: "professional",
|
description: "A professional communication assistant",
|
||||||
formalityLevel: "FORMAL" as const,
|
systemPrompt: "You are a professional assistant who helps with tasks.",
|
||||||
systemPromptTemplate: "You are a professional assistant.",
|
temperature: 0.7,
|
||||||
|
maxTokens: 2000,
|
||||||
|
llmProviderInstanceId: mockProviderId,
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
isActive: true,
|
isEnabled: true,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@@ -58,82 +60,15 @@ describe("PersonalitiesService", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("findAll", () => {
|
|
||||||
it("should return all personalities for a workspace", async () => {
|
|
||||||
const mockPersonalities = [mockPersonality];
|
|
||||||
mockPrismaService.personality.findMany.mockResolvedValue(mockPersonalities);
|
|
||||||
|
|
||||||
const result = await service.findAll(mockWorkspaceId);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonalities);
|
|
||||||
expect(prisma.personality.findMany).toHaveBeenCalledWith({
|
|
||||||
where: { workspaceId: mockWorkspaceId, isActive: true },
|
|
||||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should filter by active status", async () => {
|
|
||||||
mockPrismaService.personality.findMany.mockResolvedValue([mockPersonality]);
|
|
||||||
|
|
||||||
await service.findAll(mockWorkspaceId, false);
|
|
||||||
|
|
||||||
expect(prisma.personality.findMany).toHaveBeenCalledWith({
|
|
||||||
where: { workspaceId: mockWorkspaceId, isActive: false },
|
|
||||||
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findOne", () => {
|
|
||||||
it("should return a personality by id", async () => {
|
|
||||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
|
||||||
|
|
||||||
const result = await service.findOne(mockWorkspaceId, mockPersonalityId);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonality);
|
|
||||||
expect(prisma.personality.findUnique).toHaveBeenCalledWith({
|
|
||||||
where: {
|
|
||||||
id: mockPersonalityId,
|
|
||||||
workspaceId: mockWorkspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw NotFoundException when personality not found", async () => {
|
|
||||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await expect(service.findOne(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
|
||||||
NotFoundException,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("findDefault", () => {
|
|
||||||
it("should return the default personality", async () => {
|
|
||||||
mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);
|
|
||||||
|
|
||||||
const result = await service.findDefault(mockWorkspaceId);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonality);
|
|
||||||
expect(prisma.personality.findFirst).toHaveBeenCalledWith({
|
|
||||||
where: { workspaceId: mockWorkspaceId, isDefault: true, isActive: true },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should throw NotFoundException when no default personality exists", async () => {
|
|
||||||
mockPrismaService.personality.findFirst.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await expect(service.findDefault(mockWorkspaceId)).rejects.toThrow(NotFoundException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
const createDto: CreatePersonalityDto = {
|
const createDto: CreatePersonalityDto = {
|
||||||
name: "Casual",
|
name: "casual-helper",
|
||||||
description: "Casual communication style",
|
displayName: "Casual Helper",
|
||||||
tone: "casual",
|
description: "A casual communication helper",
|
||||||
formalityLevel: "CASUAL",
|
systemPrompt: "You are a casual assistant.",
|
||||||
systemPromptTemplate: "You are a casual assistant.",
|
temperature: 0.8,
|
||||||
|
maxTokens: 1500,
|
||||||
|
llmProviderInstanceId: mockProviderId,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should create a new personality", async () => {
|
it("should create a new personality", async () => {
|
||||||
@@ -142,6 +77,8 @@ describe("PersonalitiesService", () => {
|
|||||||
...mockPersonality,
|
...mockPersonality,
|
||||||
...createDto,
|
...createDto,
|
||||||
id: "new-personality-id",
|
id: "new-personality-id",
|
||||||
|
isDefault: false,
|
||||||
|
isEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.create(mockWorkspaceId, createDto);
|
const result = await service.create(mockWorkspaceId, createDto);
|
||||||
@@ -150,7 +87,15 @@ describe("PersonalitiesService", () => {
|
|||||||
expect(prisma.personality.create).toHaveBeenCalledWith({
|
expect(prisma.personality.create).toHaveBeenCalledWith({
|
||||||
data: {
|
data: {
|
||||||
workspaceId: mockWorkspaceId,
|
workspaceId: mockWorkspaceId,
|
||||||
...createDto,
|
name: createDto.name,
|
||||||
|
displayName: createDto.displayName,
|
||||||
|
description: createDto.description ?? null,
|
||||||
|
systemPrompt: createDto.systemPrompt,
|
||||||
|
temperature: createDto.temperature ?? null,
|
||||||
|
maxTokens: createDto.maxTokens ?? null,
|
||||||
|
llmProviderInstanceId: createDto.llmProviderInstanceId ?? null,
|
||||||
|
isDefault: false,
|
||||||
|
isEnabled: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -186,10 +131,92 @@ describe("PersonalitiesService", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("findAll", () => {
|
||||||
|
it("should return all personalities for a workspace", async () => {
|
||||||
|
const mockPersonalities = [mockPersonality];
|
||||||
|
mockPrismaService.personality.findMany.mockResolvedValue(mockPersonalities);
|
||||||
|
|
||||||
|
const result = await service.findAll(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPersonalities);
|
||||||
|
expect(prisma.personality.findMany).toHaveBeenCalledWith({
|
||||||
|
where: { workspaceId: mockWorkspaceId },
|
||||||
|
orderBy: [{ isDefault: "desc" }, { name: "asc" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findOne", () => {
|
||||||
|
it("should return a personality by id", async () => {
|
||||||
|
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
|
const result = await service.findOne(mockWorkspaceId, mockPersonalityId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPersonality);
|
||||||
|
expect(prisma.personality.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
id: mockPersonalityId,
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundException when personality not found", async () => {
|
||||||
|
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.findOne(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
||||||
|
NotFoundException
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findByName", () => {
|
||||||
|
it("should return a personality by name", async () => {
|
||||||
|
mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
|
const result = await service.findByName(mockWorkspaceId, "professional-assistant");
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPersonality);
|
||||||
|
expect(prisma.personality.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
name: "professional-assistant",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundException when personality not found", async () => {
|
||||||
|
mockPrismaService.personality.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.findByName(mockWorkspaceId, "non-existent")).rejects.toThrow(
|
||||||
|
NotFoundException
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findDefault", () => {
|
||||||
|
it("should return the default personality", async () => {
|
||||||
|
mockPrismaService.personality.findFirst.mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
|
const result = await service.findDefault(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPersonality);
|
||||||
|
expect(prisma.personality.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: { workspaceId: mockWorkspaceId, isDefault: true, isEnabled: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundException when no default personality exists", async () => {
|
||||||
|
mockPrismaService.personality.findFirst.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.findDefault(mockWorkspaceId)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
const updateDto: UpdatePersonalityDto = {
|
const updateDto: UpdatePersonalityDto = {
|
||||||
description: "Updated description",
|
description: "Updated description",
|
||||||
tone: "updated",
|
temperature: 0.9,
|
||||||
};
|
};
|
||||||
|
|
||||||
it("should update a personality", async () => {
|
it("should update a personality", async () => {
|
||||||
@@ -212,13 +239,13 @@ describe("PersonalitiesService", () => {
|
|||||||
it("should throw NotFoundException when personality not found", async () => {
|
it("should throw NotFoundException when personality not found", async () => {
|
||||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(
|
await expect(service.update(mockWorkspaceId, mockPersonalityId, updateDto)).rejects.toThrow(
|
||||||
service.update(mockWorkspaceId, mockPersonalityId, updateDto),
|
NotFoundException
|
||||||
).rejects.toThrow(NotFoundException);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should throw ConflictException when updating to existing name", async () => {
|
it("should throw ConflictException when updating to existing name", async () => {
|
||||||
const updateNameDto = { name: "Existing Name" };
|
const updateNameDto = { name: "existing-name" };
|
||||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||||
mockPrismaService.personality.findFirst.mockResolvedValue({
|
mockPrismaService.personality.findFirst.mockResolvedValue({
|
||||||
...mockPersonality,
|
...mockPersonality,
|
||||||
@@ -226,19 +253,40 @@ describe("PersonalitiesService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.update(mockWorkspaceId, mockPersonalityId, updateNameDto),
|
service.update(mockWorkspaceId, mockPersonalityId, updateNameDto)
|
||||||
).rejects.toThrow(ConflictException);
|
).rejects.toThrow(ConflictException);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should unset other defaults when setting as default", async () => {
|
||||||
|
const updateDefaultDto = { isDefault: true };
|
||||||
|
const otherPersonality = { ...mockPersonality, id: "other-id", isDefault: true };
|
||||||
|
|
||||||
|
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||||
|
mockPrismaService.personality.findFirst.mockResolvedValue(otherPersonality); // Existing default from unsetOtherDefaults
|
||||||
|
mockPrismaService.personality.update
|
||||||
|
.mockResolvedValueOnce({ ...otherPersonality, isDefault: false }) // Unset old default
|
||||||
|
.mockResolvedValueOnce({ ...mockPersonality, isDefault: true }); // Set new default
|
||||||
|
|
||||||
|
await service.update(mockWorkspaceId, mockPersonalityId, updateDefaultDto);
|
||||||
|
|
||||||
|
expect(prisma.personality.update).toHaveBeenNthCalledWith(1, {
|
||||||
|
where: { id: "other-id" },
|
||||||
|
data: { isDefault: false },
|
||||||
|
});
|
||||||
|
expect(prisma.personality.update).toHaveBeenNthCalledWith(2, {
|
||||||
|
where: { id: mockPersonalityId },
|
||||||
|
data: updateDefaultDto,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("remove", () => {
|
describe("delete", () => {
|
||||||
it("should delete a personality", async () => {
|
it("should delete a personality", async () => {
|
||||||
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||||
mockPrismaService.personality.delete.mockResolvedValue(mockPersonality);
|
mockPrismaService.personality.delete.mockResolvedValue(undefined);
|
||||||
|
|
||||||
const result = await service.remove(mockWorkspaceId, mockPersonalityId);
|
await service.delete(mockWorkspaceId, mockPersonalityId);
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonality);
|
|
||||||
expect(prisma.personality.delete).toHaveBeenCalledWith({
|
expect(prisma.personality.delete).toHaveBeenCalledWith({
|
||||||
where: { id: mockPersonalityId },
|
where: { id: mockPersonalityId },
|
||||||
});
|
});
|
||||||
@@ -247,8 +295,41 @@ describe("PersonalitiesService", () => {
|
|||||||
it("should throw NotFoundException when personality not found", async () => {
|
it("should throw NotFoundException when personality not found", async () => {
|
||||||
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(service.remove(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
await expect(service.delete(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
||||||
NotFoundException,
|
NotFoundException
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setDefault", () => {
|
||||||
|
it("should set a personality as default", async () => {
|
||||||
|
const otherPersonality = { ...mockPersonality, id: "other-id", isDefault: true };
|
||||||
|
const updatedPersonality = { ...mockPersonality, isDefault: true };
|
||||||
|
|
||||||
|
mockPrismaService.personality.findUnique.mockResolvedValue(mockPersonality);
|
||||||
|
mockPrismaService.personality.findFirst.mockResolvedValue(otherPersonality);
|
||||||
|
mockPrismaService.personality.update
|
||||||
|
.mockResolvedValueOnce({ ...otherPersonality, isDefault: false }) // Unset old default
|
||||||
|
.mockResolvedValueOnce(updatedPersonality); // Set new default
|
||||||
|
|
||||||
|
const result = await service.setDefault(mockWorkspaceId, mockPersonalityId);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({ isDefault: true });
|
||||||
|
expect(prisma.personality.update).toHaveBeenNthCalledWith(1, {
|
||||||
|
where: { id: "other-id" },
|
||||||
|
data: { isDefault: false },
|
||||||
|
});
|
||||||
|
expect(prisma.personality.update).toHaveBeenNthCalledWith(2, {
|
||||||
|
where: { id: mockPersonalityId },
|
||||||
|
data: { isDefault: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundException when personality not found", async () => {
|
||||||
|
mockPrismaService.personality.findUnique.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.setDefault(mockWorkspaceId, mockPersonalityId)).rejects.toThrow(
|
||||||
|
NotFoundException
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,52 +3,15 @@ import { PrismaService } from "../prisma/prisma.service";
|
|||||||
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
||||||
import { Personality } from "./entities/personality.entity";
|
import { Personality } from "./entities/personality.entity";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing personality/assistant configurations
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersonalitiesService {
|
export class PersonalitiesService {
|
||||||
private readonly logger = new Logger(PersonalitiesService.name);
|
private readonly logger = new Logger(PersonalitiesService.name);
|
||||||
|
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
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
|
* Create a new personality
|
||||||
*/
|
*/
|
||||||
@@ -68,15 +31,79 @@ export class PersonalitiesService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const personality = await this.prisma.personality.create({
|
const personality = await this.prisma.personality.create({
|
||||||
data: Object.assign({}, dto, {
|
data: {
|
||||||
workspaceId,
|
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}`);
|
this.logger.log(`Created personality ${personality.id} for workspace ${workspaceId}`);
|
||||||
return personality;
|
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
|
* Update an existing personality
|
||||||
*/
|
*/
|
||||||
@@ -112,15 +139,34 @@ export class PersonalitiesService {
|
|||||||
/**
|
/**
|
||||||
* Delete a personality
|
* Delete a personality
|
||||||
*/
|
*/
|
||||||
async remove(workspaceId: string, id: string): Promise<Personality> {
|
async delete(workspaceId: string, id: string): Promise<void> {
|
||||||
// Check existence
|
// Check existence
|
||||||
await this.findOne(workspaceId, id);
|
await this.findOne(workspaceId, id);
|
||||||
|
|
||||||
const personality = await this.prisma.personality.delete({
|
await this.prisma.personality.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Deleted personality ${id} from workspace ${workspaceId}`);
|
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;
|
return personality;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "./prompt-formatter.service";
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import { describe, it, expect, beforeEach } from "vitest";
|
|
||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import { PromptFormatterService, PromptContext } from "./prompt-formatter.service";
|
|
||||||
|
|
||||||
describe("PromptFormatterService", () => {
|
|
||||||
let service: PromptFormatterService;
|
|
||||||
|
|
||||||
const mockPersonality = {
|
|
||||||
id: "personality-123",
|
|
||||||
workspaceId: "workspace-123",
|
|
||||||
name: "Professional",
|
|
||||||
description: "Professional communication style",
|
|
||||||
tone: "professional",
|
|
||||||
formalityLevel: "FORMAL" as const,
|
|
||||||
systemPromptTemplate: "You are a helpful assistant for {{userName}} at {{workspaceName}}.",
|
|
||||||
isDefault: true,
|
|
||||||
isActive: true,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [PromptFormatterService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<PromptFormatterService>(PromptFormatterService);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("formatPrompt", () => {
|
|
||||||
it("should format prompt with context variables", () => {
|
|
||||||
const context: PromptContext = { userName: "John", workspaceName: "Acme Corp" };
|
|
||||||
const result = service.formatPrompt(mockPersonality, context);
|
|
||||||
expect(result.systemPrompt).toContain("John");
|
|
||||||
expect(result.systemPrompt).toContain("Acme Corp");
|
|
||||||
expect(result.metadata.personalityId).toBe(mockPersonality.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add formality modifier", () => {
|
|
||||||
const result = service.formatPrompt(mockPersonality);
|
|
||||||
expect(result.systemPrompt).toContain("professional");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should include metadata", () => {
|
|
||||||
const result = service.formatPrompt(mockPersonality);
|
|
||||||
expect(result.metadata.formattedAt).toBeInstanceOf(Date);
|
|
||||||
expect(result.metadata.tone).toBe("professional");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle custom context variables", () => {
|
|
||||||
const personality = { ...mockPersonality, systemPromptTemplate: "Custom: {{customVar}}" };
|
|
||||||
const context: PromptContext = { custom: { customVar: "test-value" } };
|
|
||||||
const result = service.formatPrompt(personality, context);
|
|
||||||
expect(result.systemPrompt).toContain("test-value");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("buildSystemPrompt", () => {
|
|
||||||
it("should build complete prompt with context", () => {
|
|
||||||
const context: PromptContext = { userName: "Jane" };
|
|
||||||
const result = service.buildSystemPrompt(mockPersonality, context);
|
|
||||||
expect(result).toContain("Jane");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should include date/time when requested", () => {
|
|
||||||
const context: PromptContext = { currentDate: "2024-01-29", currentTime: "14:30", timezone: "UTC" };
|
|
||||||
const result = service.buildSystemPrompt(mockPersonality, context, { includeDateTime: true });
|
|
||||||
expect(result).toContain("2024-01-29");
|
|
||||||
expect(result).toContain("14:30");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should include additional instructions", () => {
|
|
||||||
const result = service.buildSystemPrompt(mockPersonality, undefined, {
|
|
||||||
additionalInstructions: "Be concise.",
|
|
||||||
});
|
|
||||||
expect(result).toContain("Be concise.");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("validateTemplate", () => {
|
|
||||||
it("should validate known variables", () => {
|
|
||||||
const template = "Hello {{userName}}, welcome to {{workspaceName}}!";
|
|
||||||
const result = service.validateTemplate(template);
|
|
||||||
expect(result.valid).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should report unknown variables", () => {
|
|
||||||
const template = "Hello {{unknownVar}}!";
|
|
||||||
const result = service.validateTemplate(template);
|
|
||||||
expect(result.valid).toBe(false);
|
|
||||||
expect(result.missingVariables).toContain("unknownVar");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow custom_ prefixed variables", () => {
|
|
||||||
const template = "Value: {{custom_myVar}}";
|
|
||||||
const result = service.validateTemplate(template);
|
|
||||||
expect(result.valid).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("getFormalityLevels", () => {
|
|
||||||
it("should return all formality levels", () => {
|
|
||||||
const levels = service.getFormalityLevels();
|
|
||||||
expect(levels).toHaveLength(5);
|
|
||||||
expect(levels.map((l) => l.level)).toContain("FORMAL");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
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,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user