@sweetoburrito/backstage-plugin-ai-assistant-backend 0.15.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,22 +59,25 @@ class PgVectorStore {
59
59
  if (!this.embeddings) {
60
60
  throw new Error("No Embeddings configured for the vector store.");
61
61
  }
62
- const conditions = documents.map(() => `(metadata->>'id' = ? AND metadata->>'source' = ?)`).join(" OR ");
62
+ const conditions = documents.map(
63
+ () => `(metadata->>'id' = ? AND metadata->>'source' = ? AND metadata->>'chunk' = ?)`
64
+ ).join(" OR ");
63
65
  const params = documents.flatMap((doc) => [
64
66
  doc.metadata.id,
65
- doc.metadata.source
67
+ doc.metadata.source,
68
+ doc.metadata.chunk
66
69
  ]);
67
70
  const existingDocuments = await this.client.select("*").from(this.tableName).whereRaw(conditions, params);
68
71
  const existingMap = new Map(
69
72
  existingDocuments.map((doc) => [
70
- `${doc.metadata.id}:${doc.metadata.source}`,
73
+ `${doc.metadata.id}:${doc.metadata.source}:${doc.metadata.chunk}`,
71
74
  doc
72
75
  ])
73
76
  );
74
77
  const newDocuments = [];
75
78
  const documentsToUpdate = [];
76
79
  for (const doc of documents) {
77
- const key = `${doc.metadata.id}:${doc.metadata.source}`;
80
+ const key = `${doc.metadata.id}:${doc.metadata.source}:${doc.metadata.chunk}`;
78
81
  const existing = existingMap.get(key);
79
82
  if (!existing) {
80
83
  newDocuments.push(doc);
@@ -86,45 +89,64 @@ class PgVectorStore {
86
89
  documentsToUpdate.push({ ...doc, id: existing.id });
87
90
  }
88
91
  }
89
- const allDocumentsToAdd = [...newDocuments, ...documentsToUpdate];
90
- if (allDocumentsToAdd.length === 0) {
91
- this.logger.debug("No new or updated documents to add.");
92
+ const unchangedCount = documents.length - newDocuments.length - documentsToUpdate.length;
93
+ if (newDocuments.length === 0 && documentsToUpdate.length === 0) {
94
+ this.logger.debug(
95
+ `All ${unchangedCount} documents are unchanged, skipping processing.`
96
+ );
92
97
  return;
93
98
  }
99
+ this.logger.info(
100
+ `Processing ${documents.length} documents (${newDocuments.length} new, ${documentsToUpdate.length} updated, ${unchangedCount} unchanged).`
101
+ );
94
102
  if (documentsToUpdate.length > 0) {
95
- const uniqueDocKeys = new Set(
96
- documentsToUpdate.map(
97
- (doc) => `${doc.metadata.id}:${doc.metadata.source}`
98
- )
103
+ const contents = documentsToUpdate.map(
104
+ (doc) => doc.content.replace(/\0/g, "")
99
105
  );
100
- for (const key of uniqueDocKeys) {
101
- const [id, source] = key.split(":");
102
- await this.client(this.tableName).delete().whereRaw(`metadata->>'id' = ? AND metadata->>'source' = ?`, [
103
- id,
104
- source
105
- ]);
106
+ const vectors = await this.embeddings.embedDocuments(contents);
107
+ const updatedDocuments = documentsToUpdate.map(
108
+ (doc, index) => {
109
+ const cleanedContent = contents[index];
110
+ const vector = vectors[index];
111
+ const hash = crypto.createHash("sha256").update(cleanedContent).digest("hex");
112
+ return {
113
+ ...doc,
114
+ id: doc.id,
115
+ hash,
116
+ content: cleanedContent,
117
+ vector: `[${vector.join(",")}]`,
118
+ lastUpdated: doc.lastUpdated ?? /* @__PURE__ */ new Date()
119
+ };
120
+ }
121
+ );
122
+ for (const doc of updatedDocuments) {
123
+ const { id, ...updateData } = doc;
124
+ await this.client(this.tableName).where("id", id).update(updateData);
106
125
  }
107
126
  this.logger.info(
108
- `Deleted all chunks for ${uniqueDocKeys.size} updated documents`
127
+ `Updated ${documentsToUpdate.length} existing documents in the vector store.`
128
+ );
129
+ }
130
+ if (newDocuments.length > 0) {
131
+ const contents = newDocuments.map((doc) => doc.content.replace(/\0/g, ""));
132
+ const vectors = await this.embeddings.embedDocuments(contents);
133
+ const rows = newDocuments.map((doc, index) => {
134
+ const vector = vectors[index];
135
+ const cleanedContent = contents[index];
136
+ const hash = crypto.createHash("sha256").update(cleanedContent).digest("hex");
137
+ return {
138
+ hash,
139
+ metadata: doc.metadata,
140
+ lastUpdated: doc.lastUpdated ?? /* @__PURE__ */ new Date(),
141
+ content: cleanedContent,
142
+ vector: `[${vector.join(",")}]`
143
+ };
144
+ });
145
+ await this.client.batchInsert(this.tableName, rows, this.chunkSize);
146
+ this.logger.info(
147
+ `Added ${newDocuments.length} new documents in the vector store.`
109
148
  );
110
149
  }
111
- const contents = allDocumentsToAdd.map((doc) => doc.content);
112
- const vectors = await this.embeddings.embedDocuments(contents);
113
- const rows = allDocumentsToAdd.map((doc, index) => {
114
- const vector = vectors[index];
115
- const hash = crypto.createHash("sha256").update(doc.content).digest("hex");
116
- return {
117
- hash,
118
- metadata: doc.metadata,
119
- lastUpdated: doc.lastUpdated ?? /* @__PURE__ */ new Date(),
120
- content: doc.content.replace(/\0/g, ""),
121
- vector: `[${vector.join(",")}]`
122
- };
123
- });
124
- this.logger.info(
125
- `Adding ${rows.length} documents (${newDocuments.length} new, ${documentsToUpdate.length} updated).`
126
- );
127
- await this.client.batchInsert(this.tableName, rows, this.chunkSize);
128
150
  }
129
151
  /**
130
152
  * Deletes records from the database table by their ids.
@@ -1 +1 @@
1
- {"version":3,"file":"pg-vector-store.cjs.js","sources":["../../src/database/pg-vector-store.ts"],"sourcesContent":["import {\n DatabaseService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport {\n VectorStore,\n EmbeddingDocument,\n EmbeddingDocumentMetadata,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { Embeddings } from '@langchain/core/embeddings';\nimport { Knex } from 'knex';\nimport { createHash } from 'crypto';\n\nexport type PgVectorStoreOptions = {\n database: DatabaseService;\n logger: LoggerService;\n config: RootConfigService;\n};\n\nexport class PgVectorStore implements VectorStore {\n private readonly tableName: string = 'embeddings';\n private embeddings?: Omit<Embeddings, 'caller'>;\n\n // Recency bias configuration\n private readonly RECENCY_WEIGHT = 0.3; // Weight for document recency (0-1)\n private readonly SIMILARITY_WEIGHT = 1 - this.RECENCY_WEIGHT; // Weight for vector similarity (0-1)\n private readonly RECENCY_HALF_LIFE_DAYS = 180; // Days until recency boost is halved (6 months)\n private readonly AGE_SCALE_FACTOR = 86400; // Seconds in a day for timestamp conversion\n\n /**\n * Creates an instance of PgVectorStore.\n * @param client - The Knex client to interact with the PostgreSQL database.\n * @param [amount=4] - The number of embeddings to store.\n * @param [chunkSize=500] - The size of each chunk of embeddings.\n */\n constructor(\n private readonly client: Knex,\n private readonly logger: LoggerService,\n private readonly amount: number = 4,\n private readonly chunkSize: number = 500,\n ) {}\n\n static async fromConfig({ config, database, logger }: PgVectorStoreOptions) {\n const client = await database.getClient();\n const chunkSize = config.getOptionalNumber(\n 'aiAssistant.storage.pgVector.chunkSize',\n );\n const amount = config.getOptionalNumber(\n 'aiAssistant.storage.pgVector.amount',\n );\n\n return new PgVectorStore(client, logger, amount, chunkSize);\n }\n\n connectEmbeddings(embeddings: Omit<Embeddings, 'caller'>) {\n if (this.embeddings) {\n this.logger.warn('Embeddings already connected, overwriting.');\n }\n this.embeddings = embeddings;\n }\n\n table() {\n return this.client(this.tableName);\n }\n\n /**\n * Add documents to the vector store.\n *\n * @param {EmbeddingDocument[]} documents - The array of documents to be added.\n * @throws {Error} When no embeddings are configured for the vector store.\n * @returns {Promise<void>} Resolves when the documents have been added successfully.\n */\n async addDocuments(documents: EmbeddingDocument[]): Promise<void> {\n if (documents.length === 0) {\n return;\n }\n\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n\n // Fetch existing documents with matching (id, source) pairs\n const conditions = documents\n .map(() => `(metadata->>'id' = ? AND metadata->>'source' = ?)`)\n .join(' OR ');\n\n const params = documents.flatMap(doc => [\n doc.metadata.id,\n doc.metadata.source,\n ]);\n\n const existingDocuments: EmbeddingDocument[] = await this.client\n .select('*')\n .from(this.tableName)\n .whereRaw(conditions, params);\n\n // Build a map for quick lookups\n const existingMap = new Map(\n existingDocuments.map(doc => [\n `${doc.metadata.id}:${doc.metadata.source}`,\n doc,\n ]),\n );\n\n // Categorize documents\n const newDocuments: EmbeddingDocument[] = [];\n const documentsToUpdate: Array<EmbeddingDocument & { id: string }> = [];\n\n for (const doc of documents) {\n const key = `${doc.metadata.id}:${doc.metadata.source}`;\n const existing = existingMap.get(key);\n\n if (!existing) {\n newDocuments.push(doc);\n continue;\n }\n\n const cleanedContent = doc.content.replace(/\\0/g, '');\n const newHash = createHash('sha256').update(cleanedContent).digest('hex');\n if (!existing.hash || newHash !== existing.hash) {\n documentsToUpdate.push({ ...doc, id: existing.id! });\n }\n }\n\n const allDocumentsToAdd = [...newDocuments, ...documentsToUpdate];\n\n if (allDocumentsToAdd.length === 0) {\n this.logger.debug('No new or updated documents to add.');\n return;\n }\n\n // Delete old versions before re-adding\n if (documentsToUpdate.length > 0) {\n const uniqueDocKeys = new Set(\n documentsToUpdate.map(\n doc => `${doc.metadata.id}:${doc.metadata.source}`,\n ),\n );\n\n for (const key of uniqueDocKeys) {\n const [id, source] = key.split(':');\n await this.client(this.tableName)\n .delete()\n .whereRaw(`metadata->>'id' = ? AND metadata->>'source' = ?`, [\n id,\n source,\n ]);\n }\n\n this.logger.info(\n `Deleted all chunks for ${uniqueDocKeys.size} updated documents`,\n );\n }\n\n const contents = allDocumentsToAdd.map(doc => doc.content);\n const vectors = await this.embeddings!.embedDocuments(contents);\n\n const rows = allDocumentsToAdd.map((doc, index) => {\n const vector = vectors[index];\n const hash = createHash('sha256').update(doc.content).digest('hex');\n\n return {\n hash,\n metadata: doc.metadata,\n lastUpdated: doc.lastUpdated ?? new Date(),\n content: doc.content.replace(/\\0/g, ''),\n vector: `[${vector.join(',')}]`,\n };\n });\n this.logger.info(\n `Adding ${rows.length} documents (${newDocuments.length} new, ${documentsToUpdate.length} updated).`,\n );\n\n await this.client.batchInsert(this.tableName, rows, this.chunkSize);\n }\n\n /**\n * Deletes records from the database table by their ids.\n *\n * @param {string[]} ids - The array of ids of the records to be deleted.\n * @returns {Promise<void>} - A promise that resolves when the deletion is complete.\n */\n private async deleteById(ids: string[]) {\n await this.table().delete().whereIn('id', ids);\n }\n\n /**\n * Deletes rows from the table based on the specified filter.\n *\n * @param {EmbeddingDocMetadata} filter - The filter to apply for deletion.\n * @returns {Promise} - A Promise that resolves when the deletion is complete.\n */\n private async deleteByFilter(filter: EmbeddingDocumentMetadata) {\n const queryString = `\n DELETE FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n `;\n return this.client.raw(queryString, { filter });\n }\n\n /**\n * Deletes documents based on the provided deletion parameters.\n * Either `ids` or `filter` must be specified.\n *\n * @param {Object} deletionParams - The deletion parameters.\n * @param {Array<string>} [deletionParams.ids] - The document IDs to delete.\n * @param {EmbeddingDocMetadata} [deletionParams.filter] - The filter to match documents to be deleted.\n *\n * @return {Promise<void>} - A Promise that resolves once the documents have been deleted.\n */\n async deleteDocuments(deletionParams: {\n ids?: string[];\n filter?: EmbeddingDocumentMetadata;\n }): Promise<void> {\n const { ids, filter } = deletionParams;\n\n if (!(ids || filter)) {\n throw new Error(\n 'You must specify either ids or a filter when deleting documents.',\n );\n }\n\n if (ids && filter) {\n throw new Error(\n 'You cannot specify both ids and a filter when deleting documents.',\n );\n }\n\n if (ids) {\n await this.deleteById(ids);\n } else if (filter) {\n await this.deleteByFilter(filter);\n }\n }\n\n /**\n * Finds the most similar documents to a given query vector, along with their similarity scores.\n * Results are ranked by a weighted combination of vector similarity and document recency.\n * i.e newer documents are favored in the ranking but if no new documents exist, older but more similar documents will still be returned.\n *\n * @param {number[]} query - The query vector to compare against.\n * @param {number} amount - The maximum number of results to return.\n * @param {EmbeddingDocumentMetadata} [filter] - Optional filter to limit the search results.\n * @returns {Promise<[EmbeddingDocument, number][]>} - An array of document similarity results, where each\n * result is a tuple containing the document and its similarity score.\n */\n private async similaritySearchVectorWithScore(\n query: number[],\n amount: number,\n filter?: EmbeddingDocumentMetadata,\n ): Promise<[EmbeddingDocument, number][]> {\n const embeddingString = `[${query.join(',')}]`;\n\n const queryString = `\n SELECT\n *,\n (vector <=> :embeddingString) as \"_distance\",\n (EXTRACT(EPOCH FROM (NOW() - COALESCE(\"lastUpdated\", NOW()))) / :ageScaleFactor) as \"_age_days\",\n (\n ((vector <=> :embeddingString) * :similarityWeight) -\n (EXP(-0.693 * (EXTRACT(EPOCH FROM (NOW() - COALESCE(\"lastUpdated\", NOW()))) / :ageScaleFactor) / :recencyHalfLife) * :recencyWeight)\n ) as \"_combined_score\"\n FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n ORDER BY \"_combined_score\" ASC\n LIMIT :amount\n `;\n\n const documents = (\n await this.client.raw(queryString, {\n embeddingString,\n filter: JSON.stringify(filter ?? {}),\n amount,\n similarityWeight: this.SIMILARITY_WEIGHT,\n recencyWeight: this.RECENCY_WEIGHT,\n recencyHalfLife: this.RECENCY_HALF_LIFE_DAYS,\n ageScaleFactor: this.AGE_SCALE_FACTOR,\n })\n ).rows;\n\n const results = [] as [EmbeddingDocument, number][];\n for (const doc of documents) {\n // eslint-ignore-next-line\n if (doc._distance !== null && doc.content !== null) {\n const document: EmbeddingDocument = {\n content: doc.content,\n metadata: {\n ...doc.metadata,\n ageInDays: Math.round(doc._age_days),\n lastUpdated: doc.lastUpdated,\n },\n };\n results.push([document, doc._distance]);\n }\n }\n return results;\n }\n\n /**\n * Performs a similarity search using the given query and filter.\n *\n * @param {string} query - The query to perform the similarity search on.\n * @param {EmbeddingDocMetadata} filter - The filter to apply to the search results.\n * @param {number} [amount=4] - The number of results to return.\n * @return {Promise<EmbeddingDoc[]>} - A promise that resolves to an array of RoadieEmbeddingDoc objects representing the search results.\n * @throws {Error} - Throws an error if there are no embeddings configured for the vector store.\n */\n async similaritySearch(\n query: string,\n filter?: EmbeddingDocumentMetadata,\n amount: number = this.amount,\n ): Promise<EmbeddingDocument[]> {\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n const results = await this.similaritySearchVectorWithScore(\n await this.embeddings.embedQuery(query),\n amount,\n filter,\n );\n\n return results.map(result => result[0]);\n }\n}\n"],"names":["createHash"],"mappings":";;;;AAoBO,MAAM,aAAA,CAAqC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAgBhD,YACmB,MAAA,EACA,MAAA,EACA,MAAA,GAAiB,CAAA,EACjB,YAAoB,GAAA,EACrC;AAJiB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,SAAA,GAAA,SAAA;AAAA,EAChB;AAAA,EApBc,SAAA,GAAoB,YAAA;AAAA,EAC7B,UAAA;AAAA;AAAA,EAGS,cAAA,GAAiB,GAAA;AAAA;AAAA,EACjB,iBAAA,GAAoB,IAAI,IAAA,CAAK,cAAA;AAAA;AAAA,EAC7B,sBAAA,GAAyB,GAAA;AAAA;AAAA,EACzB,gBAAA,GAAmB,KAAA;AAAA,EAepC,aAAa,UAAA,CAAW,EAAE,MAAA,EAAQ,QAAA,EAAU,QAAO,EAAyB;AAC1E,IAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,SAAA,EAAU;AACxC,IAAA,MAAM,YAAY,MAAA,CAAO,iBAAA;AAAA,MACvB;AAAA,KACF;AACA,IAAA,MAAM,SAAS,MAAA,CAAO,iBAAA;AAAA,MACpB;AAAA,KACF;AAEA,IAAA,OAAO,IAAI,aAAA,CAAc,MAAA,EAAQ,MAAA,EAAQ,QAAQ,SAAS,CAAA;AAAA,EAC5D;AAAA,EAEA,kBAAkB,UAAA,EAAwC;AACxD,IAAA,IAAI,KAAK,UAAA,EAAY;AACnB,MAAA,IAAA,CAAK,MAAA,CAAO,KAAK,4CAA4C,CAAA;AAAA,IAC/D;AACA,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAAA,EACpB;AAAA,EAEA,KAAA,GAAQ;AACN,IAAA,OAAO,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,SAAS,CAAA;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,SAAA,EAA+C;AAChE,IAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACpB,MAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,IAClE;AAGA,IAAA,MAAM,aAAa,SAAA,CAChB,GAAA,CAAI,MAAM,CAAA,iDAAA,CAAmD,CAAA,CAC7D,KAAK,MAAM,CAAA;AAEd,IAAA,MAAM,MAAA,GAAS,SAAA,CAAU,OAAA,CAAQ,CAAA,GAAA,KAAO;AAAA,MACtC,IAAI,QAAA,CAAS,EAAA;AAAA,MACb,IAAI,QAAA,CAAS;AAAA,KACd,CAAA;AAED,IAAA,MAAM,iBAAA,GAAyC,MAAM,IAAA,CAAK,MAAA,CACvD,MAAA,CAAO,GAAG,CAAA,CACV,IAAA,CAAK,IAAA,CAAK,SAAS,CAAA,CACnB,QAAA,CAAS,YAAY,MAAM,CAAA;AAG9B,IAAA,MAAM,cAAc,IAAI,GAAA;AAAA,MACtB,iBAAA,CAAkB,IAAI,CAAA,GAAA,KAAO;AAAA,QAC3B,GAAG,GAAA,CAAI,QAAA,CAAS,EAAE,CAAA,CAAA,EAAI,GAAA,CAAI,SAAS,MAAM,CAAA,CAAA;AAAA,QACzC;AAAA,OACD;AAAA,KACH;AAGA,IAAA,MAAM,eAAoC,EAAC;AAC3C,IAAA,MAAM,oBAA+D,EAAC;AAEtE,IAAA,KAAA,MAAW,OAAO,SAAA,EAAW;AAC3B,MAAA,MAAM,GAAA,GAAM,GAAG,GAAA,CAAI,QAAA,CAAS,EAAE,CAAA,CAAA,EAAI,GAAA,CAAI,SAAS,MAAM,CAAA,CAAA;AACrD,MAAA,MAAM,QAAA,GAAW,WAAA,CAAY,GAAA,CAAI,GAAG,CAAA;AAEpC,MAAA,IAAI,CAAC,QAAA,EAAU;AACb,QAAA,YAAA,CAAa,KAAK,GAAG,CAAA;AACrB,QAAA;AAAA,MACF;AAEA,MAAA,MAAM,cAAA,GAAiB,GAAA,CAAI,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AACpD,MAAA,MAAM,OAAA,GAAUA,kBAAW,QAAQ,CAAA,CAAE,OAAO,cAAc,CAAA,CAAE,OAAO,KAAK,CAAA;AACxE,MAAA,IAAI,CAAC,QAAA,CAAS,IAAA,IAAQ,OAAA,KAAY,SAAS,IAAA,EAAM;AAC/C,QAAA,iBAAA,CAAkB,KAAK,EAAE,GAAG,KAAK,EAAA,EAAI,QAAA,CAAS,IAAK,CAAA;AAAA,MACrD;AAAA,IACF;AAEA,IAAA,MAAM,iBAAA,GAAoB,CAAC,GAAG,YAAA,EAAc,GAAG,iBAAiB,CAAA;AAEhE,IAAA,IAAI,iBAAA,CAAkB,WAAW,CAAA,EAAG;AAClC,MAAA,IAAA,CAAK,MAAA,CAAO,MAAM,qCAAqC,CAAA;AACvD,MAAA;AAAA,IACF;AAGA,IAAA,IAAI,iBAAA,CAAkB,SAAS,CAAA,EAAG;AAChC,MAAA,MAAM,gBAAgB,IAAI,GAAA;AAAA,QACxB,iBAAA,CAAkB,GAAA;AAAA,UAChB,CAAA,GAAA,KAAO,GAAG,GAAA,CAAI,QAAA,CAAS,EAAE,CAAA,CAAA,EAAI,GAAA,CAAI,SAAS,MAAM,CAAA;AAAA;AAClD,OACF;AAEA,MAAA,KAAA,MAAW,OAAO,aAAA,EAAe;AAC/B,QAAA,MAAM,CAAC,EAAA,EAAI,MAAM,CAAA,GAAI,GAAA,CAAI,MAAM,GAAG,CAAA;AAClC,QAAA,MAAM,IAAA,CAAK,OAAO,IAAA,CAAK,SAAS,EAC7B,MAAA,EAAO,CACP,SAAS,CAAA,+CAAA,CAAA,EAAmD;AAAA,UAC3D,EAAA;AAAA,UACA;AAAA,SACD,CAAA;AAAA,MACL;AAEA,MAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,QACV,CAAA,uBAAA,EAA0B,cAAc,IAAI,CAAA,kBAAA;AAAA,OAC9C;AAAA,IACF;AAEA,IAAA,MAAM,QAAA,GAAW,iBAAA,CAAkB,GAAA,CAAI,CAAA,GAAA,KAAO,IAAI,OAAO,CAAA;AACzD,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,UAAA,CAAY,eAAe,QAAQ,CAAA;AAE9D,IAAA,MAAM,IAAA,GAAO,iBAAA,CAAkB,GAAA,CAAI,CAAC,KAAK,KAAA,KAAU;AACjD,MAAA,MAAM,MAAA,GAAS,QAAQ,KAAK,CAAA;AAC5B,MAAA,MAAM,IAAA,GAAOA,kBAAW,QAAQ,CAAA,CAAE,OAAO,GAAA,CAAI,OAAO,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA;AAElE,MAAA,OAAO;AAAA,QACL,IAAA;AAAA,QACA,UAAU,GAAA,CAAI,QAAA;AAAA,QACd,WAAA,EAAa,GAAA,CAAI,WAAA,oBAAe,IAAI,IAAA,EAAK;AAAA,QACzC,OAAA,EAAS,GAAA,CAAI,OAAA,CAAQ,OAAA,CAAQ,OAAO,EAAE,CAAA;AAAA,QACtC,MAAA,EAAQ,CAAA,CAAA,EAAI,MAAA,CAAO,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA;AAAA,OAC9B;AAAA,IACF,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA;AAAA,MACV,CAAA,OAAA,EAAU,KAAK,MAAM,CAAA,YAAA,EAAe,aAAa,MAAM,CAAA,MAAA,EAAS,kBAAkB,MAAM,CAAA,UAAA;AAAA,KAC1F;AAEA,IAAA,MAAM,KAAK,MAAA,CAAO,WAAA,CAAY,KAAK,SAAA,EAAW,IAAA,EAAM,KAAK,SAAS,CAAA;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,WAAW,GAAA,EAAe;AACtC,IAAA,MAAM,KAAK,KAAA,EAAM,CAAE,QAAO,CAAE,OAAA,CAAQ,MAAM,GAAG,CAAA;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAc,eAAe,MAAA,EAAmC;AAC9D,IAAA,MAAM,WAAA,GAAc;AAAA,kBAAA,EACJ,KAAK,SAAS;AAAA;AAAA,IAAA,CAAA;AAG9B,IAAA,OAAO,KAAK,MAAA,CAAO,GAAA,CAAI,WAAA,EAAa,EAAE,QAAQ,CAAA;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,MAAM,gBAAgB,cAAA,EAGJ;AAChB,IAAA,MAAM,EAAE,GAAA,EAAK,MAAA,EAAO,GAAI,cAAA;AAExB,IAAA,IAAI,EAAE,OAAO,MAAA,CAAA,EAAS;AACpB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AAEA,IAAA,IAAI,OAAO,MAAA,EAAQ;AACjB,MAAA,MAAM,IAAI,KAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AAEA,IAAA,IAAI,GAAA,EAAK;AACP,MAAA,MAAM,IAAA,CAAK,WAAW,GAAG,CAAA;AAAA,IAC3B,WAAW,MAAA,EAAQ;AACjB,MAAA,MAAM,IAAA,CAAK,eAAe,MAAM,CAAA;AAAA,IAClC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAc,+BAAA,CACZ,KAAA,EACA,MAAA,EACA,MAAA,EACwC;AACxC,IAAA,MAAM,eAAA,GAAkB,CAAA,CAAA,EAAI,KAAA,CAAM,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,CAAA;AAE3C,IAAA,MAAM,WAAA,GAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAAA,EASb,KAAK,SAAS;AAAA;AAAA;AAAA;AAAA,EAAA,CAAA;AAMrB,IAAA,MAAM,SAAA,GAAA,CACJ,MAAM,IAAA,CAAK,MAAA,CAAO,IAAI,WAAA,EAAa;AAAA,MACjC,eAAA;AAAA,MACA,MAAA,EAAQ,IAAA,CAAK,SAAA,CAAU,MAAA,IAAU,EAAE,CAAA;AAAA,MACnC,MAAA;AAAA,MACA,kBAAkB,IAAA,CAAK,iBAAA;AAAA,MACvB,eAAe,IAAA,CAAK,cAAA;AAAA,MACpB,iBAAiB,IAAA,CAAK,sBAAA;AAAA,MACtB,gBAAgB,IAAA,CAAK;AAAA,KACtB,CAAA,EACD,IAAA;AAEF,IAAA,MAAM,UAAU,EAAC;AACjB,IAAA,KAAA,MAAW,OAAO,SAAA,EAAW;AAE3B,MAAA,IAAI,GAAA,CAAI,SAAA,KAAc,IAAA,IAAQ,GAAA,CAAI,YAAY,IAAA,EAAM;AAClD,QAAA,MAAM,QAAA,GAA8B;AAAA,UAClC,SAAS,GAAA,CAAI,OAAA;AAAA,UACb,QAAA,EAAU;AAAA,YACR,GAAG,GAAA,CAAI,QAAA;AAAA,YACP,SAAA,EAAW,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,SAAS,CAAA;AAAA,YACnC,aAAa,GAAA,CAAI;AAAA;AACnB,SACF;AACA,QAAA,OAAA,CAAQ,IAAA,CAAK,CAAC,QAAA,EAAU,GAAA,CAAI,SAAS,CAAC,CAAA;AAAA,MACxC;AAAA,IACF;AACA,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,gBAAA,CACJ,KAAA,EACA,MAAA,EACA,MAAA,GAAiB,KAAK,MAAA,EACQ;AAC9B,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACpB,MAAA,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAAA,IAClE;AACA,IAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,+BAAA;AAAA,MACzB,MAAM,IAAA,CAAK,UAAA,CAAW,UAAA,CAAW,KAAK,CAAA;AAAA,MACtC,MAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,CAAA,MAAA,KAAU,MAAA,CAAO,CAAC,CAAC,CAAA;AAAA,EACxC;AACF;;;;"}
1
+ {"version":3,"file":"pg-vector-store.cjs.js","sources":["../../src/database/pg-vector-store.ts"],"sourcesContent":["import {\n DatabaseService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport {\n VectorStore,\n EmbeddingDocument,\n EmbeddingDocumentMetadata,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { Embeddings } from '@langchain/core/embeddings';\nimport { Knex } from 'knex';\nimport { createHash } from 'crypto';\n\nexport type PgVectorStoreOptions = {\n database: DatabaseService;\n logger: LoggerService;\n config: RootConfigService;\n};\n\nexport class PgVectorStore implements VectorStore {\n private readonly tableName: string = 'embeddings';\n private embeddings?: Omit<Embeddings, 'caller'>;\n\n // Recency bias configuration\n private readonly RECENCY_WEIGHT = 0.3; // Weight for document recency (0-1)\n private readonly SIMILARITY_WEIGHT = 1 - this.RECENCY_WEIGHT; // Weight for vector similarity (0-1)\n private readonly RECENCY_HALF_LIFE_DAYS = 180; // Days until recency boost is halved (6 months)\n private readonly AGE_SCALE_FACTOR = 86400; // Seconds in a day for timestamp conversion\n\n /**\n * Creates an instance of PgVectorStore.\n * @param client - The Knex client to interact with the PostgreSQL database.\n * @param [amount=4] - The number of embeddings to store.\n * @param [chunkSize=500] - The size of each chunk of embeddings.\n */\n constructor(\n private readonly client: Knex,\n private readonly logger: LoggerService,\n private readonly amount: number = 4,\n private readonly chunkSize: number = 500,\n ) {}\n\n static async fromConfig({ config, database, logger }: PgVectorStoreOptions) {\n const client = await database.getClient();\n const chunkSize = config.getOptionalNumber(\n 'aiAssistant.storage.pgVector.chunkSize',\n );\n const amount = config.getOptionalNumber(\n 'aiAssistant.storage.pgVector.amount',\n );\n\n return new PgVectorStore(client, logger, amount, chunkSize);\n }\n\n connectEmbeddings(embeddings: Omit<Embeddings, 'caller'>) {\n if (this.embeddings) {\n this.logger.warn('Embeddings already connected, overwriting.');\n }\n this.embeddings = embeddings;\n }\n\n table() {\n return this.client(this.tableName);\n }\n\n /**\n * Add documents to the vector store.\n *\n * @param {EmbeddingDocument[]} documents - The array of documents to be added.\n * @throws {Error} When no embeddings are configured for the vector store.\n * @returns {Promise<void>} Resolves when the documents have been added successfully.\n */\n async addDocuments(documents: EmbeddingDocument[]): Promise<void> {\n if (documents.length === 0) {\n return;\n }\n\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n\n // Fetch existing documents with matching (id, source) pairs\n const conditions = documents\n .map(\n () =>\n `(metadata->>'id' = ? AND metadata->>'source' = ? AND metadata->>'chunk' = ?)`,\n )\n .join(' OR ');\n\n const params = documents.flatMap(doc => [\n doc.metadata.id,\n doc.metadata.source,\n doc.metadata.chunk,\n ]);\n\n const existingDocuments: EmbeddingDocument[] = await this.client\n .select('*')\n .from(this.tableName)\n .whereRaw(conditions, params);\n\n // Build a map for quick lookups\n const existingMap = new Map(\n existingDocuments.map(doc => [\n `${doc.metadata.id}:${doc.metadata.source}:${doc.metadata.chunk}`,\n doc,\n ]),\n );\n\n // Categorize documents\n const newDocuments: EmbeddingDocument[] = [];\n const documentsToUpdate: Array<EmbeddingDocument & { id: string }> = [];\n\n for (const doc of documents) {\n const key = `${doc.metadata.id}:${doc.metadata.source}:${doc.metadata.chunk}`;\n const existing = existingMap.get(key);\n\n if (!existing) {\n newDocuments.push(doc);\n continue;\n }\n\n const cleanedContent = doc.content.replace(/\\0/g, '');\n const newHash = createHash('sha256').update(cleanedContent).digest('hex');\n if (!existing.hash || newHash !== existing.hash) {\n documentsToUpdate.push({ ...doc, id: existing.id! });\n }\n }\n\n const unchangedCount =\n documents.length - newDocuments.length - documentsToUpdate.length;\n\n if (newDocuments.length === 0 && documentsToUpdate.length === 0) {\n this.logger.debug(\n `All ${unchangedCount} documents are unchanged, skipping processing.`,\n );\n return;\n }\n\n this.logger.info(\n `Processing ${documents.length} documents (${newDocuments.length} new, ${documentsToUpdate.length} updated, ${unchangedCount} unchanged).`,\n );\n\n // Update existing documents\n if (documentsToUpdate.length > 0) {\n const contents = documentsToUpdate.map(doc =>\n doc.content.replace(/\\0/g, ''),\n );\n const vectors = await this.embeddings!.embedDocuments(contents);\n\n const updatedDocuments: EmbeddingDocument[] = documentsToUpdate.map(\n (doc, index) => {\n const cleanedContent = contents[index];\n const vector = vectors[index];\n const hash = createHash('sha256')\n .update(cleanedContent)\n .digest('hex');\n\n return {\n ...doc,\n id: doc.id!,\n hash,\n content: cleanedContent,\n vector: `[${vector.join(',')}]`,\n lastUpdated: doc.lastUpdated ?? new Date(),\n };\n },\n );\n\n for (const doc of updatedDocuments) {\n const { id, ...updateData } = doc;\n await this.client(this.tableName).where('id', id).update(updateData);\n }\n\n this.logger.info(\n `Updated ${documentsToUpdate.length} existing documents in the vector store.`,\n );\n }\n\n if (newDocuments.length > 0) {\n const contents = newDocuments.map(doc => doc.content.replace(/\\0/g, ''));\n const vectors = await this.embeddings!.embedDocuments(contents);\n\n const rows = newDocuments.map((doc, index) => {\n const vector = vectors[index];\n const cleanedContent = contents[index];\n const hash = createHash('sha256').update(cleanedContent).digest('hex');\n\n return {\n hash,\n metadata: doc.metadata,\n lastUpdated: doc.lastUpdated ?? new Date(),\n content: cleanedContent,\n vector: `[${vector.join(',')}]`,\n };\n });\n\n await this.client.batchInsert(this.tableName, rows, this.chunkSize);\n\n this.logger.info(\n `Added ${newDocuments.length} new documents in the vector store.`,\n );\n }\n }\n\n /**\n * Deletes records from the database table by their ids.\n *\n * @param {string[]} ids - The array of ids of the records to be deleted.\n * @returns {Promise<void>} - A promise that resolves when the deletion is complete.\n */\n private async deleteById(ids: string[]) {\n await this.table().delete().whereIn('id', ids);\n }\n\n /**\n * Deletes rows from the table based on the specified filter.\n *\n * @param {EmbeddingDocMetadata} filter - The filter to apply for deletion.\n * @returns {Promise} - A Promise that resolves when the deletion is complete.\n */\n private async deleteByFilter(filter: EmbeddingDocumentMetadata) {\n const queryString = `\n DELETE FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n `;\n return this.client.raw(queryString, { filter });\n }\n\n /**\n * Deletes documents based on the provided deletion parameters.\n * Either `ids` or `filter` must be specified.\n *\n * @param {Object} deletionParams - The deletion parameters.\n * @param {Array<string>} [deletionParams.ids] - The document IDs to delete.\n * @param {EmbeddingDocMetadata} [deletionParams.filter] - The filter to match documents to be deleted.\n *\n * @return {Promise<void>} - A Promise that resolves once the documents have been deleted.\n */\n async deleteDocuments(deletionParams: {\n ids?: string[];\n filter?: EmbeddingDocumentMetadata;\n }): Promise<void> {\n const { ids, filter } = deletionParams;\n\n if (!(ids || filter)) {\n throw new Error(\n 'You must specify either ids or a filter when deleting documents.',\n );\n }\n\n if (ids && filter) {\n throw new Error(\n 'You cannot specify both ids and a filter when deleting documents.',\n );\n }\n\n if (ids) {\n await this.deleteById(ids);\n } else if (filter) {\n await this.deleteByFilter(filter);\n }\n }\n\n /**\n * Finds the most similar documents to a given query vector, along with their similarity scores.\n * Results are ranked by a weighted combination of vector similarity and document recency.\n * i.e newer documents are favored in the ranking but if no new documents exist, older but more similar documents will still be returned.\n *\n * @param {number[]} query - The query vector to compare against.\n * @param {number} amount - The maximum number of results to return.\n * @param {EmbeddingDocumentMetadata} [filter] - Optional filter to limit the search results.\n * @returns {Promise<[EmbeddingDocument, number][]>} - An array of document similarity results, where each\n * result is a tuple containing the document and its similarity score.\n */\n private async similaritySearchVectorWithScore(\n query: number[],\n amount: number,\n filter?: EmbeddingDocumentMetadata,\n ): Promise<[EmbeddingDocument, number][]> {\n const embeddingString = `[${query.join(',')}]`;\n\n const queryString = `\n SELECT\n *,\n (vector <=> :embeddingString) as \"_distance\",\n (EXTRACT(EPOCH FROM (NOW() - COALESCE(\"lastUpdated\", NOW()))) / :ageScaleFactor) as \"_age_days\",\n (\n ((vector <=> :embeddingString) * :similarityWeight) -\n (EXP(-0.693 * (EXTRACT(EPOCH FROM (NOW() - COALESCE(\"lastUpdated\", NOW()))) / :ageScaleFactor) / :recencyHalfLife) * :recencyWeight)\n ) as \"_combined_score\"\n FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n ORDER BY \"_combined_score\" ASC\n LIMIT :amount\n `;\n\n const documents = (\n await this.client.raw(queryString, {\n embeddingString,\n filter: JSON.stringify(filter ?? {}),\n amount,\n similarityWeight: this.SIMILARITY_WEIGHT,\n recencyWeight: this.RECENCY_WEIGHT,\n recencyHalfLife: this.RECENCY_HALF_LIFE_DAYS,\n ageScaleFactor: this.AGE_SCALE_FACTOR,\n })\n ).rows;\n\n const results = [] as [EmbeddingDocument, number][];\n for (const doc of documents) {\n // eslint-ignore-next-line\n if (doc._distance !== null && doc.content !== null) {\n const document: EmbeddingDocument = {\n content: doc.content,\n metadata: {\n ...doc.metadata,\n ageInDays: Math.round(doc._age_days),\n lastUpdated: doc.lastUpdated,\n },\n };\n results.push([document, doc._distance]);\n }\n }\n return results;\n }\n\n /**\n * Performs a similarity search using the given query and filter.\n *\n * @param {string} query - The query to perform the similarity search on.\n * @param {EmbeddingDocMetadata} filter - The filter to apply to the search results.\n * @param {number} [amount=4] - The number of results to return.\n * @return {Promise<EmbeddingDoc[]>} - A promise that resolves to an array of RoadieEmbeddingDoc objects representing the search results.\n * @throws {Error} - Throws an error if there are no embeddings configured for the vector store.\n */\n async similaritySearch(\n query: string,\n filter?: EmbeddingDocumentMetadata,\n amount: number = this.amount,\n ): Promise<EmbeddingDocument[]> {\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n const results = await this.similaritySearchVectorWithScore(\n await this.embeddings.embedQuery(query),\n amount,\n filter,\n );\n\n return results.map(result => result[0]);\n }\n}\n"],"names":["createHash"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ var uuid = require('uuid');
4
+
5
+ const MESSAGE_ID_NAMESPACE = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
6
+ const createDeterministicUuid = (message) => {
7
+ if (message.id && uuid.validate(message.id)) {
8
+ return message.id;
9
+ }
10
+ if (message.id) {
11
+ return uuid.v5(message.id, MESSAGE_ID_NAMESPACE);
12
+ }
13
+ return uuid.v4();
14
+ };
15
+
16
+ exports.createDeterministicUuid = createDeterministicUuid;
17
+ //# sourceMappingURL=deterministic-uuid.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deterministic-uuid.cjs.js","sources":["../../../../src/services/agent/helpers/deterministic-uuid.ts"],"sourcesContent":["import { v5 as uuidv5, v4 as uuidv4, validate } from 'uuid';\n\nconst MESSAGE_ID_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';\n\nexport const createDeterministicUuid = (message: { id?: string }): string => {\n if (message.id && validate(message.id)) {\n return message.id;\n }\n if (message.id) {\n return uuidv5(message.id, MESSAGE_ID_NAMESPACE);\n }\n\n return uuidv4();\n};\n"],"names":["validate","uuidv5","uuidv4"],"mappings":";;;;AAEA,MAAM,oBAAA,GAAuB,sCAAA;AAEtB,MAAM,uBAAA,GAA0B,CAAC,OAAA,KAAqC;AAC3E,EAAA,IAAI,OAAA,CAAQ,EAAA,IAAMA,aAAA,CAAS,OAAA,CAAQ,EAAE,CAAA,EAAG;AACtC,IAAA,OAAO,OAAA,CAAQ,EAAA;AAAA,EACjB;AACA,EAAA,IAAI,QAAQ,EAAA,EAAI;AACd,IAAA,OAAOC,OAAA,CAAO,OAAA,CAAQ,EAAA,EAAI,oBAAoB,CAAA;AAAA,EAChD;AAEA,EAAA,OAAOC,OAAA,EAAO;AAChB;;;;"}
@@ -1,17 +1,17 @@
1
1
  'use strict';
2
2
 
3
- var uuid = require('uuid');
3
+ var deterministicUuid = require('./deterministic-uuid.cjs.js');
4
4
 
5
5
  const parseLangchainMessage = (message, traceId) => {
6
- const id = uuid.validate(message.id) ? message.id : uuid.v4();
7
- const role = message.getType();
6
+ const id = deterministicUuid.createDeterministicUuid(message);
7
+ const role = message.type;
8
8
  const content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
9
9
  const metadata = {};
10
10
  if (role === "ai") {
11
11
  const aiMessage = message;
12
12
  metadata.toolCalls = aiMessage.tool_calls || [];
13
- metadata.finishReason = aiMessage.response_metadata.finish_reason || void 0;
14
- metadata.modelName = aiMessage.response_metadata.model_name || void 0;
13
+ metadata.finishReason = aiMessage.response_metadata?.finish_reason || void 0;
14
+ metadata.modelName = aiMessage.response_metadata?.model_name || void 0;
15
15
  }
16
16
  if (role === "tool") {
17
17
  const toolMessage = message;
@@ -1 +1 @@
1
- {"version":3,"file":"message-parser.cjs.js","sources":["../../../../src/services/agent/helpers/message-parser.ts"],"sourcesContent":["import { AIMessage, ToolMessage, BaseMessage } from '@langchain/core/messages';\n\nimport {\n JsonObject,\n Message,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { v4 as uuid, validate } from 'uuid';\n\nexport const parseLangchainMessage = (\n message: BaseMessage,\n traceId: string,\n): Message => {\n const id = validate(message.id) ? message.id : uuid();\n const role = message.getType();\n const content =\n typeof message.content === 'string'\n ? message.content\n : JSON.stringify(message.content);\n\n const metadata: JsonObject = {};\n\n if (role === 'ai') {\n const aiMessage = message as AIMessage;\n\n metadata.toolCalls = aiMessage.tool_calls || [];\n metadata.finishReason =\n aiMessage.response_metadata.finish_reason || undefined;\n metadata.modelName = aiMessage.response_metadata.model_name || undefined;\n }\n\n if (role === 'tool') {\n const toolMessage = message as ToolMessage;\n metadata.name = toolMessage.name || '';\n }\n\n return {\n id,\n role,\n content,\n metadata,\n score: 0,\n traceId,\n };\n};\n"],"names":["validate","uuid"],"mappings":";;;;AAQO,MAAM,qBAAA,GAAwB,CACnC,OAAA,EACA,OAAA,KACY;AACZ,EAAA,MAAM,KAAKA,aAAA,CAAS,OAAA,CAAQ,EAAE,CAAA,GAAI,OAAA,CAAQ,KAAKC,OAAA,EAAK;AACpD,EAAA,MAAM,IAAA,GAAO,QAAQ,OAAA,EAAQ;AAC7B,EAAA,MAAM,OAAA,GACJ,OAAO,OAAA,CAAQ,OAAA,KAAY,QAAA,GACvB,QAAQ,OAAA,GACR,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,OAAO,CAAA;AAEpC,EAAA,MAAM,WAAuB,EAAC;AAE9B,EAAA,IAAI,SAAS,IAAA,EAAM;AACjB,IAAA,MAAM,SAAA,GAAY,OAAA;AAElB,IAAA,QAAA,CAAS,SAAA,GAAY,SAAA,CAAU,UAAA,IAAc,EAAC;AAC9C,IAAA,QAAA,CAAS,YAAA,GACP,SAAA,CAAU,iBAAA,CAAkB,aAAA,IAAiB,MAAA;AAC/C,IAAA,QAAA,CAAS,SAAA,GAAY,SAAA,CAAU,iBAAA,CAAkB,UAAA,IAAc,MAAA;AAAA,EACjE;AAEA,EAAA,IAAI,SAAS,MAAA,EAAQ;AACnB,IAAA,MAAM,WAAA,GAAc,OAAA;AACpB,IAAA,QAAA,CAAS,IAAA,GAAO,YAAY,IAAA,IAAQ,EAAA;AAAA,EACtC;AAEA,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,IAAA;AAAA,IACA,OAAA;AAAA,IACA,QAAA;AAAA,IACA,KAAA,EAAO,CAAA;AAAA,IACP;AAAA,GACF;AACF;;;;"}
1
+ {"version":3,"file":"message-parser.cjs.js","sources":["../../../../src/services/agent/helpers/message-parser.ts"],"sourcesContent":["import { AIMessage, ToolMessage, BaseMessage } from '@langchain/core/messages';\n\nimport {\n JsonObject,\n Message,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { createDeterministicUuid } from './deterministic-uuid';\n\nexport const parseLangchainMessage = (\n message: BaseMessage,\n traceId: string,\n): Message => {\n const id = createDeterministicUuid(message);\n const role = message.type as Message['role'];\n const content =\n typeof message.content === 'string'\n ? message.content\n : JSON.stringify(message.content);\n\n const metadata: JsonObject = {};\n\n if (role === 'ai') {\n const aiMessage = message as AIMessage;\n\n metadata.toolCalls = aiMessage.tool_calls || [];\n\n metadata.finishReason =\n (aiMessage.response_metadata?.finish_reason as string) || undefined;\n metadata.modelName = aiMessage.response_metadata?.model_name || undefined;\n }\n\n if (role === 'tool') {\n const toolMessage = message as ToolMessage;\n metadata.name = toolMessage.name || '';\n }\n\n return {\n id,\n role,\n content,\n metadata,\n score: 0,\n traceId,\n };\n};\n"],"names":["createDeterministicUuid"],"mappings":";;;;AAQO,MAAM,qBAAA,GAAwB,CACnC,OAAA,EACA,OAAA,KACY;AACZ,EAAA,MAAM,EAAA,GAAKA,0CAAwB,OAAO,CAAA;AAC1C,EAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,EAAA,MAAM,OAAA,GACJ,OAAO,OAAA,CAAQ,OAAA,KAAY,QAAA,GACvB,QAAQ,OAAA,GACR,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,OAAO,CAAA;AAEpC,EAAA,MAAM,WAAuB,EAAC;AAE9B,EAAA,IAAI,SAAS,IAAA,EAAM;AACjB,IAAA,MAAM,SAAA,GAAY,OAAA;AAElB,IAAA,QAAA,CAAS,SAAA,GAAY,SAAA,CAAU,UAAA,IAAc,EAAC;AAE9C,IAAA,QAAA,CAAS,YAAA,GACN,SAAA,CAAU,iBAAA,EAAmB,aAAA,IAA4B,MAAA;AAC5D,IAAA,QAAA,CAAS,SAAA,GAAY,SAAA,CAAU,iBAAA,EAAmB,UAAA,IAAc,MAAA;AAAA,EAClE;AAEA,EAAA,IAAI,SAAS,MAAA,EAAQ;AACnB,IAAA,MAAM,WAAA,GAAc,OAAA;AACpB,IAAA,QAAA,CAAS,IAAA,GAAO,YAAY,IAAA,IAAQ,EAAA;AAAA,EACtC;AAEA,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,IAAA;AAAA,IACA,OAAA;AAAA,IACA,QAAA;AAAA,IACA,KAAA,EAAO,CAAA;AAAA,IACP;AAAA,GACF;AACF;;;;"}
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var backendPluginApi = require('@backstage/backend-plugin-api');
4
- var prebuilt = require('@langchain/langgraph/prebuilt');
4
+ var langchain = require('langchain');
5
5
  var toolFilter = require('./helpers/tool-filter.cjs.js');
6
6
  var prompts = require('../../constants/prompts.cjs.js');
7
7
  var prompts$1 = require('@langchain/core/prompts');
@@ -9,6 +9,7 @@ var callbacks = require('../callbacks.cjs.js');
9
9
  var model = require('../model.cjs.js');
10
10
  var tools = require('../tools.cjs.js');
11
11
  var messageParser = require('./helpers/message-parser.cjs.js');
12
+ var deterministicUuid = require('./helpers/deterministic-uuid.cjs.js');
12
13
 
13
14
  const createAgentService = ({
14
15
  model,
@@ -62,11 +63,6 @@ ${contentPrompt}`;
62
63
  context,
63
64
  systemPrompt
64
65
  });
65
- const agent = prebuilt.createReactAgent({
66
- llm,
67
- tools,
68
- prompt: agentPrompt[0].text
69
- });
70
66
  const { callbacks } = await callback.getChainCallbacks({
71
67
  userId,
72
68
  conversationId,
@@ -77,33 +73,49 @@ ${contentPrompt}`;
77
73
  conversationId,
78
74
  modelId: resolvedModelId
79
75
  });
80
- agent.config = {
81
- ...agent.config,
76
+ const agent = langchain.createAgent({
77
+ model: llm,
78
+ tools,
79
+ systemPrompt: agentPrompt[0].text
80
+ }).withConfig({
82
81
  callbacks,
83
82
  metadata,
84
83
  runId,
85
84
  runName
86
- };
85
+ });
87
86
  return agent;
88
87
  };
89
88
  const stream = async (options) => {
90
- const { messages, onStreamChunk } = options;
89
+ const { messages, onStreamChunk, onStreamEnd } = options;
91
90
  const agent = await createAgent(options);
92
91
  const promptStream = await agent.stream(
93
92
  {
94
93
  messages
95
94
  },
96
95
  {
97
- streamMode: ["values"]
96
+ streamMode: ["messages"]
98
97
  }
99
98
  );
100
- for await (const [, chunk] of promptStream) {
101
- const { messages: promptMessages } = chunk;
99
+ const promptMessages = [];
100
+ for await (const [, [chunk]] of promptStream) {
101
+ const messageChunk = chunk;
102
+ messageChunk.id = deterministicUuid.createDeterministicUuid(messageChunk);
103
+ const existingChunksIndex = promptMessages.findIndex(
104
+ (m) => m.id === messageChunk.id
105
+ );
106
+ if (existingChunksIndex === -1) {
107
+ promptMessages.push(messageChunk);
108
+ } else {
109
+ const existingChunk = promptMessages[existingChunksIndex];
110
+ existingChunk.concat(messageChunk);
111
+ promptMessages[existingChunksIndex] = existingChunk.concat(messageChunk);
112
+ }
102
113
  const parsedMessages = promptMessages.map(
103
114
  (m) => messageParser.parseLangchainMessage(m, options.metadata.runId)
104
115
  );
105
116
  onStreamChunk(parsedMessages);
106
117
  }
118
+ onStreamEnd?.();
107
119
  };
108
120
  const prompt = async (options) => {
109
121
  const {
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../../../src/services/agent/index.ts"],"sourcesContent":["import {\n BackstageCredentials,\n coreServices,\n createServiceFactory,\n createServiceRef,\n RootConfigService,\n ServiceRef,\n} from '@backstage/backend-plugin-api';\nimport { createReactAgent } from '@langchain/langgraph/prebuilt';\nimport {\n EnabledTool,\n Message,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { toolFilter } from './helpers/tool-filter';\nimport {\n DEFAULT_FORMATTING_PROMPT,\n DEFAULT_IDENTITY_PROMPT,\n DEFAULT_SYSTEM_PROMPT,\n DEFAULT_TOOL_GUIDELINE,\n} from '../../constants/prompts';\nimport { SystemMessagePromptTemplate } from '@langchain/core/prompts';\n\nimport { CallbackService, callbackServiceRef } from '../callbacks';\nimport { modelServiceRef, ModelService } from '../model';\nimport { toolsServiceRef, ToolsService } from '../tools';\n\nimport { BaseMessage } from '@langchain/core/messages';\nimport { parseLangchainMessage } from './helpers/message-parser';\n\ntype PromptOptions = {\n credentials: BackstageCredentials;\n metadata: {\n conversationId: string;\n userId: string;\n runName: string;\n runId: string;\n };\n messages: Message[];\n modelId?: string;\n tools?: EnabledTool[];\n systemPrompt?: string;\n context?: string;\n};\n\ntype StreamOptions = PromptOptions & {\n onStreamChunk: (messages: Message[]) => void;\n};\n\nexport type AgentService = {\n prompt: (options: PromptOptions) => Promise<Message[]>;\n stream: (options: StreamOptions) => Promise<void>;\n};\n\ntype AgentServiceOptions = {\n model: ModelService;\n config: RootConfigService;\n tool: ToolsService;\n callback: CallbackService;\n};\n\nconst createAgentService = ({\n model,\n tool,\n config,\n callback,\n}: AgentServiceOptions): AgentService => {\n const identityPrompt =\n config.getOptionalString('aiAssistant.prompt.identity') ||\n DEFAULT_IDENTITY_PROMPT;\n\n const formattingPrompt =\n config.getOptionalString('aiAssistant.prompt.formatting') ||\n DEFAULT_FORMATTING_PROMPT;\n\n const contentPrompt =\n config.getOptionalString('aiAssistant.prompt.content') ||\n DEFAULT_SYSTEM_PROMPT;\n\n const defaultBasePrompt = `${identityPrompt}\\n\\n${formattingPrompt}\\n\\n${contentPrompt}`;\n\n const toolGuideline =\n config.getOptionalString('aiAssistant.prompt.toolGuideline') ||\n DEFAULT_TOOL_GUIDELINE;\n\n const systemPromptTemplate = SystemMessagePromptTemplate.fromTemplate(`\n PURPOSE:\n {systemPrompt}\n\n TOOL USAGE GUIDELINES:\n {toolGuideline}\n\n Available tools:\n {toolList}\n\n Context:\n {context}`);\n\n const createAgent = async (\n options: PromptOptions,\n ): Promise<ReturnType<typeof createReactAgent>> => {\n const {\n modelId,\n credentials,\n tools: enabledTools,\n systemPrompt = defaultBasePrompt,\n context = 'none',\n metadata: { conversationId, userId, runId, runName },\n } = options;\n\n const { chatModel: llm, id: resolvedModelId } = model.getModel(\n modelId ?? 'default',\n );\n\n const tools = await tool.getPrincipalTools({\n credentials,\n filter: t => {\n return toolFilter(t, enabledTools);\n },\n });\n\n const toolList = tools.map(t => `- ${t.name}: ${t.description}`).join('\\n');\n\n const agentPrompt = await systemPromptTemplate.formatMessages({\n toolGuideline,\n toolList,\n context,\n systemPrompt,\n });\n\n const agent = createReactAgent({\n llm,\n tools,\n prompt: agentPrompt[0].text,\n });\n\n const { callbacks } = await callback.getChainCallbacks({\n userId,\n conversationId,\n modelId: resolvedModelId,\n });\n\n const { metadata } = await callback.getChainMetadata({\n userId,\n conversationId,\n modelId: resolvedModelId,\n });\n\n agent.config = {\n ...agent.config,\n callbacks,\n metadata,\n runId,\n runName,\n };\n\n return agent;\n };\n\n const stream: AgentService['stream'] = async options => {\n const { messages, onStreamChunk } = options;\n\n const agent = await createAgent(options);\n\n const promptStream = await agent.stream(\n {\n messages,\n },\n {\n streamMode: ['values'],\n },\n );\n\n for await (const [, chunk] of promptStream) {\n const { messages: promptMessages } = chunk;\n\n const parsedMessages: Message[] = (promptMessages as BaseMessage[]).map(\n m => parseLangchainMessage(m, options.metadata.runId),\n );\n\n onStreamChunk(parsedMessages);\n }\n };\n\n const prompt: AgentService['prompt'] = async options => {\n const {\n messages,\n metadata: { runId },\n } = options;\n const agent = await createAgent(options);\n\n const result = await agent.invoke({\n messages,\n });\n\n return (result.messages as BaseMessage[]).map(m =>\n parseLangchainMessage(m, runId),\n );\n };\n\n return { prompt, stream };\n};\n\nexport const agentServiceRef: ServiceRef<AgentService, 'plugin', 'singleton'> =\n createServiceRef<AgentService>({\n id: 'ai-assistant.conversation-service',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n config: coreServices.rootConfig,\n model: modelServiceRef,\n tool: toolsServiceRef,\n callback: callbackServiceRef,\n },\n factory: async options => {\n return createAgentService(options);\n },\n }),\n });\n"],"names":["DEFAULT_IDENTITY_PROMPT","DEFAULT_FORMATTING_PROMPT","DEFAULT_SYSTEM_PROMPT","DEFAULT_TOOL_GUIDELINE","SystemMessagePromptTemplate","toolFilter","createReactAgent","parseLangchainMessage","createServiceRef","createServiceFactory","coreServices","modelServiceRef","toolsServiceRef","callbackServiceRef"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs.js","sources":["../../../src/services/agent/index.ts"],"sourcesContent":["import {\n BackstageCredentials,\n coreServices,\n createServiceFactory,\n createServiceRef,\n RootConfigService,\n ServiceRef,\n} from '@backstage/backend-plugin-api';\nimport { createAgent as createLangchainAgent } from 'langchain';\nimport {\n EnabledTool,\n Message,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { toolFilter } from './helpers/tool-filter';\nimport {\n DEFAULT_FORMATTING_PROMPT,\n DEFAULT_IDENTITY_PROMPT,\n DEFAULT_SYSTEM_PROMPT,\n DEFAULT_TOOL_GUIDELINE,\n} from '../../constants/prompts';\nimport { SystemMessagePromptTemplate } from '@langchain/core/prompts';\n\nimport { CallbackService, callbackServiceRef } from '../callbacks';\nimport { modelServiceRef, ModelService } from '../model';\nimport { toolsServiceRef, ToolsService } from '../tools';\n\nimport { BaseMessage, BaseMessageChunk } from '@langchain/core/messages';\nimport { parseLangchainMessage } from './helpers/message-parser';\nimport { createDeterministicUuid } from './helpers/deterministic-uuid';\n\ntype PromptOptions = {\n credentials: BackstageCredentials;\n metadata: {\n conversationId: string;\n userId: string;\n runName: string;\n runId: string;\n };\n messages: Message[];\n modelId?: string;\n tools?: EnabledTool[];\n systemPrompt?: string;\n context?: string;\n};\n\ntype StreamOptions = PromptOptions & {\n onStreamChunk: (messages: Message[]) => void;\n onStreamEnd?: () => void;\n};\n\nexport type AgentService = {\n prompt: (options: PromptOptions) => Promise<Message[]>;\n stream: (options: StreamOptions) => Promise<void>;\n};\n\ntype AgentServiceOptions = {\n model: ModelService;\n config: RootConfigService;\n tool: ToolsService;\n callback: CallbackService;\n};\n\nconst createAgentService = ({\n model,\n tool,\n config,\n callback,\n}: AgentServiceOptions): AgentService => {\n const identityPrompt =\n config.getOptionalString('aiAssistant.prompt.identity') ||\n DEFAULT_IDENTITY_PROMPT;\n\n const formattingPrompt =\n config.getOptionalString('aiAssistant.prompt.formatting') ||\n DEFAULT_FORMATTING_PROMPT;\n\n const contentPrompt =\n config.getOptionalString('aiAssistant.prompt.content') ||\n DEFAULT_SYSTEM_PROMPT;\n\n const defaultBasePrompt = `${identityPrompt}\\n\\n${formattingPrompt}\\n\\n${contentPrompt}`;\n\n const toolGuideline =\n config.getOptionalString('aiAssistant.prompt.toolGuideline') ||\n DEFAULT_TOOL_GUIDELINE;\n\n const systemPromptTemplate = SystemMessagePromptTemplate.fromTemplate(`\n PURPOSE:\n {systemPrompt}\n\n TOOL USAGE GUIDELINES:\n {toolGuideline}\n\n Available tools:\n {toolList}\n\n Context:\n {context}`);\n\n const createAgent = async (\n options: PromptOptions,\n ): Promise<ReturnType<typeof createLangchainAgent>> => {\n const {\n modelId,\n credentials,\n tools: enabledTools,\n systemPrompt = defaultBasePrompt,\n context = 'none',\n metadata: { conversationId, userId, runId, runName },\n } = options;\n\n const { chatModel: llm, id: resolvedModelId } = model.getModel(\n modelId ?? 'default',\n );\n\n const tools = await tool.getPrincipalTools({\n credentials,\n filter: t => {\n return toolFilter(t, enabledTools);\n },\n });\n\n const toolList = tools.map(t => `- ${t.name}: ${t.description}`).join('\\n');\n\n const agentPrompt = await systemPromptTemplate.formatMessages({\n toolGuideline,\n toolList,\n context,\n systemPrompt,\n });\n\n const { callbacks } = await callback.getChainCallbacks({\n userId,\n conversationId,\n modelId: resolvedModelId,\n });\n\n const { metadata } = await callback.getChainMetadata({\n userId,\n conversationId,\n modelId: resolvedModelId,\n });\n\n const agent = createLangchainAgent({\n model: llm,\n tools,\n systemPrompt: agentPrompt[0].text,\n }).withConfig({\n callbacks,\n metadata,\n runId,\n runName,\n });\n\n return agent;\n };\n\n const stream: AgentService['stream'] = async options => {\n const { messages, onStreamChunk, onStreamEnd } = options;\n\n const agent = await createAgent(options);\n\n const promptStream = await agent.stream(\n {\n messages,\n },\n {\n streamMode: ['messages'],\n },\n );\n\n const promptMessages: BaseMessageChunk[] = [];\n\n for await (const [, [chunk]] of promptStream) {\n const messageChunk = chunk as BaseMessageChunk;\n\n messageChunk.id = createDeterministicUuid(messageChunk);\n\n const existingChunksIndex = promptMessages.findIndex(\n m => m.id === messageChunk.id,\n );\n\n if (existingChunksIndex === -1) {\n promptMessages.push(messageChunk);\n } else {\n const existingChunk = promptMessages[existingChunksIndex];\n\n existingChunk.concat(messageChunk);\n\n promptMessages[existingChunksIndex] =\n existingChunk.concat(messageChunk);\n }\n\n const parsedMessages: Message[] = promptMessages.map(m =>\n parseLangchainMessage(m, options.metadata.runId),\n );\n\n onStreamChunk(parsedMessages);\n }\n\n onStreamEnd?.();\n };\n\n const prompt: AgentService['prompt'] = async options => {\n const {\n messages,\n metadata: { runId },\n } = options;\n const agent = await createAgent(options);\n\n const result = await agent.invoke({\n messages,\n });\n\n return (result.messages as BaseMessage[]).map(m =>\n parseLangchainMessage(m, runId),\n );\n };\n\n return { prompt, stream };\n};\n\nexport const agentServiceRef: ServiceRef<AgentService, 'plugin', 'singleton'> =\n createServiceRef<AgentService>({\n id: 'ai-assistant.conversation-service',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n config: coreServices.rootConfig,\n model: modelServiceRef,\n tool: toolsServiceRef,\n callback: callbackServiceRef,\n },\n factory: async options => {\n return createAgentService(options);\n },\n }),\n });\n"],"names":["DEFAULT_IDENTITY_PROMPT","DEFAULT_FORMATTING_PROMPT","DEFAULT_SYSTEM_PROMPT","DEFAULT_TOOL_GUIDELINE","SystemMessagePromptTemplate","toolFilter","createLangchainAgent","createDeterministicUuid","parseLangchainMessage","createServiceRef","createServiceFactory","coreServices","modelServiceRef","toolsServiceRef","callbackServiceRef"],"mappings":"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"}
@@ -51,6 +51,7 @@ const createChatService = async ({
51
51
  });
52
52
  const responseMessages = [];
53
53
  const conversationMessages = [...recentConversationMessages, ...messages];
54
+ const newMessages = [];
54
55
  agent.stream({
55
56
  credentials,
56
57
  messages: conversationMessages,
@@ -64,38 +65,38 @@ const createChatService = async ({
64
65
  },
65
66
  context: context[0].text,
66
67
  onStreamChunk: async (chunkMessages) => {
67
- const newMessages = chunkMessages.filter(
68
- (m) => conversationMessages.findIndex((cm) => cm.id === m.id) === -1
68
+ if (chunkMessages.length === 0) {
69
+ return;
70
+ }
71
+ const existingNewMessageIndex = newMessages.findIndex(
72
+ (cm) => cm.id === chunkMessages[0].id
69
73
  );
70
- if (newMessages.length !== 0) {
71
- conversation.addMessages(
72
- newMessages,
73
- userEntityRef,
74
- conversationId,
75
- conversationMessages
74
+ if (existingNewMessageIndex !== -1) {
75
+ newMessages.splice(
76
+ existingNewMessageIndex,
77
+ chunkMessages.length,
78
+ ...chunkMessages
76
79
  );
77
- conversationMessages.push(...newMessages);
78
- responseMessages.push(...newMessages);
79
- for await (const m of newMessages) {
80
- const words = m.content.split(" ");
81
- const chunkSize = 5;
82
- let messageBuilder = "";
83
- for (let i = 0; i < words.length; i += chunkSize) {
84
- const wordChunk = words.slice(i, i + chunkSize).join(" ");
85
- messageBuilder = messageBuilder.concat(wordChunk).concat(" ");
86
- m.content = messageBuilder;
87
- await new Promise((resolve) => setTimeout(resolve, 50));
88
- signals.publish({
89
- channel: `ai-assistant.chat.conversation-stream:${conversationId}`,
90
- message: { messages: [m] },
91
- recipients: {
92
- type: "user",
93
- entityRef: userEntityRef
94
- }
95
- });
96
- }
97
- }
80
+ } else {
81
+ newMessages.push(...chunkMessages);
98
82
  }
83
+ responseMessages.push(...chunkMessages);
84
+ signals.publish({
85
+ channel: `ai-assistant.chat.conversation-stream:${conversationId}`,
86
+ message: { messages: chunkMessages },
87
+ recipients: {
88
+ type: "user",
89
+ entityRef: userEntityRef
90
+ }
91
+ });
92
+ },
93
+ onStreamEnd: async () => {
94
+ conversation.addMessages(
95
+ newMessages,
96
+ userEntityRef,
97
+ conversationId,
98
+ conversationMessages
99
+ );
99
100
  }
100
101
  });
101
102
  return responseMessages;
@@ -1 +1 @@
1
- {"version":3,"file":"chat.cjs.js","sources":["../../src/services/chat.ts"],"sourcesContent":["import {\n CatalogService,\n catalogServiceRef,\n} from '@backstage/plugin-catalog-node';\nimport {\n SignalsService,\n signalsServiceRef,\n} from '@backstage/plugin-signals-node';\n\nimport {\n Message,\n EnabledTool,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\n\nimport { getUser } from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { v4 as uuid } from 'uuid';\nimport type {\n BackstageCredentials,\n CacheService,\n UserInfoService,\n AuthService,\n ServiceRef,\n} from '@backstage/backend-plugin-api';\nimport {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport { ConversationService, conversationServiceRef } from './conversation';\nimport { agentServiceRef, AgentService } from './agent';\nimport { SystemMessagePromptTemplate } from '@langchain/core/prompts';\n\nexport type ChatServiceOptions = {\n signals: SignalsService;\n catalog: CatalogService;\n cache: CacheService;\n auth: AuthService;\n userInfo: UserInfoService;\n conversation: ConversationService;\n agent: AgentService;\n};\n\ntype PromptOptions = {\n credentials: BackstageCredentials;\n messages: Message[];\n conversationId: string;\n stream?: boolean;\n tools?: EnabledTool[];\n modelId?: string;\n};\n\nexport type ChatService = {\n prompt: (options: PromptOptions) => Promise<Message[]>;\n};\n\nexport const createChatService = async ({\n signals,\n catalog,\n cache,\n auth,\n userInfo,\n conversation,\n agent,\n}: ChatServiceOptions): Promise<ChatService> => {\n const contextPromptTemplate = SystemMessagePromptTemplate.fromTemplate(`\n Calling User:\n {user}`);\n\n const prompt: ChatService['prompt'] = async ({\n conversationId,\n messages,\n stream = true,\n credentials,\n tools: enabledTools,\n modelId,\n }: PromptOptions) => {\n const streamFn = async () => {\n const { userEntityRef } = await userInfo.getUserInfo(credentials);\n const recentConversationMessages =\n await conversation.getRecentConversationMessages({\n conversationId,\n userEntityRef,\n limit: 10,\n excludeRoles: ['tool'],\n });\n\n const user = await getUser(cache, userEntityRef, catalog, auth);\n\n const messagesWithoutSystem = messages.filter(m => m.role !== 'system');\n\n conversation.addMessages(\n messagesWithoutSystem,\n userEntityRef,\n conversationId,\n recentConversationMessages,\n );\n\n const traceId = uuid();\n\n const context = await contextPromptTemplate.formatMessages({\n user,\n });\n\n const responseMessages: Message[] = [];\n\n const conversationMessages = [...recentConversationMessages, ...messages];\n\n agent.stream({\n credentials,\n messages: conversationMessages,\n tools: enabledTools,\n modelId,\n metadata: {\n conversationId,\n userId: userEntityRef,\n runName: 'ai-assistant-chat',\n runId: traceId,\n },\n context: context[0].text,\n onStreamChunk: async chunkMessages => {\n const newMessages: Message[] = chunkMessages.filter(\n m => conversationMessages.findIndex(cm => cm.id === m.id) === -1,\n );\n\n if (newMessages.length !== 0) {\n conversation.addMessages(\n newMessages,\n userEntityRef,\n conversationId,\n conversationMessages,\n );\n\n conversationMessages.push(...newMessages);\n responseMessages.push(...newMessages);\n\n // Simulate streaming until langchain messages error is better understood\n for await (const m of newMessages) {\n const words = m.content.split(' ');\n const chunkSize = 5; // Send 5 words at a time\n let messageBuilder = '';\n\n for (let i = 0; i < words.length; i += chunkSize) {\n const wordChunk = words.slice(i, i + chunkSize).join(' ');\n messageBuilder = messageBuilder.concat(wordChunk).concat(' ');\n m.content = messageBuilder;\n\n await new Promise(resolve => setTimeout(resolve, 50));\n\n signals.publish({\n channel: `ai-assistant.chat.conversation-stream:${conversationId}`,\n message: { messages: [m] },\n recipients: {\n type: 'user',\n entityRef: userEntityRef,\n },\n });\n }\n }\n }\n },\n });\n\n return responseMessages;\n };\n\n return stream ? await streamFn() : [];\n };\n\n return {\n prompt,\n };\n};\n\nexport const chatServiceRef: ServiceRef<ChatService, 'plugin', 'singleton'> =\n createServiceRef<ChatService>({\n id: 'ai-assistant.chat-service',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n cache: coreServices.cache,\n auth: coreServices.auth,\n userInfo: coreServices.userInfo,\n signals: signalsServiceRef,\n catalog: catalogServiceRef,\n conversation: conversationServiceRef,\n agent: agentServiceRef,\n },\n factory: async options => {\n return createChatService(options);\n },\n }),\n });\n"],"names":["SystemMessagePromptTemplate","getUser","uuid","createServiceRef","createServiceFactory","coreServices","signalsServiceRef","catalogServiceRef","conversationServiceRef","agentServiceRef"],"mappings":";;;;;;;;;;;AAuDO,MAAM,oBAAoB,OAAO;AAAA,EACtC,OAAA;AAAA,EACA,OAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA;AAAA,EACA,QAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAA,KAAgD;AAC9C,EAAA,MAAM,qBAAA,GAAwBA,oCAA4B,YAAA,CAAa;AAAA;AAAA,UAAA,CAE9D,CAAA;AAET,EAAA,MAAM,SAAgC,OAAO;AAAA,IAC3C,cAAA;AAAA,IACA,QAAA;AAAA,IACA,MAAA,GAAS,IAAA;AAAA,IACT,WAAA;AAAA,IACA,KAAA,EAAO,YAAA;AAAA,IACP;AAAA,GACF,KAAqB;AACnB,IAAA,MAAM,WAAW,YAAY;AAC3B,MAAA,MAAM,EAAE,aAAA,EAAc,GAAI,MAAM,QAAA,CAAS,YAAY,WAAW,CAAA;AAChE,MAAA,MAAM,0BAAA,GACJ,MAAM,YAAA,CAAa,6BAAA,CAA8B;AAAA,QAC/C,cAAA;AAAA,QACA,aAAA;AAAA,QACA,KAAA,EAAO,EAAA;AAAA,QACP,YAAA,EAAc,CAAC,MAAM;AAAA,OACtB,CAAA;AAEH,MAAA,MAAM,OAAO,MAAMC,sCAAA,CAAQ,KAAA,EAAO,aAAA,EAAe,SAAS,IAAI,CAAA;AAE9D,MAAA,MAAM,wBAAwB,QAAA,CAAS,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,QAAQ,CAAA;AAEtE,MAAA,YAAA,CAAa,WAAA;AAAA,QACX,qBAAA;AAAA,QACA,aAAA;AAAA,QACA,cAAA;AAAA,QACA;AAAA,OACF;AAEA,MAAA,MAAM,UAAUC,OAAA,EAAK;AAErB,MAAA,MAAM,OAAA,GAAU,MAAM,qBAAA,CAAsB,cAAA,CAAe;AAAA,QACzD;AAAA,OACD,CAAA;AAED,MAAA,MAAM,mBAA8B,EAAC;AAErC,MAAA,MAAM,oBAAA,GAAuB,CAAC,GAAG,0BAAA,EAA4B,GAAG,QAAQ,CAAA;AAExE,MAAA,KAAA,CAAM,MAAA,CAAO;AAAA,QACX,WAAA;AAAA,QACA,QAAA,EAAU,oBAAA;AAAA,QACV,KAAA,EAAO,YAAA;AAAA,QACP,OAAA;AAAA,QACA,QAAA,EAAU;AAAA,UACR,cAAA;AAAA,UACA,MAAA,EAAQ,aAAA;AAAA,UACR,OAAA,EAAS,mBAAA;AAAA,UACT,KAAA,EAAO;AAAA,SACT;AAAA,QACA,OAAA,EAAS,OAAA,CAAQ,CAAC,CAAA,CAAE,IAAA;AAAA,QACpB,aAAA,EAAe,OAAM,aAAA,KAAiB;AACpC,UAAA,MAAM,cAAyB,aAAA,CAAc,MAAA;AAAA,YAC3C,CAAA,CAAA,KAAK,qBAAqB,SAAA,CAAU,CAAA,EAAA,KAAM,GAAG,EAAA,KAAO,CAAA,CAAE,EAAE,CAAA,KAAM;AAAA,WAChE;AAEA,UAAA,IAAI,WAAA,CAAY,WAAW,CAAA,EAAG;AAC5B,YAAA,YAAA,CAAa,WAAA;AAAA,cACX,WAAA;AAAA,cACA,aAAA;AAAA,cACA,cAAA;AAAA,cACA;AAAA,aACF;AAEA,YAAA,oBAAA,CAAqB,IAAA,CAAK,GAAG,WAAW,CAAA;AACxC,YAAA,gBAAA,CAAiB,IAAA,CAAK,GAAG,WAAW,CAAA;AAGpC,YAAA,WAAA,MAAiB,KAAK,WAAA,EAAa;AACjC,cAAA,MAAM,KAAA,GAAQ,CAAA,CAAE,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA;AACjC,cAAA,MAAM,SAAA,GAAY,CAAA;AAClB,cAAA,IAAI,cAAA,GAAiB,EAAA;AAErB,cAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,MAAA,EAAQ,KAAK,SAAA,EAAW;AAChD,gBAAA,MAAM,SAAA,GAAY,MAAM,KAAA,CAAM,CAAA,EAAG,IAAI,SAAS,CAAA,CAAE,KAAK,GAAG,CAAA;AACxD,gBAAA,cAAA,GAAiB,cAAA,CAAe,MAAA,CAAO,SAAS,CAAA,CAAE,OAAO,GAAG,CAAA;AAC5D,gBAAA,CAAA,CAAE,OAAA,GAAU,cAAA;AAEZ,gBAAA,MAAM,IAAI,OAAA,CAAQ,CAAA,OAAA,KAAW,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAEpD,gBAAA,OAAA,CAAQ,OAAA,CAAQ;AAAA,kBACd,OAAA,EAAS,yCAAyC,cAAc,CAAA,CAAA;AAAA,kBAChE,OAAA,EAAS,EAAE,QAAA,EAAU,CAAC,CAAC,CAAA,EAAE;AAAA,kBACzB,UAAA,EAAY;AAAA,oBACV,IAAA,EAAM,MAAA;AAAA,oBACN,SAAA,EAAW;AAAA;AACb,iBACD,CAAA;AAAA,cACH;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,OACD,CAAA;AAED,MAAA,OAAO,gBAAA;AAAA,IACT,CAAA;AAEA,IAAA,OAAO,MAAA,GAAS,MAAM,QAAA,EAAS,GAAI,EAAC;AAAA,EACtC,CAAA;AAEA,EAAA,OAAO;AAAA,IACL;AAAA,GACF;AACF;AAEO,MAAM,iBACXC,iCAAA,CAA8B;AAAA,EAC5B,EAAA,EAAI,2BAAA;AAAA,EACJ,cAAA,EAAgB,OAAM,OAAA,KACpBC,qCAAA,CAAqB;AAAA,IACnB,OAAA;AAAA,IACA,IAAA,EAAM;AAAA,MACJ,OAAOC,6BAAA,CAAa,KAAA;AAAA,MACpB,MAAMA,6BAAA,CAAa,IAAA;AAAA,MACnB,UAAUA,6BAAA,CAAa,QAAA;AAAA,MACvB,OAAA,EAASC,mCAAA;AAAA,MACT,OAAA,EAASC,mCAAA;AAAA,MACT,YAAA,EAAcC,mCAAA;AAAA,MACd,KAAA,EAAOC;AAAA,KACT;AAAA,IACA,OAAA,EAAS,OAAM,OAAA,KAAW;AACxB,MAAA,OAAO,kBAAkB,OAAO,CAAA;AAAA,IAClC;AAAA,GACD;AACL,CAAC;;;;;"}
1
+ {"version":3,"file":"chat.cjs.js","sources":["../../src/services/chat.ts"],"sourcesContent":["import {\n CatalogService,\n catalogServiceRef,\n} from '@backstage/plugin-catalog-node';\nimport {\n SignalsService,\n signalsServiceRef,\n} from '@backstage/plugin-signals-node';\n\nimport {\n Message,\n EnabledTool,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\n\nimport { getUser } from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { v4 as uuid } from 'uuid';\nimport type {\n BackstageCredentials,\n CacheService,\n UserInfoService,\n AuthService,\n ServiceRef,\n} from '@backstage/backend-plugin-api';\nimport {\n coreServices,\n createServiceFactory,\n createServiceRef,\n} from '@backstage/backend-plugin-api';\nimport { ConversationService, conversationServiceRef } from './conversation';\nimport { agentServiceRef, AgentService } from './agent';\nimport { SystemMessagePromptTemplate } from '@langchain/core/prompts';\n\nexport type ChatServiceOptions = {\n signals: SignalsService;\n catalog: CatalogService;\n cache: CacheService;\n auth: AuthService;\n userInfo: UserInfoService;\n conversation: ConversationService;\n agent: AgentService;\n};\n\ntype PromptOptions = {\n credentials: BackstageCredentials;\n messages: Message[];\n conversationId: string;\n stream?: boolean;\n tools?: EnabledTool[];\n modelId?: string;\n};\n\nexport type ChatService = {\n prompt: (options: PromptOptions) => Promise<Message[]>;\n};\n\nexport const createChatService = async ({\n signals,\n catalog,\n cache,\n auth,\n userInfo,\n conversation,\n agent,\n}: ChatServiceOptions): Promise<ChatService> => {\n const contextPromptTemplate = SystemMessagePromptTemplate.fromTemplate(`\n Calling User:\n {user}`);\n\n const prompt: ChatService['prompt'] = async ({\n conversationId,\n messages,\n stream = true,\n credentials,\n tools: enabledTools,\n modelId,\n }: PromptOptions) => {\n const streamFn = async () => {\n const { userEntityRef } = await userInfo.getUserInfo(credentials);\n const recentConversationMessages =\n await conversation.getRecentConversationMessages({\n conversationId,\n userEntityRef,\n limit: 10,\n excludeRoles: ['tool'],\n });\n\n const user = await getUser(cache, userEntityRef, catalog, auth);\n\n const messagesWithoutSystem = messages.filter(m => m.role !== 'system');\n\n conversation.addMessages(\n messagesWithoutSystem,\n userEntityRef,\n conversationId,\n recentConversationMessages,\n );\n\n const traceId = uuid();\n\n const context = await contextPromptTemplate.formatMessages({\n user,\n });\n\n const responseMessages: Message[] = [];\n\n const conversationMessages = [...recentConversationMessages, ...messages];\n\n const newMessages: Message[] = [];\n\n agent.stream({\n credentials,\n messages: conversationMessages,\n tools: enabledTools,\n modelId,\n metadata: {\n conversationId,\n userId: userEntityRef,\n runName: 'ai-assistant-chat',\n runId: traceId,\n },\n context: context[0].text,\n onStreamChunk: async chunkMessages => {\n if (chunkMessages.length === 0) {\n return;\n }\n\n const existingNewMessageIndex = newMessages.findIndex(\n cm => cm.id === chunkMessages[0].id,\n );\n\n if (existingNewMessageIndex !== -1) {\n newMessages.splice(\n existingNewMessageIndex,\n chunkMessages.length,\n ...chunkMessages,\n );\n } else {\n newMessages.push(...chunkMessages);\n }\n\n responseMessages.push(...chunkMessages);\n\n signals.publish({\n channel: `ai-assistant.chat.conversation-stream:${conversationId}`,\n message: { messages: chunkMessages },\n recipients: {\n type: 'user',\n entityRef: userEntityRef,\n },\n });\n },\n onStreamEnd: async () => {\n conversation.addMessages(\n newMessages,\n userEntityRef,\n conversationId,\n conversationMessages,\n );\n },\n });\n\n return responseMessages;\n };\n\n return stream ? await streamFn() : [];\n };\n\n return {\n prompt,\n };\n};\n\nexport const chatServiceRef: ServiceRef<ChatService, 'plugin', 'singleton'> =\n createServiceRef<ChatService>({\n id: 'ai-assistant.chat-service',\n defaultFactory: async service =>\n createServiceFactory({\n service,\n deps: {\n cache: coreServices.cache,\n auth: coreServices.auth,\n userInfo: coreServices.userInfo,\n signals: signalsServiceRef,\n catalog: catalogServiceRef,\n conversation: conversationServiceRef,\n agent: agentServiceRef,\n },\n factory: async options => {\n return createChatService(options);\n },\n }),\n });\n"],"names":["SystemMessagePromptTemplate","getUser","uuid","createServiceRef","createServiceFactory","coreServices","signalsServiceRef","catalogServiceRef","conversationServiceRef","agentServiceRef"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweetoburrito/backstage-plugin-ai-assistant-backend",
3
- "version": "0.15.3",
3
+ "version": "0.16.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
@@ -39,15 +39,15 @@
39
39
  "@backstage/errors": "backstage:^",
40
40
  "@backstage/plugin-catalog-node": "backstage:^",
41
41
  "@backstage/plugin-signals-node": "backstage:^",
42
- "@langchain/core": "^0.3.80",
43
- "@langchain/langgraph": "^0.4.9",
44
- "@langchain/mcp-adapters": "^1.0.0",
45
- "@langchain/textsplitters": "^0.1.0",
46
- "@sweetoburrito/backstage-plugin-ai-assistant-common": "^0.8.0",
47
- "@sweetoburrito/backstage-plugin-ai-assistant-node": "^0.10.0",
42
+ "@langchain/core": "^1.1.19",
43
+ "@langchain/mcp-adapters": "^1.1.2",
44
+ "@langchain/textsplitters": "^1.0.1",
45
+ "@sweetoburrito/backstage-plugin-ai-assistant-common": "^0.9.0",
46
+ "@sweetoburrito/backstage-plugin-ai-assistant-node": "^0.11.0",
48
47
  "express": "^4.17.1",
49
48
  "express-promise-router": "^4.1.0",
50
49
  "knex": "^3.1.0",
50
+ "langchain": "^1.2.17",
51
51
  "uuid": "^11.1.0",
52
52
  "zod": "^4.1.11"
53
53
  },
@@ -60,7 +60,7 @@
60
60
  "@backstage/plugin-events-backend": "backstage:^",
61
61
  "@backstage/plugin-signals-backend": "backstage:^",
62
62
  "@backstage/types": "backstage:^",
63
- "@drodil/backstage-plugin-qeta-backend": "^3.46.0",
63
+ "@drodil/backstage-plugin-qeta-backend": "^3.58.0",
64
64
  "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-callback-provider-langfuse": "workspace:^",
65
65
  "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-embeddings-provider-azure-open-ai": "workspace:^",
66
66
  "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-embeddings-provider-ollama": "workspace:^",