packages/chat-data/src/search/experienceSemantic.ts

import type { ResumeEntry } from '@portfolio/chat-contract'; import type { EmbeddingProvider, ExperienceRepository } from '../providers/types'; import { cosineSimilarity } from './semantic';

export type ExperienceSemanticRanker = { scoreExperiences(records: ResumeEntry[], query: string): Promise<Map<string, number>>; };

type ExperienceEmbeddingSemanticRankerOptions = { embeddingProvider: EmbeddingProvider; experienceRepository: Pick<ExperienceRepository, 'getEmbedding'>; scoreScale?: number; };

function normalizeQuery(query: string): string { return typeof query === 'string' ? query.trim() : ''; }

export function createExperienceEmbeddingSemanticRanker( options: ExperienceEmbeddingSemanticRankerOptions ): ExperienceSemanticRanker { const { embeddingProvider, experienceRepository, scoreScale = 8 } = options;

return { async scoreExperiences(records, query) { if (!records.length) { return new Map(); }

  const normalized = normalizeQuery(query);
  if (!normalized) {
    return new Map();
  }

  const embeddings = await embeddingProvider.embedTexts([normalized]);
  const queryEmbedding = embeddings[0];
  if (!queryEmbedding?.length) {
    return new Map();
  }

  const scores = new Map<string, number>();
  for (const record of records) {
    const recordEmbedding = experienceRepository.getEmbedding
      ? await experienceRepository.getEmbedding(record)
      : undefined;
    if (!recordEmbedding?.length) {
      continue;
    }
    const similarity = cosineSimilarity(queryEmbedding, recordEmbedding);
    if (similarity > 0) {
      scores.set(record.id, similarity * scoreScale);
    }
  }

  return scores;
},

}; }