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.

116 lines
3.5 KiB

11 months ago
  1. const { parseLMStudioBasePath } = require("../../AiProviders/lmStudio");
  2. const { maximumChunkLength } = require("../../helpers");
  3. class LMStudioEmbedder {
  4. constructor() {
  5. if (!process.env.EMBEDDING_BASE_PATH)
  6. throw new Error("No embedding base path was set.");
  7. if (!process.env.EMBEDDING_MODEL_PREF)
  8. throw new Error("No embedding model was set.");
  9. const { OpenAI: OpenAIApi } = require("openai");
  10. this.lmstudio = new OpenAIApi({
  11. baseURL: parseLMStudioBasePath(process.env.EMBEDDING_BASE_PATH),
  12. apiKey: null,
  13. });
  14. this.model = process.env.EMBEDDING_MODEL_PREF;
  15. // Limit of how many strings we can process in a single pass to stay with resource or network limits
  16. this.maxConcurrentChunks = 1;
  17. this.embeddingMaxChunkLength = maximumChunkLength();
  18. }
  19. log(text, ...args) {
  20. console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args);
  21. }
  22. async #isAlive() {
  23. return await this.lmstudio.models
  24. .list()
  25. .then((res) => res?.data?.length > 0)
  26. .catch((e) => {
  27. this.log(e.message);
  28. return false;
  29. });
  30. }
  31. async embedTextInput(textInput) {
  32. const result = await this.embedChunks(
  33. Array.isArray(textInput) ? textInput : [textInput]
  34. );
  35. return result?.[0] || [];
  36. }
  37. async embedChunks(textChunks = []) {
  38. if (!(await this.#isAlive()))
  39. throw new Error(
  40. `LMStudio service could not be reached. Is LMStudio running?`
  41. );
  42. this.log(
  43. `Embedding ${textChunks.length} chunks of text with ${this.model}.`
  44. );
  45. // LMStudio will drop all queued requests now? So if there are many going on
  46. // we need to do them sequentially or else only the first resolves and the others
  47. // get dropped or go unanswered >:(
  48. let results = [];
  49. let hasError = false;
  50. for (const chunk of textChunks) {
  51. if (hasError) break; // If an error occurred don't continue and exit early.
  52. results.push(
  53. await this.lmstudio.embeddings
  54. .create({
  55. model: this.model,
  56. input: chunk,
  57. })
  58. .then((result) => {
  59. const embedding = result.data?.[0]?.embedding;
  60. if (!Array.isArray(embedding) || !embedding.length)
  61. throw {
  62. type: "EMPTY_ARR",
  63. message: "The embedding was empty from LMStudio",
  64. };
  65. console.log(`Embedding length: ${embedding.length}`);
  66. return { data: embedding, error: null };
  67. })
  68. .catch((e) => {
  69. e.type =
  70. e?.response?.data?.error?.code ||
  71. e?.response?.status ||
  72. "failed_to_embed";
  73. e.message = e?.response?.data?.error?.message || e.message;
  74. hasError = true;
  75. return { data: [], error: e };
  76. })
  77. );
  78. }
  79. // Accumulate errors from embedding.
  80. // If any are present throw an abort error.
  81. const errors = results
  82. .filter((res) => !!res.error)
  83. .map((res) => res.error)
  84. .flat();
  85. if (errors.length > 0) {
  86. let uniqueErrors = new Set();
  87. console.log(errors);
  88. errors.map((error) =>
  89. uniqueErrors.add(`[${error.type}]: ${error.message}`)
  90. );
  91. if (errors.length > 0)
  92. throw new Error(
  93. `LMStudio Failed to embed: ${Array.from(uniqueErrors).join(", ")}`
  94. );
  95. }
  96. const data = results.map((res) => res?.data || []);
  97. return data.length > 0 ? data : null;
  98. }
  99. }
  100. module.exports = {
  101. LMStudioEmbedder,
  102. };