feat(#82): Implement personality module #107
@@ -1,4 +1,38 @@
|
|||||||
import { PartialType } from "@nestjs/mapped-types";
|
import { IsString, IsOptional, IsBoolean, MinLength, MaxLength, IsIn } from "class-validator";
|
||||||
import { CreatePersonalityDto } from "./create-personality.dto";
|
import { FORMALITY_LEVELS, FormalityLevelType } from "./create-personality.dto";
|
||||||
|
|
||||||
export class UpdatePersonalityDto extends PartialType(CreatePersonalityDto) {}
|
export class UpdatePersonalityDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(100)
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(500)
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
@MaxLength(50)
|
||||||
|
tone?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(FORMALITY_LEVELS)
|
||||||
|
formalityLevel?: FormalityLevelType;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MinLength(10)
|
||||||
|
systemPromptTemplate?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isDefault?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,29 +2,24 @@ 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 { AuthGuard } from "../auth/guards/auth.guard";
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||||
import { CreatePersonalityDto, UpdatePersonalityDto } from "./dto";
|
|
||||||
|
|
||||||
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 mockRequest = {
|
|
||||||
user: { id: mockUserId },
|
|
||||||
workspaceId: mockWorkspaceId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockPersonality = {
|
const mockPersonality = {
|
||||||
id: mockPersonalityId,
|
id: mockPersonalityId,
|
||||||
workspaceId: mockWorkspaceId,
|
workspaceId: mockWorkspaceId,
|
||||||
name: "Professional",
|
name: "Professional",
|
||||||
description: "Professional communication style",
|
|
||||||
tone: "professional",
|
tone: "professional",
|
||||||
formalityLevel: "FORMAL" as const,
|
formalityLevel: "FORMAL",
|
||||||
systemPromptTemplate: "You are a professional assistant.",
|
systemPromptTemplate: "You are a professional assistant.",
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
@@ -41,105 +36,82 @@ describe("PersonalitiesController", () => {
|
|||||||
remove: vi.fn(),
|
remove: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockAuthGuard = {
|
const mockPromptFormatterService = {
|
||||||
canActivate: vi.fn().mockReturnValue(true),
|
formatPrompt: vi.fn(),
|
||||||
|
getFormalityLevels: 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: PersonalitiesService,
|
{ provide: PromptFormatterService, useValue: mockPromptFormatterService },
|
||||||
useValue: mockPersonalitiesService,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideGuard(AuthGuard)
|
.overrideGuard(AuthGuard)
|
||||||
.useValue(mockAuthGuard)
|
.useValue({ canActivate: () => true })
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
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 () => {
|
||||||
const mockPersonalities = [mockPersonality];
|
|
||||||
mockPersonalitiesService.findAll.mockResolvedValue(mockPersonalities);
|
|
||||||
|
|
||||||
const result = await controller.findAll(mockRequest as any);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonalities);
|
|
||||||
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should filter by active status", async () => {
|
|
||||||
mockPersonalitiesService.findAll.mockResolvedValue([mockPersonality]);
|
mockPersonalitiesService.findAll.mockResolvedValue([mockPersonality]);
|
||||||
|
const result = await controller.findAll(mockRequest);
|
||||||
await controller.findAll(mockRequest as any, false);
|
expect(result).toEqual([mockPersonality]);
|
||||||
|
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, undefined);
|
||||||
expect(service.findAll).toHaveBeenCalledWith(mockWorkspaceId, false);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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 as any, mockPersonalityId);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonality);
|
expect(result).toEqual(mockPersonality);
|
||||||
expect(service.findOne).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
const result = await controller.findDefault(mockRequest as any);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonality);
|
expect(result).toEqual(mockPersonality);
|
||||||
expect(service.findDefault).toHaveBeenCalledWith(mockWorkspaceId);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getFormalityLevels", () => {
|
||||||
|
it("should return formality levels", () => {
|
||||||
|
const levels = [{ level: "FORMAL", description: "Professional" }];
|
||||||
|
mockPromptFormatterService.getFormalityLevels.mockReturnValue(levels);
|
||||||
|
const result = controller.getFormalityLevels();
|
||||||
|
expect(result).toEqual(levels);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
const createDto: CreatePersonalityDto = {
|
|
||||||
name: "Casual",
|
|
||||||
description: "Casual communication style",
|
|
||||||
tone: "casual",
|
|
||||||
formalityLevel: "CASUAL",
|
|
||||||
systemPromptTemplate: "You are a casual assistant.",
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should create a new personality", async () => {
|
it("should create a new personality", async () => {
|
||||||
const newPersonality = { ...mockPersonality, ...createDto, id: "new-id" };
|
const createDto = {
|
||||||
mockPersonalitiesService.create.mockResolvedValue(newPersonality);
|
name: "Casual",
|
||||||
|
tone: "casual",
|
||||||
const result = await controller.create(mockRequest as any, createDto);
|
formalityLevel: "CASUAL" as const,
|
||||||
|
systemPromptTemplate: "You are a casual assistant.",
|
||||||
expect(result).toEqual(newPersonality);
|
};
|
||||||
|
mockPersonalitiesService.create.mockResolvedValue({ ...mockPersonality, ...createDto });
|
||||||
|
await controller.create(mockRequest, createDto);
|
||||||
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
|
expect(service.create).toHaveBeenCalledWith(mockWorkspaceId, createDto);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("update", () => {
|
describe("update", () => {
|
||||||
const updateDto: UpdatePersonalityDto = {
|
|
||||||
description: "Updated description",
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should update a personality", async () => {
|
it("should update a personality", async () => {
|
||||||
const updatedPersonality = { ...mockPersonality, ...updateDto };
|
const updateDto = { description: "Updated" };
|
||||||
mockPersonalitiesService.update.mockResolvedValue(updatedPersonality);
|
mockPersonalitiesService.update.mockResolvedValue({ ...mockPersonality, ...updateDto });
|
||||||
|
await controller.update(mockRequest, mockPersonalityId, updateDto);
|
||||||
const result = await controller.update(mockRequest as any, mockPersonalityId, updateDto);
|
|
||||||
|
|
||||||
expect(result).toEqual(updatedPersonality);
|
|
||||||
expect(service.update).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId, updateDto);
|
expect(service.update).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId, updateDto);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -147,11 +119,21 @@ describe("PersonalitiesController", () => {
|
|||||||
describe("remove", () => {
|
describe("remove", () => {
|
||||||
it("should delete a personality", async () => {
|
it("should delete a personality", async () => {
|
||||||
mockPersonalitiesService.remove.mockResolvedValue(mockPersonality);
|
mockPersonalitiesService.remove.mockResolvedValue(mockPersonality);
|
||||||
|
await controller.remove(mockRequest, mockPersonalityId);
|
||||||
const result = await controller.remove(mockRequest as any, mockPersonalityId);
|
|
||||||
|
|
||||||
expect(result).toEqual(mockPersonality);
|
|
||||||
expect(service.remove).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
expect(service.remove).toHaveBeenCalledWith(mockWorkspaceId, mockPersonalityId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("previewPrompt", () => {
|
||||||
|
it("should return formatted system prompt", async () => {
|
||||||
|
const context = { userName: "John" };
|
||||||
|
mockPersonalitiesService.findOne.mockResolvedValue(mockPersonality);
|
||||||
|
mockPromptFormatterService.formatPrompt.mockReturnValue({
|
||||||
|
systemPrompt: "Formatted prompt",
|
||||||
|
metadata: {},
|
||||||
|
});
|
||||||
|
const result = await controller.previewPrompt(mockRequest, mockPersonalityId, context);
|
||||||
|
expect(result.systemPrompt).toBe("Formatted prompt");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,69 +9,81 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
Req,
|
Req,
|
||||||
|
ParseBoolPipe,
|
||||||
|
HttpCode,
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface AuthenticatedRequest {
|
||||||
|
user: { id: string };
|
||||||
|
workspaceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Controller("personalities")
|
@Controller("personalities")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
export class PersonalitiesController {
|
export class PersonalitiesController {
|
||||||
constructor(private readonly personalitiesService: PersonalitiesService) {}
|
constructor(
|
||||||
|
private readonly personalitiesService: PersonalitiesService,
|
||||||
|
private readonly promptFormatter: PromptFormatterService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all personalities for the current workspace
|
|
||||||
*/
|
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(
|
async findAll(
|
||||||
@Req() req: any,
|
@Req() req: AuthenticatedRequest,
|
||||||
@Query("isActive") isActive: boolean = true,
|
@Query("isActive", new ParseBoolPipe({ optional: true })) isActive?: boolean,
|
||||||
): Promise<Personality[]> {
|
): Promise<Personality[]> {
|
||||||
return this.personalitiesService.findAll(req.workspaceId, isActive);
|
return this.personalitiesService.findAll(req.workspaceId, isActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the default personality for the current workspace
|
|
||||||
*/
|
|
||||||
@Get("default")
|
@Get("default")
|
||||||
async findDefault(@Req() req: any): Promise<Personality> {
|
async findDefault(@Req() req: AuthenticatedRequest): Promise<Personality> {
|
||||||
return this.personalitiesService.findDefault(req.workspaceId);
|
return this.personalitiesService.findDefault(req.workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Get("formality-levels")
|
||||||
* Get a specific personality by ID
|
getFormalityLevels(): Array<{ level: string; description: string }> {
|
||||||
*/
|
return this.promptFormatter.getFormalityLevels();
|
||||||
|
}
|
||||||
|
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
async findOne(@Req() req: any, @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()
|
||||||
async create(@Req() req: any, @Body() dto: CreatePersonalityDto): Promise<Personality> {
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
async create(@Req() req: AuthenticatedRequest, @Body() dto: CreatePersonalityDto): Promise<Personality> {
|
||||||
return this.personalitiesService.create(req.workspaceId, dto);
|
return this.personalitiesService.create(req.workspaceId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing personality
|
|
||||||
*/
|
|
||||||
@Put(":id")
|
@Put(":id")
|
||||||
async update(
|
async update(
|
||||||
@Req() req: any,
|
@Req() req: AuthenticatedRequest,
|
||||||
@Param("id") id: string,
|
@Param("id") id: string,
|
||||||
@Body() dto: UpdatePersonalityDto,
|
@Body() dto: UpdatePersonalityDto,
|
||||||
): Promise<Personality> {
|
): Promise<Personality> {
|
||||||
return this.personalitiesService.update(req.workspaceId, id, dto);
|
return this.personalitiesService.update(req.workspaceId, id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a personality
|
|
||||||
*/
|
|
||||||
@Delete(":id")
|
@Delete(":id")
|
||||||
async remove(@Req() req: any, @Param("id") id: string): Promise<Personality> {
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async remove(@Req() req: AuthenticatedRequest, @Param("id") id: string): Promise<Personality> {
|
||||||
return this.personalitiesService.remove(req.workspaceId, id);
|
return this.personalitiesService.remove(req.workspaceId, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(":id/preview")
|
||||||
|
async previewPrompt(
|
||||||
|
@Req() req: AuthenticatedRequest,
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() context?: PromptContext,
|
||||||
|
): Promise<{ systemPrompt: string }> {
|
||||||
|
const personality = await this.personalitiesService.findOne(req.workspaceId, id);
|
||||||
|
const { systemPrompt } = this.promptFormatter.formatPrompt(personality, context);
|
||||||
|
return { systemPrompt };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ 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],
|
providers: [PersonalitiesService, PromptFormatterService],
|
||||||
exports: [PersonalitiesService],
|
exports: [PersonalitiesService, PromptFormatterService],
|
||||||
})
|
})
|
||||||
export class PersonalitiesModule {}
|
export class PersonalitiesModule {}
|
||||||
|
|||||||
1
apps/api/src/personalities/services/index.ts
Normal file
1
apps/api/src/personalities/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./prompt-formatter.service";
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
137
apps/api/src/personalities/services/prompt-formatter.service.ts
Normal file
137
apps/api/src/personalities/services/prompt-formatter.service.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
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 = context?.currentDate ?? now.toISOString().split("T")[0];
|
||||||
|
const timeStr = context?.currentTime ?? now.toTimeString().slice(0, 5);
|
||||||
|
const tzStr = 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)) {
|
||||||
|
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 = Array.from(matches, (m) => m[1]);
|
||||||
|
|
||||||
|
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(): Array<{ level: FormalityLevel; description: string }> {
|
||||||
|
return Object.entries(FORMALITY_MODIFIERS).map(([level, description]) => ({
|
||||||
|
level: level as FormalityLevel,
|
||||||
|
description,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user