matchers.js 4.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. // Levenshtein distance implementation
  2. function levenshteinDistance(a, b) {
  3. if (a.length === 0)
  4. return b.length;
  5. if (b.length === 0)
  6. return a.length;
  7. const matrix = Array(b.length + 1)
  8. .fill(null)
  9. .map(() => Array(a.length + 1).fill(null));
  10. for (let i = 0; i <= a.length; i++)
  11. matrix[0][i] = i;
  12. for (let j = 0; j <= b.length; j++)
  13. matrix[j][0] = j;
  14. for (let j = 1; j <= b.length; j++) {
  15. for (let i = 1; i <= a.length; i++) {
  16. const substitutionCost = a[i - 1] === b[j - 1] ? 0 : 1;
  17. matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + substitutionCost);
  18. }
  19. }
  20. return matrix[b.length][a.length];
  21. }
  22. export async function toBeRelativeCloseTo(received, expected, options = {}) {
  23. const { threshold = 0.1, algorithm = "levenshtein" } = options;
  24. if (threshold < 0 || threshold > 1) {
  25. throw new Error("Relative distance is normalized, and threshold must be between 0 and 1.");
  26. }
  27. let distance;
  28. let maxLength;
  29. switch (algorithm) {
  30. case "levenshtein":
  31. distance = levenshteinDistance(received, expected);
  32. maxLength = Math.max(received.length, expected.length);
  33. break;
  34. default:
  35. throw new Error(`Unsupported algorithm: ${algorithm}`);
  36. }
  37. // Calculate relative distance (normalized between 0 and 1)
  38. const relativeDistance = maxLength === 0 ? 0 : distance / maxLength;
  39. const pass = relativeDistance <= threshold;
  40. return {
  41. pass,
  42. message: () => pass
  43. ? `Expected "${received}" not to be relatively close to "${expected}" (threshold: ${threshold}, actual distance: ${relativeDistance})`
  44. : `Expected "${received}" to be relatively close to "${expected}" (threshold: ${threshold}, actual distance: ${relativeDistance})`,
  45. };
  46. }
  47. export async function toBeAbsoluteCloseTo(received, expected, options = {}) {
  48. const { threshold = 3, algorithm = "levenshtein" } = options;
  49. let distance;
  50. switch (algorithm) {
  51. case "levenshtein":
  52. distance = levenshteinDistance(received, expected);
  53. break;
  54. default:
  55. throw new Error(`Unsupported algorithm: ${algorithm}`);
  56. }
  57. const pass = distance <= threshold;
  58. return {
  59. pass,
  60. message: () => pass
  61. ? `Expected "${received}" not to be absolutely close to "${expected}" (threshold: ${threshold}, actual distance: ${distance})`
  62. : `Expected "${received}" to be absolutely close to "${expected}" (threshold: ${threshold}, actual distance: ${distance})`,
  63. };
  64. }
  65. export async function toBeSemanticCloseTo(received, expected, options) {
  66. const { threshold = 0.2, embeddings, algorithm = "cosine" } = options;
  67. // Get embeddings for both strings
  68. const [receivedEmbedding, expectedEmbedding] = await Promise.all([
  69. embeddings.embedQuery(received),
  70. embeddings.embedQuery(expected),
  71. ]);
  72. // Calculate similarity based on chosen algorithm
  73. let similarity;
  74. switch (algorithm) {
  75. case "cosine": {
  76. // Compute cosine similarity
  77. const dotProduct = receivedEmbedding.reduce((sum, a, i) => sum + a * expectedEmbedding[i], 0);
  78. const receivedMagnitude = Math.sqrt(receivedEmbedding.reduce((sum, a) => sum + a * a, 0));
  79. const expectedMagnitude = Math.sqrt(expectedEmbedding.reduce((sum, a) => sum + a * a, 0));
  80. similarity = dotProduct / (receivedMagnitude * expectedMagnitude);
  81. break;
  82. }
  83. case "dot-product": {
  84. // Compute dot product similarity
  85. similarity = receivedEmbedding.reduce((sum, a, i) => sum + a * expectedEmbedding[i], 0);
  86. break;
  87. }
  88. default:
  89. throw new Error(`Unsupported algorithm: ${algorithm}`);
  90. }
  91. const pass = similarity >= 1 - threshold;
  92. return {
  93. pass,
  94. message: () => pass
  95. ? `Expected "${received}" not to be semantically close to "${expected}" (threshold: ${threshold}, similarity: ${similarity})`
  96. : `Expected "${received}" to be semantically close to "${expected}" (threshold: ${threshold}, similarity: ${similarity})`,
  97. };
  98. }