feat(EMBEDDINGS-0001): enable local embedder by default and overhaul settings page
- Wire local embedding provider as the default on startup when no profile is configured - Refactor embedding settings into dedicated service, DTOs, mappers and models - Rebuild settings page with profile management UI and live test feedback - Expose index summary (indexed versions + embedding count) on repo endpoints - Harden indexing pipeline and context search with additional test coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
131
src/lib/server/services/embedding-settings.service.ts
Normal file
131
src/lib/server/services/embedding-settings.service.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type Database from 'better-sqlite3';
|
||||
import type { EmbeddingSettingsUpdateDto } from '$lib/dtos/embedding-settings.js';
|
||||
import { createProviderFromProfile, getDefaultLocalProfile } from '$lib/server/embeddings/registry.js';
|
||||
import { EmbeddingProfileMapper } from '$lib/server/mappers/embedding-profile.mapper.js';
|
||||
import { EmbeddingProfile, EmbeddingProfileEntity } from '$lib/server/models/embedding-profile.js';
|
||||
import { EmbeddingSettings } from '$lib/server/models/embedding-settings.js';
|
||||
import { InvalidInputError } from '$lib/server/utils/validation.js';
|
||||
|
||||
export class EmbeddingSettingsService {
|
||||
constructor(private readonly db: Database.Database) {}
|
||||
|
||||
getSettings(): EmbeddingSettings {
|
||||
const profiles = this.loadProfiles();
|
||||
const activeProfile = profiles.find((profile) => profile.isDefault && profile.enabled) ?? null;
|
||||
|
||||
return new EmbeddingSettings({ profiles, activeProfile });
|
||||
}
|
||||
|
||||
async updateSettings(input: EmbeddingSettingsUpdateDto): Promise<EmbeddingSettings> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
this.db.prepare('UPDATE embedding_profiles SET is_default = 0, updated_at = ?').run(now);
|
||||
|
||||
if (input.activeProfileId === null) {
|
||||
return this.getSettings();
|
||||
}
|
||||
|
||||
const profile =
|
||||
input.activeProfileId === 'local-default'
|
||||
? this.buildDefaultLocalProfile(now)
|
||||
: this.buildCustomProfile(input, now);
|
||||
|
||||
const available = await createProviderFromProfile(profile).isAvailable();
|
||||
if (!available) {
|
||||
throw new InvalidInputError(
|
||||
`Could not connect to the "${profile.providerKind}" provider. Check your configuration.`
|
||||
);
|
||||
}
|
||||
|
||||
this.persistProfile(profile);
|
||||
return this.getSettings();
|
||||
}
|
||||
|
||||
private loadProfiles(): EmbeddingProfile[] {
|
||||
return this.db
|
||||
.prepare('SELECT * FROM embedding_profiles ORDER BY is_default DESC, created_at ASC')
|
||||
.all()
|
||||
.map((row) => EmbeddingProfileMapper.fromEntity(new EmbeddingProfileEntity(row as never)));
|
||||
}
|
||||
|
||||
private buildDefaultLocalProfile(now: number): EmbeddingProfile {
|
||||
const defaultLocal = getDefaultLocalProfile();
|
||||
|
||||
return new EmbeddingProfile({
|
||||
id: defaultLocal.id,
|
||||
providerKind: defaultLocal.providerKind,
|
||||
title: defaultLocal.title,
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
model: defaultLocal.model,
|
||||
dimensions: defaultLocal.dimensions,
|
||||
config: {},
|
||||
createdAt: this.getCreatedAt(defaultLocal.id, now),
|
||||
updatedAt: now
|
||||
});
|
||||
}
|
||||
|
||||
private buildCustomProfile(input: EmbeddingSettingsUpdateDto, now: number): EmbeddingProfile {
|
||||
const candidate = input.profile;
|
||||
if (!candidate) {
|
||||
throw new InvalidInputError('profile is required for custom embedding providers');
|
||||
}
|
||||
if (candidate.id !== input.activeProfileId) {
|
||||
throw new InvalidInputError('activeProfileId must match profile.id');
|
||||
}
|
||||
if (!candidate.title || !candidate.model) {
|
||||
throw new InvalidInputError('profile title and model are required');
|
||||
}
|
||||
|
||||
return new EmbeddingProfile({
|
||||
id: candidate.id,
|
||||
providerKind: candidate.providerKind,
|
||||
title: candidate.title,
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
model: candidate.model,
|
||||
dimensions: candidate.dimensions,
|
||||
config: candidate.config,
|
||||
createdAt: this.getCreatedAt(candidate.id, now),
|
||||
updatedAt: now
|
||||
});
|
||||
}
|
||||
|
||||
private getCreatedAt(id: string, fallback: number): number {
|
||||
return (
|
||||
this.db
|
||||
.prepare<[string], { created_at: number }>('SELECT created_at FROM embedding_profiles WHERE id = ?')
|
||||
.get(id)?.created_at ?? fallback
|
||||
);
|
||||
}
|
||||
|
||||
private persistProfile(profile: EmbeddingProfile): void {
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO embedding_profiles
|
||||
(id, provider_kind, title, enabled, is_default, model, dimensions, config, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
provider_kind = excluded.provider_kind,
|
||||
title = excluded.title,
|
||||
enabled = excluded.enabled,
|
||||
is_default = excluded.is_default,
|
||||
model = excluded.model,
|
||||
dimensions = excluded.dimensions,
|
||||
config = excluded.config,
|
||||
updated_at = excluded.updated_at`
|
||||
)
|
||||
.run(
|
||||
profile.id,
|
||||
profile.providerKind,
|
||||
profile.title,
|
||||
profile.enabled ? 1 : 0,
|
||||
profile.isDefault ? 1 : 0,
|
||||
profile.model,
|
||||
profile.dimensions,
|
||||
JSON.stringify(profile.config),
|
||||
profile.createdAt,
|
||||
profile.updatedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user