You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

100 lines
3.5 KiB

11 months ago
  1. const { toChunks } = require("../../helpers");
  2. class AzureOpenAiEmbedder {
  3. constructor() {
  4. const { OpenAIClient, AzureKeyCredential } = require("@azure/openai");
  5. if (!process.env.AZURE_OPENAI_ENDPOINT)
  6. throw new Error("No Azure API endpoint was set.");
  7. if (!process.env.AZURE_OPENAI_KEY)
  8. throw new Error("No Azure API key was set.");
  9. const openai = new OpenAIClient(
  10. process.env.AZURE_OPENAI_ENDPOINT,
  11. new AzureKeyCredential(process.env.AZURE_OPENAI_KEY)
  12. );
  13. this.openai = openai;
  14. // Limit of how many strings we can process in a single pass to stay with resource or network limits
  15. // https://learn.microsoft.com/en-us/azure/ai-services/openai/faq#i-am-trying-to-use-embeddings-and-received-the-error--invalidrequesterror--too-many-inputs--the-max-number-of-inputs-is-1---how-do-i-fix-this-:~:text=consisting%20of%20up%20to%2016%20inputs%20per%20API%20request
  16. this.maxConcurrentChunks = 16;
  17. // https://learn.microsoft.com/en-us/answers/questions/1188074/text-embedding-ada-002-token-context-length
  18. this.embeddingMaxChunkLength = 2048;
  19. }
  20. async embedTextInput(textInput) {
  21. const result = await this.embedChunks(
  22. Array.isArray(textInput) ? textInput : [textInput]
  23. );
  24. return result?.[0] || [];
  25. }
  26. async embedChunks(textChunks = []) {
  27. const textEmbeddingModel =
  28. process.env.EMBEDDING_MODEL_PREF || "text-embedding-ada-002";
  29. if (!textEmbeddingModel)
  30. throw new Error(
  31. "No EMBEDDING_MODEL_PREF ENV defined. This must the name of a deployment on your Azure account for an embedding model."
  32. );
  33. // Because there is a limit on how many chunks can be sent at once to Azure OpenAI
  34. // we concurrently execute each max batch of text chunks possible.
  35. // Refer to constructor maxConcurrentChunks for more info.
  36. const embeddingRequests = [];
  37. for (const chunk of toChunks(textChunks, this.maxConcurrentChunks)) {
  38. embeddingRequests.push(
  39. new Promise((resolve) => {
  40. this.openai
  41. .getEmbeddings(textEmbeddingModel, chunk)
  42. .then((res) => {
  43. resolve({ data: res.data, error: null });
  44. })
  45. .catch((e) => {
  46. e.type =
  47. e?.response?.data?.error?.code ||
  48. e?.response?.status ||
  49. "failed_to_embed";
  50. e.message = e?.response?.data?.error?.message || e.message;
  51. resolve({ data: [], error: e });
  52. });
  53. })
  54. );
  55. }
  56. const { data = [], error = null } = await Promise.all(
  57. embeddingRequests
  58. ).then((results) => {
  59. // If any errors were returned from Azure abort the entire sequence because the embeddings
  60. // will be incomplete.
  61. const errors = results
  62. .filter((res) => !!res.error)
  63. .map((res) => res.error)
  64. .flat();
  65. if (errors.length > 0) {
  66. let uniqueErrors = new Set();
  67. errors.map((error) =>
  68. uniqueErrors.add(`[${error.type}]: ${error.message}`)
  69. );
  70. return {
  71. data: [],
  72. error: Array.from(uniqueErrors).join(", "),
  73. };
  74. }
  75. return {
  76. data: results.map((res) => res?.data || []).flat(),
  77. error: null,
  78. };
  79. });
  80. if (!!error) throw new Error(`Azure OpenAI Failed to embed: ${error}`);
  81. return data.length > 0 &&
  82. data.every((embd) => embd.hasOwnProperty("embedding"))
  83. ? data.map((embd) => embd.embedding)
  84. : null;
  85. }
  86. }
  87. module.exports = {
  88. AzureOpenAiEmbedder,
  89. };