feat(#133): add workspace-scoped LLM configuration
Implement per-workspace LLM provider and personality configuration with proper hierarchy (workspace > user > system fallback). Schema: - Add WorkspaceLlmSettings model with provider/personality FKs - One-to-one relation with Workspace - JSON settings field for extensibility Service: - getSettings: Retrieves/creates workspace settings - updateSettings: Updates with null value support - getEffectiveLlmProvider: Hierarchy-based provider selection - getEffectivePersonality: Hierarchy-based personality selection Endpoints: - GET /workspaces/:id/settings/llm - Get settings - PATCH /workspaces/:id/settings/llm - Update settings - GET /workspaces/:id/settings/llm/effective-provider - GET /workspaces/:id/settings/llm/effective-personality Configuration hierarchy: 1. Workspace-configured provider/personality 2. User-specific provider (for providers) 3. System default fallback Tests: 34 passing with 100% coverage Fixes #133 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -214,6 +214,7 @@ model Workspace {
|
|||||||
knowledgeTags KnowledgeTag[]
|
knowledgeTags KnowledgeTag[]
|
||||||
cronSchedules CronSchedule[]
|
cronSchedules CronSchedule[]
|
||||||
personalities Personality[]
|
personalities Personality[]
|
||||||
|
llmSettings WorkspaceLlmSettings?
|
||||||
|
|
||||||
@@index([ownerId])
|
@@index([ownerId])
|
||||||
@@map("workspaces")
|
@@map("workspaces")
|
||||||
@@ -942,7 +943,8 @@ model Personality {
|
|||||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
llmProviderInstance LlmProviderInstance? @relation("PersonalityLlmProvider", fields: [llmProviderInstanceId], references: [id], onDelete: SetNull)
|
llmProviderInstance LlmProviderInstance? @relation("PersonalityLlmProvider", fields: [llmProviderInstanceId], references: [id], onDelete: SetNull)
|
||||||
|
workspaceLlmSettings WorkspaceLlmSettings[] @relation("WorkspacePersonality")
|
||||||
|
|
||||||
@@unique([id, workspaceId])
|
@@unique([id, workspaceId])
|
||||||
@@unique([workspaceId, name])
|
@@unique([workspaceId, name])
|
||||||
@@ -969,8 +971,9 @@ 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")
|
personalities Personality[] @relation("PersonalityLlmProvider")
|
||||||
|
workspaceLlmSettings WorkspaceLlmSettings[] @relation("WorkspaceLlmProvider")
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([providerType])
|
@@index([providerType])
|
||||||
@@ -978,3 +981,25 @@ model LlmProviderInstance {
|
|||||||
@@index([isEnabled])
|
@@index([isEnabled])
|
||||||
@@map("llm_provider_instances")
|
@@map("llm_provider_instances")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// WORKSPACE LLM SETTINGS
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
model WorkspaceLlmSettings {
|
||||||
|
id String @id @default(uuid()) @db.Uuid
|
||||||
|
workspaceId String @unique @map("workspace_id") @db.Uuid
|
||||||
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
defaultLlmProviderId String? @map("default_llm_provider_id") @db.Uuid
|
||||||
|
defaultLlmProvider LlmProviderInstance? @relation("WorkspaceLlmProvider", fields: [defaultLlmProviderId], references: [id], onDelete: SetNull)
|
||||||
|
defaultPersonalityId String? @map("default_personality_id") @db.Uuid
|
||||||
|
defaultPersonality Personality? @relation("WorkspacePersonality", fields: [defaultPersonalityId], references: [id], onDelete: SetNull)
|
||||||
|
settings Json? @default("{}")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
||||||
|
|
||||||
|
@@index([workspaceId])
|
||||||
|
@@index([defaultLlmProviderId])
|
||||||
|
@@index([defaultPersonalityId])
|
||||||
|
@@map("workspace_llm_settings")
|
||||||
|
}
|
||||||
|
|||||||
1
apps/api/src/workspace-settings/dto/index.ts
Normal file
1
apps/api/src/workspace-settings/dto/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { UpdateWorkspaceSettingsDto } from "./update-workspace-settings.dto";
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { IsOptional, IsUUID, IsObject } from "class-validator";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for updating workspace LLM settings
|
||||||
|
* All fields are optional to support partial updates
|
||||||
|
*/
|
||||||
|
export class UpdateWorkspaceSettingsDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID("4", { message: "defaultLlmProviderId must be a valid UUID" })
|
||||||
|
defaultLlmProviderId?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID("4", { message: "defaultPersonalityId must be a valid UUID" })
|
||||||
|
defaultPersonalityId?: string | null;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject({ message: "settings must be an object" })
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
}
|
||||||
4
apps/api/src/workspace-settings/index.ts
Normal file
4
apps/api/src/workspace-settings/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { WorkspaceSettingsModule } from "./workspace-settings.module";
|
||||||
|
export { WorkspaceSettingsService } from "./workspace-settings.service";
|
||||||
|
export { WorkspaceSettingsController } from "./workspace-settings.controller";
|
||||||
|
export * from "./dto";
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { ExecutionContext } from "@nestjs/common";
|
||||||
|
import { WorkspaceSettingsController } from "./workspace-settings.controller";
|
||||||
|
import { WorkspaceSettingsService } from "./workspace-settings.service";
|
||||||
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||||
|
import type { WorkspaceLlmSettings, LlmProviderInstance, Personality } from "@prisma/client";
|
||||||
|
import type { AuthenticatedRequest } from "../common/types/user.types";
|
||||||
|
|
||||||
|
describe("WorkspaceSettingsController", () => {
|
||||||
|
let controller: WorkspaceSettingsController;
|
||||||
|
let service: WorkspaceSettingsService;
|
||||||
|
|
||||||
|
const mockWorkspaceId = "workspace-123";
|
||||||
|
const mockUserId = "user-123";
|
||||||
|
|
||||||
|
const mockSettings: WorkspaceLlmSettings = {
|
||||||
|
id: "settings-123",
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
defaultLlmProviderId: "provider-123",
|
||||||
|
defaultPersonalityId: "personality-123",
|
||||||
|
settings: {},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProvider: LlmProviderInstance = {
|
||||||
|
id: "provider-123",
|
||||||
|
providerType: "ollama",
|
||||||
|
displayName: "Test Provider",
|
||||||
|
userId: null,
|
||||||
|
config: { endpoint: "http://localhost:11434" },
|
||||||
|
isDefault: true,
|
||||||
|
isEnabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPersonality: Personality = {
|
||||||
|
id: "personality-123",
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
name: "default",
|
||||||
|
displayName: "Default",
|
||||||
|
description: "Default personality",
|
||||||
|
systemPrompt: "You are a helpful assistant",
|
||||||
|
temperature: null,
|
||||||
|
maxTokens: null,
|
||||||
|
llmProviderInstanceId: null,
|
||||||
|
isDefault: true,
|
||||||
|
isEnabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockAuthGuard = {
|
||||||
|
canActivate: vi.fn((context: ExecutionContext) => {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
request.user = {
|
||||||
|
id: mockUserId,
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
emailVerified: true,
|
||||||
|
image: null,
|
||||||
|
authProviderId: null,
|
||||||
|
preferences: {},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockAuthRequest: AuthenticatedRequest = {
|
||||||
|
user: {
|
||||||
|
id: mockUserId,
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test User",
|
||||||
|
emailVerified: true,
|
||||||
|
image: null,
|
||||||
|
authProviderId: null,
|
||||||
|
preferences: {},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
} as AuthenticatedRequest;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [WorkspaceSettingsController],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: WorkspaceSettingsService,
|
||||||
|
useValue: {
|
||||||
|
getSettings: vi.fn(),
|
||||||
|
updateSettings: vi.fn(),
|
||||||
|
getEffectiveLlmProvider: vi.fn(),
|
||||||
|
getEffectivePersonality: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.overrideGuard(AuthGuard)
|
||||||
|
.useValue(mockAuthGuard)
|
||||||
|
.compile();
|
||||||
|
|
||||||
|
controller = module.get<WorkspaceSettingsController>(WorkspaceSettingsController);
|
||||||
|
service = module.get<WorkspaceSettingsService>(WorkspaceSettingsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getSettings", () => {
|
||||||
|
it("should return workspace settings", async () => {
|
||||||
|
vi.spyOn(service, "getSettings").mockResolvedValue(mockSettings);
|
||||||
|
|
||||||
|
const result = await controller.getSettings(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockSettings);
|
||||||
|
expect(service.getSettings).toHaveBeenCalledWith(mockWorkspaceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle service errors", async () => {
|
||||||
|
vi.spyOn(service, "getSettings").mockRejectedValue(new Error("Service error"));
|
||||||
|
|
||||||
|
await expect(controller.getSettings(mockWorkspaceId)).rejects.toThrow("Service error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with valid workspace ID", async () => {
|
||||||
|
vi.spyOn(service, "getSettings").mockResolvedValue(mockSettings);
|
||||||
|
|
||||||
|
const result = await controller.getSettings(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result.workspaceId).toBe(mockWorkspaceId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateSettings", () => {
|
||||||
|
it("should update workspace settings", async () => {
|
||||||
|
const updateDto = {
|
||||||
|
defaultLlmProviderId: "new-provider-123",
|
||||||
|
defaultPersonalityId: "new-personality-123",
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSettings = { ...mockSettings, ...updateDto };
|
||||||
|
vi.spyOn(service, "updateSettings").mockResolvedValue(updatedSettings);
|
||||||
|
|
||||||
|
const result = await controller.updateSettings(mockWorkspaceId, updateDto);
|
||||||
|
|
||||||
|
expect(result).toEqual(updatedSettings);
|
||||||
|
expect(service.updateSettings).toHaveBeenCalledWith(mockWorkspaceId, updateDto);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow partial updates", async () => {
|
||||||
|
const updateDto = {
|
||||||
|
defaultLlmProviderId: "new-provider-123",
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSettings = {
|
||||||
|
...mockSettings,
|
||||||
|
defaultLlmProviderId: updateDto.defaultLlmProviderId,
|
||||||
|
};
|
||||||
|
vi.spyOn(service, "updateSettings").mockResolvedValue(updatedSettings);
|
||||||
|
|
||||||
|
const result = await controller.updateSettings(mockWorkspaceId, updateDto);
|
||||||
|
|
||||||
|
expect(result.defaultLlmProviderId).toBe(updateDto.defaultLlmProviderId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle null values", async () => {
|
||||||
|
const updateDto = {
|
||||||
|
defaultLlmProviderId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSettings = { ...mockSettings, defaultLlmProviderId: null };
|
||||||
|
vi.spyOn(service, "updateSettings").mockResolvedValue(updatedSettings);
|
||||||
|
|
||||||
|
const result = await controller.updateSettings(mockWorkspaceId, updateDto);
|
||||||
|
|
||||||
|
expect(result.defaultLlmProviderId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle service errors", async () => {
|
||||||
|
const updateDto = { defaultLlmProviderId: "invalid-id" };
|
||||||
|
vi.spyOn(service, "updateSettings").mockRejectedValue(new Error("Provider not found"));
|
||||||
|
|
||||||
|
await expect(controller.updateSettings(mockWorkspaceId, updateDto)).rejects.toThrow(
|
||||||
|
"Provider not found"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEffectiveProvider", () => {
|
||||||
|
it("should return effective provider with authenticated user", async () => {
|
||||||
|
vi.spyOn(service, "getEffectiveLlmProvider").mockResolvedValue(mockProvider);
|
||||||
|
|
||||||
|
const result = await controller.getEffectiveProvider(mockWorkspaceId, mockAuthRequest);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockProvider);
|
||||||
|
expect(service.getEffectiveLlmProvider).toHaveBeenCalledWith(mockWorkspaceId, mockUserId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return effective provider without user ID when not authenticated", async () => {
|
||||||
|
const unauthRequest = { user: undefined } as AuthenticatedRequest;
|
||||||
|
vi.spyOn(service, "getEffectiveLlmProvider").mockResolvedValue(mockProvider);
|
||||||
|
|
||||||
|
const result = await controller.getEffectiveProvider(mockWorkspaceId, unauthRequest);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockProvider);
|
||||||
|
expect(service.getEffectiveLlmProvider).toHaveBeenCalledWith(mockWorkspaceId, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle no provider available error", async () => {
|
||||||
|
vi.spyOn(service, "getEffectiveLlmProvider").mockRejectedValue(
|
||||||
|
new Error("No LLM provider available")
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.getEffectiveProvider(mockWorkspaceId, mockAuthRequest)
|
||||||
|
).rejects.toThrow("No LLM provider available");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass user ID to service when available", async () => {
|
||||||
|
vi.spyOn(service, "getEffectiveLlmProvider").mockResolvedValue(mockProvider);
|
||||||
|
|
||||||
|
await controller.getEffectiveProvider(mockWorkspaceId, mockAuthRequest);
|
||||||
|
|
||||||
|
expect(service.getEffectiveLlmProvider).toHaveBeenCalledWith(mockWorkspaceId, mockUserId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEffectivePersonality", () => {
|
||||||
|
it("should return effective personality", async () => {
|
||||||
|
vi.spyOn(service, "getEffectivePersonality").mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
|
const result = await controller.getEffectivePersonality(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPersonality);
|
||||||
|
expect(service.getEffectivePersonality).toHaveBeenCalledWith(mockWorkspaceId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle no personality available error", async () => {
|
||||||
|
vi.spyOn(service, "getEffectivePersonality").mockRejectedValue(
|
||||||
|
new Error("No personality available")
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(controller.getEffectivePersonality(mockWorkspaceId)).rejects.toThrow(
|
||||||
|
"No personality available"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should work with valid workspace ID", async () => {
|
||||||
|
vi.spyOn(service, "getEffectivePersonality").mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
|
const result = await controller.getEffectivePersonality(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result.workspaceId).toBe(mockWorkspaceId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("endpoint paths", () => {
|
||||||
|
it("should be accessible at /workspaces/:workspaceId/settings/llm", () => {
|
||||||
|
const metadata = Reflect.getMetadata("path", WorkspaceSettingsController);
|
||||||
|
expect(metadata).toBe("workspaces/:workspaceId/settings/llm");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { Controller, Get, Patch, Body, Param, Request, UseGuards } from "@nestjs/common";
|
||||||
|
import { WorkspaceSettingsService } from "./workspace-settings.service";
|
||||||
|
import { UpdateWorkspaceSettingsDto } from "./dto";
|
||||||
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||||
|
import type { AuthenticatedRequest } from "../common/types/user.types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for workspace LLM settings endpoints
|
||||||
|
* All endpoints require authentication
|
||||||
|
*/
|
||||||
|
@Controller("workspaces/:workspaceId/settings/llm")
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
export class WorkspaceSettingsController {
|
||||||
|
constructor(private readonly workspaceSettingsService: WorkspaceSettingsService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/workspaces/:workspaceId/settings/llm
|
||||||
|
* Get workspace LLM settings
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
async getSettings(@Param("workspaceId") workspaceId: string) {
|
||||||
|
return this.workspaceSettingsService.getSettings(workspaceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH /api/workspaces/:workspaceId/settings/llm
|
||||||
|
* Update workspace LLM settings
|
||||||
|
*/
|
||||||
|
@Patch()
|
||||||
|
async updateSettings(
|
||||||
|
@Param("workspaceId") workspaceId: string,
|
||||||
|
@Body() dto: UpdateWorkspaceSettingsDto
|
||||||
|
) {
|
||||||
|
return this.workspaceSettingsService.updateSettings(workspaceId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/workspaces/:workspaceId/settings/llm/effective-provider
|
||||||
|
* Get effective LLM provider for workspace
|
||||||
|
*/
|
||||||
|
@Get("effective-provider")
|
||||||
|
async getEffectiveProvider(
|
||||||
|
@Param("workspaceId") workspaceId: string,
|
||||||
|
@Request() req: AuthenticatedRequest
|
||||||
|
) {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
return this.workspaceSettingsService.getEffectiveLlmProvider(workspaceId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/workspaces/:workspaceId/settings/llm/effective-personality
|
||||||
|
* Get effective personality for workspace
|
||||||
|
*/
|
||||||
|
@Get("effective-personality")
|
||||||
|
async getEffectivePersonality(@Param("workspaceId") workspaceId: string) {
|
||||||
|
return this.workspaceSettingsService.getEffectivePersonality(workspaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/api/src/workspace-settings/workspace-settings.module.ts
Normal file
12
apps/api/src/workspace-settings/workspace-settings.module.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { WorkspaceSettingsController } from "./workspace-settings.controller";
|
||||||
|
import { WorkspaceSettingsService } from "./workspace-settings.service";
|
||||||
|
import { PrismaModule } from "../prisma/prisma.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
controllers: [WorkspaceSettingsController],
|
||||||
|
providers: [WorkspaceSettingsService],
|
||||||
|
exports: [WorkspaceSettingsService],
|
||||||
|
})
|
||||||
|
export class WorkspaceSettingsModule {}
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { WorkspaceSettingsService } from "./workspace-settings.service";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import type { WorkspaceLlmSettings, LlmProviderInstance, Personality } from "@prisma/client";
|
||||||
|
|
||||||
|
describe("WorkspaceSettingsService", () => {
|
||||||
|
let service: WorkspaceSettingsService;
|
||||||
|
let prisma: PrismaService;
|
||||||
|
|
||||||
|
const mockWorkspaceId = "workspace-123";
|
||||||
|
const mockUserId = "user-123";
|
||||||
|
|
||||||
|
const mockSettings: WorkspaceLlmSettings = {
|
||||||
|
id: "settings-123",
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
defaultLlmProviderId: "provider-123",
|
||||||
|
defaultPersonalityId: "personality-123",
|
||||||
|
settings: {},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProvider: LlmProviderInstance = {
|
||||||
|
id: "provider-123",
|
||||||
|
providerType: "ollama",
|
||||||
|
displayName: "Test Provider",
|
||||||
|
userId: null,
|
||||||
|
config: { endpoint: "http://localhost:11434" },
|
||||||
|
isDefault: true,
|
||||||
|
isEnabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockUserProvider: LlmProviderInstance = {
|
||||||
|
id: "user-provider-123",
|
||||||
|
providerType: "ollama",
|
||||||
|
displayName: "User Provider",
|
||||||
|
userId: mockUserId,
|
||||||
|
config: { endpoint: "http://user-ollama:11434" },
|
||||||
|
isDefault: false,
|
||||||
|
isEnabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPersonality: Personality = {
|
||||||
|
id: "personality-123",
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
name: "default",
|
||||||
|
displayName: "Default",
|
||||||
|
description: "Default personality",
|
||||||
|
systemPrompt: "You are a helpful assistant",
|
||||||
|
temperature: null,
|
||||||
|
maxTokens: null,
|
||||||
|
llmProviderInstanceId: null,
|
||||||
|
isDefault: true,
|
||||||
|
isEnabled: true,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
WorkspaceSettingsService,
|
||||||
|
{
|
||||||
|
provide: PrismaService,
|
||||||
|
useValue: {
|
||||||
|
workspaceLlmSettings: {
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
create: vi.fn(),
|
||||||
|
update: vi.fn(),
|
||||||
|
},
|
||||||
|
llmProviderInstance: {
|
||||||
|
findFirst: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
},
|
||||||
|
personality: {
|
||||||
|
findFirst: vi.fn(),
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<WorkspaceSettingsService>(WorkspaceSettingsService);
|
||||||
|
prisma = module.get<PrismaService>(PrismaService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getSettings", () => {
|
||||||
|
it("should return existing settings for workspace", async () => {
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
|
||||||
|
|
||||||
|
const result = await service.getSettings(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockSettings);
|
||||||
|
expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { workspaceId: mockWorkspaceId },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create default settings if not exists", async () => {
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(null);
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "create").mockResolvedValue(mockSettings);
|
||||||
|
|
||||||
|
const result = await service.getSettings(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockSettings);
|
||||||
|
expect(prisma.workspaceLlmSettings.create).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
settings: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle workspace with no settings gracefully", async () => {
|
||||||
|
const newSettings = {
|
||||||
|
...mockSettings,
|
||||||
|
defaultLlmProviderId: null,
|
||||||
|
defaultPersonalityId: null,
|
||||||
|
};
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(null);
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "create").mockResolvedValue(newSettings);
|
||||||
|
|
||||||
|
const result = await service.getSettings(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.workspaceId).toBe(mockWorkspaceId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updateSettings", () => {
|
||||||
|
it("should update existing settings", async () => {
|
||||||
|
const updateDto = {
|
||||||
|
defaultLlmProviderId: "new-provider-123",
|
||||||
|
defaultPersonalityId: "new-personality-123",
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSettings = { ...mockSettings, ...updateDto };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "update").mockResolvedValue(updatedSettings);
|
||||||
|
|
||||||
|
const result = await service.updateSettings(mockWorkspaceId, updateDto);
|
||||||
|
|
||||||
|
expect(result).toEqual(updatedSettings);
|
||||||
|
expect(prisma.workspaceLlmSettings.update).toHaveBeenCalledWith({
|
||||||
|
where: { workspaceId: mockWorkspaceId },
|
||||||
|
data: updateDto,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow setting provider to null", async () => {
|
||||||
|
const updateDto = {
|
||||||
|
defaultLlmProviderId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSettings = { ...mockSettings, defaultLlmProviderId: null };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "update").mockResolvedValue(updatedSettings);
|
||||||
|
|
||||||
|
const result = await service.updateSettings(mockWorkspaceId, updateDto);
|
||||||
|
|
||||||
|
expect(result.defaultLlmProviderId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow setting personality to null", async () => {
|
||||||
|
const updateDto = {
|
||||||
|
defaultPersonalityId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSettings = { ...mockSettings, defaultPersonalityId: null };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "update").mockResolvedValue(updatedSettings);
|
||||||
|
|
||||||
|
const result = await service.updateSettings(mockWorkspaceId, updateDto);
|
||||||
|
|
||||||
|
expect(result.defaultPersonalityId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should update custom settings object", async () => {
|
||||||
|
const updateDto = {
|
||||||
|
settings: { customKey: "customValue" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedSettings = { ...mockSettings, settings: updateDto.settings };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "update").mockResolvedValue(updatedSettings);
|
||||||
|
|
||||||
|
const result = await service.updateSettings(mockWorkspaceId, updateDto);
|
||||||
|
|
||||||
|
expect(result.settings).toEqual(updateDto.settings);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEffectiveLlmProvider", () => {
|
||||||
|
it("should return workspace provider when set", async () => {
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
|
||||||
|
vi.spyOn(prisma.llmProviderInstance, "findUnique").mockResolvedValue(mockProvider);
|
||||||
|
|
||||||
|
const result = await service.getEffectiveLlmProvider(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockProvider);
|
||||||
|
expect(prisma.llmProviderInstance.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { id: mockSettings.defaultLlmProviderId! },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return user provider when workspace provider not set and userId provided", async () => {
|
||||||
|
const settingsWithoutProvider = { ...mockSettings, defaultLlmProviderId: null };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
|
||||||
|
settingsWithoutProvider
|
||||||
|
);
|
||||||
|
vi.spyOn(prisma.llmProviderInstance, "findFirst")
|
||||||
|
.mockResolvedValueOnce(mockUserProvider)
|
||||||
|
.mockResolvedValueOnce(null);
|
||||||
|
|
||||||
|
const result = await service.getEffectiveLlmProvider(mockWorkspaceId, mockUserId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockUserProvider);
|
||||||
|
expect(prisma.llmProviderInstance.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
userId: mockUserId,
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to system default when workspace and user providers not set", async () => {
|
||||||
|
const settingsWithoutProvider = { ...mockSettings, defaultLlmProviderId: null };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
|
||||||
|
settingsWithoutProvider
|
||||||
|
);
|
||||||
|
vi.spyOn(prisma.llmProviderInstance, "findFirst")
|
||||||
|
.mockResolvedValueOnce(null) // No user provider
|
||||||
|
.mockResolvedValueOnce(mockProvider); // System default
|
||||||
|
|
||||||
|
const result = await service.getEffectiveLlmProvider(mockWorkspaceId, mockUserId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockProvider);
|
||||||
|
expect(prisma.llmProviderInstance.findFirst).toHaveBeenNthCalledWith(2, {
|
||||||
|
where: {
|
||||||
|
userId: null,
|
||||||
|
isDefault: true,
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when no provider available", async () => {
|
||||||
|
const settingsWithoutProvider = { ...mockSettings, defaultLlmProviderId: null };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
|
||||||
|
settingsWithoutProvider
|
||||||
|
);
|
||||||
|
vi.spyOn(prisma.llmProviderInstance, "findFirst").mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.getEffectiveLlmProvider(mockWorkspaceId)).rejects.toThrow(
|
||||||
|
`No LLM provider available for workspace ${mockWorkspaceId}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when workspace provider is set but not found", async () => {
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
|
||||||
|
vi.spyOn(prisma.llmProviderInstance, "findUnique").mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.getEffectiveLlmProvider(mockWorkspaceId)).rejects.toThrow(
|
||||||
|
`LLM provider ${mockSettings.defaultLlmProviderId} not found`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getEffectivePersonality", () => {
|
||||||
|
it("should return workspace personality when set", async () => {
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
|
||||||
|
vi.spyOn(prisma.personality, "findUnique").mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
|
const result = await service.getEffectivePersonality(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPersonality);
|
||||||
|
expect(prisma.personality.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
id: mockSettings.defaultPersonalityId!,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to default personality when workspace personality not set", async () => {
|
||||||
|
const settingsWithoutPersonality = { ...mockSettings, defaultPersonalityId: null };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
|
||||||
|
settingsWithoutPersonality
|
||||||
|
);
|
||||||
|
vi.spyOn(prisma.personality, "findFirst").mockResolvedValue(mockPersonality);
|
||||||
|
|
||||||
|
const result = await service.getEffectivePersonality(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(mockPersonality);
|
||||||
|
expect(prisma.personality.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
isDefault: true,
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to any enabled personality when no default exists", async () => {
|
||||||
|
const settingsWithoutPersonality = { ...mockSettings, defaultPersonalityId: null };
|
||||||
|
const nonDefaultPersonality = { ...mockPersonality, isDefault: false };
|
||||||
|
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
|
||||||
|
settingsWithoutPersonality
|
||||||
|
);
|
||||||
|
vi.spyOn(prisma.personality, "findFirst")
|
||||||
|
.mockResolvedValueOnce(null) // No default personality
|
||||||
|
.mockResolvedValueOnce(nonDefaultPersonality); // Any enabled personality
|
||||||
|
|
||||||
|
const result = await service.getEffectivePersonality(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(result).toEqual(nonDefaultPersonality);
|
||||||
|
expect(prisma.personality.findFirst).toHaveBeenNthCalledWith(2, {
|
||||||
|
where: {
|
||||||
|
workspaceId: mockWorkspaceId,
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when no personality available", async () => {
|
||||||
|
const settingsWithoutPersonality = { ...mockSettings, defaultPersonalityId: null };
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(
|
||||||
|
settingsWithoutPersonality
|
||||||
|
);
|
||||||
|
vi.spyOn(prisma.personality, "findFirst").mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.getEffectivePersonality(mockWorkspaceId)).rejects.toThrow(
|
||||||
|
`No personality available for workspace ${mockWorkspaceId}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when workspace personality is set but not found", async () => {
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
|
||||||
|
vi.spyOn(prisma.personality, "findUnique").mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(service.getEffectivePersonality(mockWorkspaceId)).rejects.toThrow(
|
||||||
|
`Personality ${mockSettings.defaultPersonalityId} not found`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("workspace isolation", () => {
|
||||||
|
it("should only access settings for specified workspace", async () => {
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(mockSettings);
|
||||||
|
|
||||||
|
await service.getSettings(mockWorkspaceId);
|
||||||
|
|
||||||
|
expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { workspaceId: mockWorkspaceId },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not allow cross-workspace settings access", async () => {
|
||||||
|
const otherWorkspaceId = "other-workspace-123";
|
||||||
|
|
||||||
|
vi.spyOn(prisma.workspaceLlmSettings, "findUnique").mockResolvedValue(null);
|
||||||
|
|
||||||
|
const result1 = await service.getSettings(mockWorkspaceId);
|
||||||
|
const result2 = await service.getSettings(otherWorkspaceId);
|
||||||
|
|
||||||
|
// Each workspace should have separate calls
|
||||||
|
expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledTimes(2);
|
||||||
|
expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { workspaceId: mockWorkspaceId },
|
||||||
|
});
|
||||||
|
expect(prisma.workspaceLlmSettings.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { workspaceId: otherWorkspaceId },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
187
apps/api/src/workspace-settings/workspace-settings.service.ts
Normal file
187
apps/api/src/workspace-settings/workspace-settings.service.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import type { WorkspaceLlmSettings, LlmProviderInstance, Personality } from "@prisma/client";
|
||||||
|
import type { UpdateWorkspaceSettingsDto } from "./dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for managing workspace LLM settings
|
||||||
|
* Handles configuration hierarchy: workspace > user > system
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class WorkspaceSettingsService {
|
||||||
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings for a workspace (creates default if not exists)
|
||||||
|
*
|
||||||
|
* @param workspaceId - Workspace ID
|
||||||
|
* @returns Workspace LLM settings
|
||||||
|
*/
|
||||||
|
async getSettings(workspaceId: string): Promise<WorkspaceLlmSettings> {
|
||||||
|
let settings = await this.prisma.workspaceLlmSettings.findUnique({
|
||||||
|
where: { workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create default settings if they don't exist
|
||||||
|
settings ??= await this.prisma.workspaceLlmSettings.create({
|
||||||
|
data: {
|
||||||
|
workspaceId,
|
||||||
|
settings: {} as unknown as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update workspace LLM settings
|
||||||
|
*
|
||||||
|
* @param workspaceId - Workspace ID
|
||||||
|
* @param dto - Update data
|
||||||
|
* @returns Updated settings
|
||||||
|
*/
|
||||||
|
async updateSettings(
|
||||||
|
workspaceId: string,
|
||||||
|
dto: UpdateWorkspaceSettingsDto
|
||||||
|
): Promise<WorkspaceLlmSettings> {
|
||||||
|
const data: Prisma.WorkspaceLlmSettingsUncheckedUpdateInput = {};
|
||||||
|
|
||||||
|
if (dto.defaultLlmProviderId !== undefined) {
|
||||||
|
data.defaultLlmProviderId = dto.defaultLlmProviderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.defaultPersonalityId !== undefined) {
|
||||||
|
data.defaultPersonalityId = dto.defaultPersonalityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.settings !== undefined) {
|
||||||
|
data.settings = dto.settings as unknown as Prisma.InputJsonValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await this.prisma.workspaceLlmSettings.update({
|
||||||
|
where: { workspaceId },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get effective LLM provider for a workspace
|
||||||
|
* Priority: workspace > user > system default
|
||||||
|
*
|
||||||
|
* @param workspaceId - Workspace ID
|
||||||
|
* @param userId - Optional user ID for user-level provider
|
||||||
|
* @returns Effective LLM provider instance
|
||||||
|
* @throws {Error} If no provider available
|
||||||
|
*/
|
||||||
|
async getEffectiveLlmProvider(
|
||||||
|
workspaceId: string,
|
||||||
|
userId?: string
|
||||||
|
): Promise<LlmProviderInstance> {
|
||||||
|
// Get workspace settings
|
||||||
|
const settings = await this.prisma.workspaceLlmSettings.findUnique({
|
||||||
|
where: { workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Check workspace-level provider
|
||||||
|
if (settings?.defaultLlmProviderId) {
|
||||||
|
const provider = await this.prisma.llmProviderInstance.findUnique({
|
||||||
|
where: { id: settings.defaultLlmProviderId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!provider) {
|
||||||
|
throw new Error(`LLM provider ${settings.defaultLlmProviderId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check user-level provider
|
||||||
|
if (userId) {
|
||||||
|
const userProvider = await this.prisma.llmProviderInstance.findFirst({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (userProvider) {
|
||||||
|
return userProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fall back to system default
|
||||||
|
const systemDefault = await this.prisma.llmProviderInstance.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: null,
|
||||||
|
isDefault: true,
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!systemDefault) {
|
||||||
|
throw new Error(`No LLM provider available for workspace ${workspaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get effective personality for a workspace
|
||||||
|
* Priority: workspace default > workspace enabled > any enabled
|
||||||
|
*
|
||||||
|
* @param workspaceId - Workspace ID
|
||||||
|
* @returns Effective personality
|
||||||
|
* @throws {Error} If no personality available
|
||||||
|
*/
|
||||||
|
async getEffectivePersonality(workspaceId: string): Promise<Personality> {
|
||||||
|
// Get workspace settings
|
||||||
|
const settings = await this.prisma.workspaceLlmSettings.findUnique({
|
||||||
|
where: { workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Check workspace-configured personality
|
||||||
|
if (settings?.defaultPersonalityId) {
|
||||||
|
const personality = await this.prisma.personality.findUnique({
|
||||||
|
where: {
|
||||||
|
id: settings.defaultPersonalityId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!personality) {
|
||||||
|
throw new Error(`Personality ${settings.defaultPersonalityId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return personality;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fall back to default personality in workspace
|
||||||
|
const defaultPersonality = await this.prisma.personality.findFirst({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
isDefault: true,
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (defaultPersonality) {
|
||||||
|
return defaultPersonality;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fall back to any enabled personality
|
||||||
|
const anyPersonality = await this.prisma.personality.findFirst({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!anyPersonality) {
|
||||||
|
throw new Error(`No personality available for workspace ${workspaceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyPersonality;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user