@sweetoburrito/backstage-plugin-ai-assistant-backend 0.6.0 → 0.7.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.
- package/dist/constants/prompts.cjs.js +14 -1
- package/dist/constants/prompts.cjs.js.map +1 -1
- package/dist/database/chat-store.cjs.js +9 -7
- package/dist/database/chat-store.cjs.js.map +1 -1
- package/dist/database/pg-vector-store.cjs.js.map +1 -1
- package/dist/plugin.cjs.js +14 -4
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/services/chat.cjs.js +132 -97
- package/dist/services/chat.cjs.js.map +1 -1
- package/dist/services/router/middleware/validation.cjs.js.map +1 -1
- package/dist/services/summarizer.cjs.js +41 -0
- package/dist/services/summarizer.cjs.js.map +1 -0
- package/dist/services/tools/searchKnowledge.cjs.js +47 -0
- package/dist/services/tools/searchKnowledge.cjs.js.map +1 -0
- package/migrations/20251005_tools.js +32 -0
- package/package.json +6 -4
- package/dist/services/prompt.cjs.js +0 -31
- package/dist/services/prompt.cjs.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const DEFAULT_SUMMARY_PROMPT = "
|
|
3
|
+
const DEFAULT_SUMMARY_PROMPT = "Summarize this conversation in a concise manner. The summary should capture the main points. Return the summary only, without any additional text.";
|
|
4
4
|
const DEFAULT_SYSTEM_PROMPT = `
|
|
5
5
|
You are a helpful assistant that answers questions based on provided context from various documents. The context may come from sources such as internal wikis, code repositories, technical documentation, or other structured or unstructured data.
|
|
6
6
|
|
|
@@ -12,8 +12,21 @@ Rules:
|
|
|
12
12
|
5. Return only the relevant information without any filler or unnecessary details.
|
|
13
13
|
6. If you don't know the answer, admit it and suggest ways to find the information.
|
|
14
14
|
7. Always return a well-structured response using markdown.
|
|
15
|
+
8. **Actively use available tools** to enhance your responses:
|
|
16
|
+
9. Adapt your approach based on the specific tools and capabilities available in the current session
|
|
17
|
+
10. When you have a link to an image, include it in your response using markdown format .
|
|
18
|
+
`;
|
|
19
|
+
const DEFAULT_TOOL_GUIDELINE = `
|
|
20
|
+
TOOL USAGE GUIDELINES:
|
|
21
|
+
- Only use tools when explicitly needed to answer the user's question
|
|
22
|
+
- Read tool descriptions carefully before using them
|
|
23
|
+
- If you can answer without tools, do so
|
|
24
|
+
- When using tools, always explain why you're using each tool
|
|
25
|
+
- Use tools in logical sequence, not randomly
|
|
26
|
+
- If a tool fails, try an alternative approach before using another tool
|
|
15
27
|
`;
|
|
16
28
|
|
|
17
29
|
exports.DEFAULT_SUMMARY_PROMPT = DEFAULT_SUMMARY_PROMPT;
|
|
18
30
|
exports.DEFAULT_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT;
|
|
31
|
+
exports.DEFAULT_TOOL_GUIDELINE = DEFAULT_TOOL_GUIDELINE;
|
|
19
32
|
//# sourceMappingURL=prompts.cjs.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompts.cjs.js","sources":["../../src/constants/prompts.ts"],"sourcesContent":["export const DEFAULT_SUMMARY_PROMPT =\n '
|
|
1
|
+
{"version":3,"file":"prompts.cjs.js","sources":["../../src/constants/prompts.ts"],"sourcesContent":["export const DEFAULT_SUMMARY_PROMPT =\n 'Summarize this conversation in a concise manner. The summary should capture the main points. Return the summary only, without any additional text.';\n\nexport const DEFAULT_SYSTEM_PROMPT = `\nYou are a helpful assistant that answers questions based on provided context from various documents. The context may come from sources such as internal wikis, code repositories, technical documentation, or other structured or unstructured data.\n\nRules:\n1. Always base your answers on the provided context. Do not make up information.\n2. When relevant, cite or reference the source information provided in the context.\n3. Format answers clearly and concisely. Use bullet points for lists when appropriate.\n4. Maintain a professional, friendly, and helpful tone.\n5. Return only the relevant information without any filler or unnecessary details.\n6. If you don't know the answer, admit it and suggest ways to find the information.\n7. Always return a well-structured response using markdown.\n8. **Actively use available tools** to enhance your responses:\n9. Adapt your approach based on the specific tools and capabilities available in the current session\n10. When you have a link to an image, include it in your response using markdown format .\n`;\n\nexport const DEFAULT_TOOL_GUIDELINE = `\nTOOL USAGE GUIDELINES:\n- Only use tools when explicitly needed to answer the user's question\n- Read tool descriptions carefully before using them\n- If you can answer without tools, do so\n- When using tools, always explain why you're using each tool\n- Use tools in logical sequence, not randomly\n- If a tool fails, try an alternative approach before using another tool\n`;\n"],"names":[],"mappings":";;AAAO,MAAM,sBAAA,GACX;AAEK,MAAM,qBAAA,GAAwB;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAgB9B,MAAM,sBAAA,GAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;;;;"}
|
|
@@ -20,16 +20,20 @@ class ChatStore {
|
|
|
20
20
|
conversationTable() {
|
|
21
21
|
return this.client(CONVERSATION_TABLE_NAME);
|
|
22
22
|
}
|
|
23
|
-
async getChatMessages(conversationId, userRef, limit) {
|
|
23
|
+
async getChatMessages(conversationId, userRef, limit, excludeRoles) {
|
|
24
24
|
let query = this.messageTable().where({ conversation_id: conversationId, userRef }).select("*").orderBy("created_at", "asc");
|
|
25
25
|
if (typeof limit === "number") {
|
|
26
26
|
query = query.limit(limit).orderBy("created_at", "desc");
|
|
27
27
|
}
|
|
28
|
+
if (excludeRoles && excludeRoles.length > 0) {
|
|
29
|
+
query = query.whereNotIn("role", excludeRoles);
|
|
30
|
+
}
|
|
28
31
|
const rows = await query;
|
|
29
32
|
const chatMessages = rows.map((row) => ({
|
|
30
33
|
role: row.role,
|
|
31
34
|
content: row.content,
|
|
32
|
-
id: row.id
|
|
35
|
+
id: row.id,
|
|
36
|
+
metadata: row.metadata
|
|
33
37
|
}));
|
|
34
38
|
return chatMessages;
|
|
35
39
|
}
|
|
@@ -39,6 +43,7 @@ class ChatStore {
|
|
|
39
43
|
conversation_id: conversationId,
|
|
40
44
|
role: msg.role,
|
|
41
45
|
content: msg.content,
|
|
46
|
+
metadata: msg.metadata,
|
|
42
47
|
userRef,
|
|
43
48
|
created_at: this.client.fn.now()
|
|
44
49
|
}));
|
|
@@ -47,13 +52,10 @@ class ChatStore {
|
|
|
47
52
|
async updateMessage(message) {
|
|
48
53
|
await this.messageTable().where({ id: message.id }).update({
|
|
49
54
|
role: message.role,
|
|
50
|
-
content: message.content
|
|
55
|
+
content: message.content,
|
|
56
|
+
metadata: message.metadata
|
|
51
57
|
});
|
|
52
58
|
}
|
|
53
|
-
async getConversationSize(conversationId) {
|
|
54
|
-
const count = await this.messageTable().where({ conversation_id: conversationId }).count("* as count").first();
|
|
55
|
-
return count?.count ? Number(count.count) : 0;
|
|
56
|
-
}
|
|
57
59
|
async getConversation(conversationId, userRef) {
|
|
58
60
|
const row = await this.conversationTable().where({ id: conversationId, userRef }).first();
|
|
59
61
|
if (!row) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-store.cjs.js","sources":["../../src/database/chat-store.ts"],"sourcesContent":["import { DatabaseService } from '@backstage/backend-plugin-api';\nimport {\n Message,\n Conversation,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\n\nimport { Knex } from 'knex';\n\nconst MESSAGE_TABLE_NAME = 'message';\nconst CONVERSATION_TABLE_NAME = 'conversation';\n\nexport type ChatStoreOptions = {\n database: DatabaseService;\n};\n\nexport class ChatStore {\n /**\n * Creates an instance of ChatStore.\n * @param client - The Knex client to interact with the PostgreSQL database.\n */\n constructor(private readonly client: Knex) {}\n\n static async fromConfig({ database }: ChatStoreOptions) {\n const client = await database.getClient();\n return new ChatStore(client);\n }\n\n messageTable() {\n return this.client(MESSAGE_TABLE_NAME);\n }\n\n conversationTable() {\n return this.client(CONVERSATION_TABLE_NAME);\n }\n\n async getChatMessages(\n conversationId: string,\n userRef: string,\n limit?: number,\n ): Promise<Required<Message>[]> {\n let query = this.messageTable()\n .where({ conversation_id: conversationId, userRef })\n .select('*')\n .orderBy('created_at', 'asc');\n\n if (typeof limit === 'number') {\n query = query.limit(limit).orderBy('created_at', 'desc');\n }\n\n const rows = await query;\n\n const chatMessages: Required<Message>[] = rows.map(row => ({\n role: row.role,\n content: row.content,\n id: row.id,\n }));\n\n return chatMessages;\n }\n\n async addChatMessage(\n messages: Message[],\n userRef: string,\n conversationId: string,\n ): Promise<void> {\n const rows = messages.map(msg => ({\n id: msg.id,\n conversation_id: conversationId,\n role: msg.role,\n content: msg.content,\n userRef,\n created_at: this.client.fn.now(),\n }));\n\n await this.messageTable().insert(rows);\n }\n\n async updateMessage(message: Required<Message>) {\n await this.messageTable().where({ id: message.id }).update({\n role: message.role,\n content: message.content,\n
|
|
1
|
+
{"version":3,"file":"chat-store.cjs.js","sources":["../../src/database/chat-store.ts"],"sourcesContent":["import { DatabaseService } from '@backstage/backend-plugin-api';\nimport {\n Message,\n Conversation,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\n\nimport { Knex } from 'knex';\n\nconst MESSAGE_TABLE_NAME = 'message';\nconst CONVERSATION_TABLE_NAME = 'conversation';\n\nexport type ChatStoreOptions = {\n database: DatabaseService;\n};\n\nexport class ChatStore {\n /**\n * Creates an instance of ChatStore.\n * @param client - The Knex client to interact with the PostgreSQL database.\n */\n constructor(private readonly client: Knex) {}\n\n static async fromConfig({ database }: ChatStoreOptions) {\n const client = await database.getClient();\n return new ChatStore(client);\n }\n\n messageTable() {\n return this.client(MESSAGE_TABLE_NAME);\n }\n\n conversationTable() {\n return this.client(CONVERSATION_TABLE_NAME);\n }\n\n async getChatMessages(\n conversationId: string,\n userRef: string,\n limit?: number,\n excludeRoles?: Message['role'][],\n ): Promise<Required<Message>[]> {\n let query = this.messageTable()\n .where({ conversation_id: conversationId, userRef })\n .select('*')\n .orderBy('created_at', 'asc');\n\n if (typeof limit === 'number') {\n query = query.limit(limit).orderBy('created_at', 'desc');\n }\n\n if (excludeRoles && excludeRoles.length > 0) {\n query = query.whereNotIn('role', excludeRoles);\n }\n\n const rows = await query;\n\n const chatMessages: Required<Message>[] = rows.map(row => ({\n role: row.role,\n content: row.content,\n id: row.id,\n metadata: row.metadata,\n }));\n\n return chatMessages;\n }\n\n async addChatMessage(\n messages: Message[],\n userRef: string,\n conversationId: string,\n ): Promise<void> {\n const rows = messages.map(msg => ({\n id: msg.id,\n conversation_id: conversationId,\n role: msg.role,\n content: msg.content,\n metadata: msg.metadata,\n userRef,\n created_at: this.client.fn.now(),\n }));\n\n await this.messageTable().insert(rows);\n }\n\n async updateMessage(message: Required<Message>) {\n await this.messageTable().where({ id: message.id }).update({\n role: message.role,\n content: message.content,\n metadata: message.metadata,\n });\n }\n\n async getConversation(\n conversationId: string,\n userRef: string,\n ): Promise<Conversation> {\n const row = await this.conversationTable()\n .where({ id: conversationId, userRef })\n .first();\n\n if (!row) {\n throw new Error('Conversation not found');\n }\n\n const conversation: Conversation = {\n id: row.id,\n title: row.title,\n userRef: row.userRef,\n };\n\n return conversation;\n }\n\n async createConversation(conversation: Conversation) {\n await this.conversationTable().insert({\n id: conversation.id,\n title: conversation.title,\n userRef: conversation.userRef,\n });\n }\n\n async updateConversation(conversation: Conversation) {\n await this.conversationTable().where({ id: conversation.id }).update({\n title: conversation.title,\n userRef: conversation.userRef,\n });\n }\n\n async getConversations(userRef: string): Promise<Conversation[]> {\n const rows = await this.conversationTable()\n .where({ userRef })\n .select('*')\n .orderBy('created_at', 'desc');\n\n const conversations: Conversation[] = rows.map(row => ({\n id: row.id,\n title: row.title,\n userRef: row.userRef,\n }));\n\n return conversations;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -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';\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 /**\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 const texts = documents.map(({ content }) => content);\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n\n const vectors = await this.embeddings.embedDocuments(texts);\n this.logger.info(\n `Received ${vectors.length} vectors from embeddings creation.`,\n );\n this.addVectors(vectors, documents);\n }\n\n /**\n * Adds vectors to the database along with corresponding documents.\n *\n * @param {number[][]} vectors - The vectors to be added.\n * @param {EmbeddingDoc[]} documents - The corresponding documents.\n * @return {Promise<void>} - A promise that resolves when the vectors are added successfully.\n * @throws {Error} - If there is an error inserting the vectors.\n */\n private async addVectors(\n vectors: number[][],\n documents: EmbeddingDocument[],\n ): Promise<void> {\n try {\n const rows = [];\n for (let i = 0; i < vectors.length; i += 1) {\n const embedding = vectors[i];\n const embeddingString = `[${embedding.join(',')}]`;\n const values = {\n content: documents[i].content.replace(/\\0/g, ''),\n vector: embeddingString.replace(/\\0/g, ''),\n metadata: documents[i].metadata,\n };\n rows.push(values);\n }\n\n await this.client.batchInsert(this.tableName, rows, this.chunkSize);\n } catch (e) {\n this.logger.error((e as Error).message);\n throw new Error(`Error inserting: ${(e as Error).message}`);\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 *\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 const queryString = `\n SELECT *, vector <=> :embeddingString as \"_distance\"\n FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n ORDER BY \"_distance\" 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 })\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 = {\n content: doc.content,\n metadata: doc.metadata,\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":[],"mappings":"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"}
|
|
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';\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 /**\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 const texts = documents.map(({ content }) => content);\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n\n const vectors = await this.embeddings.embedDocuments(texts);\n this.logger.info(\n `Received ${vectors.length} vectors from embeddings creation.`,\n );\n this.addVectors(vectors, documents);\n }\n\n /**\n * Adds vectors to the database along with corresponding documents.\n *\n * @param {number[][]} vectors - The vectors to be added.\n * @param {EmbeddingDoc[]} documents - The corresponding documents.\n * @return {Promise<void>} - A promise that resolves when the vectors are added successfully.\n * @throws {Error} - If there is an error inserting the vectors.\n */\n private async addVectors(\n vectors: number[][],\n documents: EmbeddingDocument[],\n ): Promise<void> {\n try {\n const rows = [];\n for (let i = 0; i < vectors.length; i += 1) {\n const embedding = vectors[i];\n const embeddingString = `[${embedding.join(',')}]`;\n const values = {\n content: documents[i].content.replace(/\\0/g, ''),\n vector: embeddingString.replace(/\\0/g, ''),\n metadata: documents[i].metadata,\n };\n rows.push(values);\n }\n\n await this.client.batchInsert(this.tableName, rows, this.chunkSize);\n } catch (e) {\n this.logger.error((e as Error).message);\n throw new Error(`Error inserting: ${(e as Error).message}`);\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 *\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 const queryString = `\n SELECT *, vector <=> :embeddingString as \"_distance\"\n FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n ORDER BY \"_distance\" 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 })\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 = {\n content: doc.content,\n metadata: doc.metadata,\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":[],"mappings":"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"}
|
package/dist/plugin.cjs.js
CHANGED
|
@@ -5,16 +5,17 @@ var index = require('./services/router/index.cjs.js');
|
|
|
5
5
|
var backstagePluginAiAssistantNode = require('@sweetoburrito/backstage-plugin-ai-assistant-node');
|
|
6
6
|
var ingestor = require('./services/ingestor.cjs.js');
|
|
7
7
|
var chat = require('./services/chat.cjs.js');
|
|
8
|
-
var prompt = require('./services/prompt.cjs.js');
|
|
9
8
|
var migrations = require('./database/migrations.cjs.js');
|
|
10
9
|
var pgVectorStore = require('./database/pg-vector-store.cjs.js');
|
|
11
10
|
var pluginSignalsNode = require('@backstage/plugin-signals-node');
|
|
11
|
+
var searchKnowledge = require('./services/tools/searchKnowledge.cjs.js');
|
|
12
12
|
|
|
13
13
|
const aiAssistantPlugin = backendPluginApi.createBackendPlugin({
|
|
14
14
|
pluginId: "ai-assistant",
|
|
15
15
|
register(env) {
|
|
16
16
|
const ingestors = [];
|
|
17
17
|
const models = [];
|
|
18
|
+
const tools = [];
|
|
18
19
|
let embeddingsProvider;
|
|
19
20
|
env.registerExtensionPoint(backstagePluginAiAssistantNode.dataIngestorExtensionPoint, {
|
|
20
21
|
registerIngestor: (ingestor) => {
|
|
@@ -41,6 +42,15 @@ const aiAssistantPlugin = backendPluginApi.createBackendPlugin({
|
|
|
41
42
|
models.push(model);
|
|
42
43
|
}
|
|
43
44
|
});
|
|
45
|
+
env.registerExtensionPoint(backstagePluginAiAssistantNode.toolExtensionPoint, {
|
|
46
|
+
register: (tool) => {
|
|
47
|
+
const existingTool = tools.find((t) => t.name === tool.name);
|
|
48
|
+
if (existingTool) {
|
|
49
|
+
throw new Error(`Tool with name ${tool.name} is already registered.`);
|
|
50
|
+
}
|
|
51
|
+
tools.push(tool);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
44
54
|
env.registerInit({
|
|
45
55
|
deps: {
|
|
46
56
|
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
@@ -66,12 +76,12 @@ const aiAssistantPlugin = backendPluginApi.createBackendPlugin({
|
|
|
66
76
|
vectorStore,
|
|
67
77
|
ingestors
|
|
68
78
|
});
|
|
69
|
-
const
|
|
79
|
+
const searchKnowledgeTool = searchKnowledge.createSearchKnowledgeTool({ vectorStore });
|
|
80
|
+
tools.push(searchKnowledgeTool);
|
|
70
81
|
const chat$1 = await chat.createChatService({
|
|
71
82
|
...options,
|
|
72
83
|
models,
|
|
73
|
-
|
|
74
|
-
promptBuilder
|
|
84
|
+
tools
|
|
75
85
|
});
|
|
76
86
|
httpRouter.use(await index.createRouter({ ...options, chat: chat$1 }));
|
|
77
87
|
dataIngestionPipeline.start();
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './services/router';\nimport {\n dataIngestorExtensionPoint,\n EmbeddingsProvider,\n embeddingsProviderExtensionPoint,\n Ingestor,\n Model,\n modelProviderExtensionPoint,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { createDataIngestionPipeline } from './services/ingestor';\nimport { createChatService } from './services/chat';\nimport {
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './services/router';\nimport {\n dataIngestorExtensionPoint,\n EmbeddingsProvider,\n embeddingsProviderExtensionPoint,\n Ingestor,\n Model,\n modelProviderExtensionPoint,\n Tool,\n toolExtensionPoint,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { createDataIngestionPipeline } from './services/ingestor';\nimport { createChatService } from './services/chat';\nimport { applyDatabaseMigrations } from './database/migrations';\nimport { PgVectorStore } from './database';\nimport { signalsServiceRef } from '@backstage/plugin-signals-node';\nimport { createSearchKnowledgeTool } from './services/tools/searchKnowledge';\n\n/**\n * aiAssistantPlugin backend plugin\n *\n * @public\n */\n\nexport const aiAssistantPlugin = createBackendPlugin({\n pluginId: 'ai-assistant',\n register(env) {\n const ingestors: Ingestor[] = [];\n const models: Model[] = [];\n const tools: Tool[] = [];\n\n let embeddingsProvider: EmbeddingsProvider;\n\n env.registerExtensionPoint(dataIngestorExtensionPoint, {\n registerIngestor: ingestor => {\n const existingIngestor = ingestors.find(i => i.id === ingestor.id);\n if (existingIngestor) {\n throw new Error(\n `Ingestor with id ${ingestor.id} is already registered.`,\n );\n }\n ingestors.push(ingestor);\n },\n });\n\n env.registerExtensionPoint(embeddingsProviderExtensionPoint, {\n register: provider => {\n embeddingsProvider = provider;\n },\n });\n\n env.registerExtensionPoint(modelProviderExtensionPoint, {\n register: model => {\n const existingModel = models.find(m => m.id === model.id);\n if (existingModel) {\n throw new Error(`Model with id ${model.id} is already registered.`);\n }\n models.push(model);\n },\n });\n\n env.registerExtensionPoint(toolExtensionPoint, {\n register: tool => {\n const existingTool = tools.find(t => t.name === tool.name);\n if (existingTool) {\n throw new Error(`Tool with name ${tool.name} is already registered.`);\n }\n tools.push(tool);\n },\n });\n\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n database: coreServices.database,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n scheduler: coreServices.scheduler,\n httpAuth: coreServices.httpAuth,\n userInfo: coreServices.userInfo,\n signals: signalsServiceRef,\n },\n\n async init(options) {\n const { httpRouter, database } = options;\n const client = await database.getClient();\n\n await applyDatabaseMigrations(client);\n\n const vectorStore = await PgVectorStore.fromConfig(options);\n\n if (!embeddingsProvider) {\n throw new Error('No Embeddings Provider was registered.');\n }\n\n vectorStore.connectEmbeddings(await embeddingsProvider.getEmbeddings());\n\n const dataIngestionPipeline = createDataIngestionPipeline({\n ...options,\n vectorStore,\n ingestors,\n });\n\n const searchKnowledgeTool = createSearchKnowledgeTool({ vectorStore });\n tools.push(searchKnowledgeTool);\n\n const chat = await createChatService({\n ...options,\n models,\n tools,\n });\n\n httpRouter.use(await createRouter({ ...options, chat }));\n dataIngestionPipeline.start();\n },\n });\n },\n});\n"],"names":["createBackendPlugin","dataIngestorExtensionPoint","embeddingsProviderExtensionPoint","modelProviderExtensionPoint","toolExtensionPoint","coreServices","signalsServiceRef","applyDatabaseMigrations","PgVectorStore","createDataIngestionPipeline","createSearchKnowledgeTool","chat","createChatService","createRouter"],"mappings":"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"}
|
|
@@ -1,98 +1,84 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var uuid = require('uuid');
|
|
4
3
|
var chatStore = require('../database/chat-store.cjs.js');
|
|
5
4
|
var prompts = require('../constants/prompts.cjs.js');
|
|
5
|
+
var tools = require('@langchain/core/tools');
|
|
6
|
+
var prebuilt = require('@langchain/langgraph/prebuilt');
|
|
7
|
+
var prompts$1 = require('@langchain/core/prompts');
|
|
8
|
+
var summarizer = require('./summarizer.cjs.js');
|
|
9
|
+
var uuid = require('uuid');
|
|
6
10
|
|
|
7
11
|
const createChatService = async ({
|
|
8
12
|
models,
|
|
13
|
+
tools: tools$1,
|
|
9
14
|
logger,
|
|
10
|
-
vectorStore,
|
|
11
|
-
promptBuilder,
|
|
12
15
|
database,
|
|
13
16
|
signals,
|
|
14
17
|
config
|
|
15
18
|
}) => {
|
|
16
19
|
logger.info(`Available models: ${models.map((m) => m.id).join(", ")}`);
|
|
20
|
+
const system = config.getOptionalString("aiAssistant.prompt.system") || prompts.DEFAULT_SYSTEM_PROMPT;
|
|
21
|
+
const toolGuideline = config.getOptionalString("aiAssistant.prompt.toolGuideline") || prompts.DEFAULT_TOOL_GUIDELINE;
|
|
17
22
|
const chatStore$1 = await chatStore.ChatStore.fromConfig({ database });
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
const summarizer$1 = await summarizer.createSummarizerService({ config, models });
|
|
24
|
+
const agentTools = tools$1.map((tool) => new tools.DynamicStructuredTool(tool));
|
|
25
|
+
const systemPromptTemplate = prompts$1.SystemMessagePromptTemplate.fromTemplate(`
|
|
26
|
+
PURPOSE:
|
|
27
|
+
{basePrompt}
|
|
28
|
+
|
|
29
|
+
TOOL USAGE GUIDELINES:
|
|
30
|
+
{toolGuideline}
|
|
31
|
+
|
|
32
|
+
Available tools:
|
|
33
|
+
{toolList}
|
|
34
|
+
|
|
35
|
+
Context:
|
|
36
|
+
{context}`);
|
|
37
|
+
const addMessages = async (messages, userRef, conversationId, recentConversationMessages) => {
|
|
38
|
+
const recentMessages = recentConversationMessages || await chatStore$1.getChatMessages(conversationId, userRef, 5, ["tool"]);
|
|
39
|
+
const conversationSize = (recentMessages?.length ?? 0) + messages.length;
|
|
40
|
+
if (recentMessages.length === 0) {
|
|
41
|
+
const conversation2 = {
|
|
42
|
+
id: conversationId,
|
|
43
|
+
title: "New Conversation",
|
|
44
|
+
userRef
|
|
45
|
+
};
|
|
46
|
+
chatStore$1.createConversation(conversation2);
|
|
47
|
+
chatStore$1.addChatMessage(messages, userRef, conversationId);
|
|
40
48
|
signals.publish({
|
|
41
|
-
channel: `ai-assistant.chat.
|
|
42
|
-
message:
|
|
49
|
+
channel: `ai-assistant.chat.conversation-details-update`,
|
|
50
|
+
message: { conversation: conversation2 },
|
|
43
51
|
recipients: {
|
|
44
52
|
type: "user",
|
|
45
|
-
entityRef:
|
|
53
|
+
entityRef: userRef
|
|
46
54
|
}
|
|
47
55
|
});
|
|
56
|
+
return;
|
|
48
57
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const conversationSize = await chatStore$1.getConversationSize(
|
|
52
|
-
conversationId
|
|
53
|
-
);
|
|
54
|
-
if (conversationSize < 1) {
|
|
55
|
-
await chatStore$1.createConversation({
|
|
56
|
-
id: conversationId,
|
|
57
|
-
userRef,
|
|
58
|
-
title: "New Conversation"
|
|
59
|
-
});
|
|
60
|
-
await chatStore$1.addChatMessage(messages, userRef, conversationId);
|
|
58
|
+
if (conversationSize < 5) {
|
|
59
|
+
chatStore$1.addChatMessage(messages, userRef, conversationId);
|
|
61
60
|
return;
|
|
62
61
|
}
|
|
63
62
|
const conversation = await chatStore$1.getConversation(
|
|
64
63
|
conversationId,
|
|
65
64
|
userRef
|
|
66
65
|
);
|
|
67
|
-
if (conversationSize + messages.length < 5) {
|
|
68
|
-
await chatStore$1.addChatMessage(messages, userRef, conversationId);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
66
|
if (conversation.title !== "New Conversation") {
|
|
72
|
-
|
|
67
|
+
chatStore$1.addChatMessage(messages, userRef, conversationId);
|
|
73
68
|
return;
|
|
74
69
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const summaryPrompt = config.getOptionalString("aiAssistant.conversation.summaryPrompt") ?? prompts.DEFAULT_SUMMARY_PROMPT;
|
|
86
|
-
const { text } = await summaryModel.invoke([
|
|
87
|
-
...conversationMessages,
|
|
88
|
-
{
|
|
89
|
-
role: "system",
|
|
90
|
-
content: summaryPrompt
|
|
70
|
+
const summary = await summarizer$1.summarize(recentMessages, "25 characters");
|
|
71
|
+
conversation.title = summary;
|
|
72
|
+
chatStore$1.updateConversation(conversation);
|
|
73
|
+
chatStore$1.addChatMessage(messages, userRef, conversationId);
|
|
74
|
+
signals.publish({
|
|
75
|
+
channel: `ai-assistant.chat.conversation-details-update`,
|
|
76
|
+
message: { conversation },
|
|
77
|
+
recipients: {
|
|
78
|
+
type: "user",
|
|
79
|
+
entityRef: userRef
|
|
91
80
|
}
|
|
92
|
-
|
|
93
|
-
conversation.title = text.trim();
|
|
94
|
-
await chatStore$1.updateConversation(conversation);
|
|
95
|
-
await chatStore$1.addChatMessage(messages, userRef, conversationId);
|
|
81
|
+
});
|
|
96
82
|
};
|
|
97
83
|
const prompt = async ({
|
|
98
84
|
conversationId,
|
|
@@ -101,43 +87,91 @@ const createChatService = async ({
|
|
|
101
87
|
stream = true,
|
|
102
88
|
userEntityRef
|
|
103
89
|
}) => {
|
|
104
|
-
const model =
|
|
90
|
+
const model = models.find((m) => m.id === modelId)?.chatModel;
|
|
105
91
|
if (!model) {
|
|
106
92
|
throw new Error(`Model with id ${modelId} not found`);
|
|
107
93
|
}
|
|
108
|
-
await addChatMessage(messages, userEntityRef, conversationId);
|
|
109
|
-
const context = await vectorStore.similaritySearch(
|
|
110
|
-
messages.filter((m) => m.role === "user").map((m) => m.content).join("\n")
|
|
111
|
-
);
|
|
112
|
-
const recentConversationMessages = await chatStore$1.getChatMessages(
|
|
113
|
-
conversationId,
|
|
114
|
-
userEntityRef,
|
|
115
|
-
10
|
|
116
|
-
);
|
|
117
|
-
const promptMessages = promptBuilder.buildPrompt(
|
|
118
|
-
[...recentConversationMessages, ...messages],
|
|
119
|
-
context
|
|
120
|
-
);
|
|
121
|
-
const messageId = uuid.v4();
|
|
122
94
|
const aiMessage = {
|
|
123
|
-
id:
|
|
124
|
-
role: "
|
|
125
|
-
content: ""
|
|
95
|
+
id: uuid.v4(),
|
|
96
|
+
role: "ai",
|
|
97
|
+
content: "",
|
|
98
|
+
metadata: {}
|
|
126
99
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
100
|
+
const streamFn = async () => {
|
|
101
|
+
const recentConversationMessages = await chatStore$1.getChatMessages(
|
|
102
|
+
conversationId,
|
|
103
|
+
userEntityRef,
|
|
104
|
+
10,
|
|
105
|
+
["tool"]
|
|
106
|
+
);
|
|
107
|
+
const systemPrompt = await systemPromptTemplate.formatMessages({
|
|
108
|
+
basePrompt: system,
|
|
109
|
+
toolGuideline,
|
|
110
|
+
toolList: agentTools.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n"),
|
|
111
|
+
context: `none`
|
|
134
112
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
113
|
+
const agent = prebuilt.createReactAgent({
|
|
114
|
+
llm: model,
|
|
115
|
+
tools: agentTools,
|
|
116
|
+
prompt: systemPrompt[0].text
|
|
117
|
+
});
|
|
118
|
+
const promptStream = await agent.stream(
|
|
119
|
+
{
|
|
120
|
+
messages: [...recentConversationMessages, ...messages]
|
|
121
|
+
},
|
|
122
|
+
{ streamMode: "messages" }
|
|
123
|
+
);
|
|
124
|
+
const responseMessages = [];
|
|
125
|
+
let firstMessageId;
|
|
126
|
+
for await (const [
|
|
127
|
+
message,
|
|
128
|
+
{ __pregel_task_id: messageId }
|
|
129
|
+
] of promptStream) {
|
|
130
|
+
if (!firstMessageId) {
|
|
131
|
+
firstMessageId = messageId;
|
|
132
|
+
}
|
|
133
|
+
const id = messageId === firstMessageId ? aiMessage.id : messageId;
|
|
134
|
+
const index = responseMessages.findIndex((m) => m.id === id);
|
|
135
|
+
const role = message.getType();
|
|
136
|
+
if (index === -1) {
|
|
137
|
+
const content = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
138
|
+
const toolName = role === "tool" ? message.name : void 0;
|
|
139
|
+
const metadata = {};
|
|
140
|
+
if (toolName) {
|
|
141
|
+
metadata.name = toolName;
|
|
142
|
+
}
|
|
143
|
+
responseMessages.push({
|
|
144
|
+
role,
|
|
145
|
+
content,
|
|
146
|
+
id,
|
|
147
|
+
metadata
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (index !== -1) {
|
|
151
|
+
responseMessages[index] = {
|
|
152
|
+
...responseMessages[index],
|
|
153
|
+
content: responseMessages[index].content += message.content
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
signals.publish({
|
|
157
|
+
channel: `ai-assistant.chat.conversation-stream:${conversationId}`,
|
|
158
|
+
message: { messages: responseMessages.filter((m) => m.content) },
|
|
159
|
+
recipients: {
|
|
160
|
+
type: "user",
|
|
161
|
+
entityRef: userEntityRef
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
addMessages(
|
|
166
|
+
[...messages, ...responseMessages],
|
|
167
|
+
userEntityRef,
|
|
168
|
+
conversationId,
|
|
169
|
+
recentConversationMessages
|
|
170
|
+
);
|
|
171
|
+
return responseMessages;
|
|
172
|
+
};
|
|
173
|
+
const result = streamFn();
|
|
174
|
+
return stream ? [aiMessage] : result;
|
|
141
175
|
};
|
|
142
176
|
const getAvailableModels = async () => {
|
|
143
177
|
return models.map((x) => x.id);
|
|
@@ -160,7 +194,8 @@ const createChatService = async ({
|
|
|
160
194
|
prompt,
|
|
161
195
|
getAvailableModels,
|
|
162
196
|
getConversation,
|
|
163
|
-
getConversations
|
|
197
|
+
getConversations,
|
|
198
|
+
addMessages
|
|
164
199
|
};
|
|
165
200
|
};
|
|
166
201
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat.cjs.js","sources":["../../src/services/chat.ts"],"sourcesContent":["import {\n Model,\n VectorStore,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport {\n LoggerService,\n RootConfigService,\n DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport { PromptBuilder } from './prompt';\nimport { v4 as uuid } from 'uuid';\nimport { ChatStore } from '../database/chat-store';\nimport {\n Conversation,\n Message,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { SignalsService } from '@backstage/plugin-signals-node';\nimport { DEFAULT_SUMMARY_PROMPT } from '../constants/prompts';\n\nexport type ChatServiceOptions = {\n models: Model[];\n logger: LoggerService;\n vectorStore: VectorStore;\n config: RootConfigService;\n promptBuilder: PromptBuilder;\n database: DatabaseService;\n signals: SignalsService;\n};\n\ntype StreamOptions = {\n modelId: string;\n messages: Message[];\n messageId: string;\n userEntityRef: string;\n};\n\ntype PromptOptions = {\n modelId: string;\n messages: Message[];\n conversationId: string;\n stream?: boolean;\n userEntityRef: string;\n};\n\ntype GetConversationOptions = {\n conversationId: string;\n userEntityRef: string;\n};\n\ntype GetConversationsOptions = {\n userEntityRef: string;\n};\n\nexport type ChatService = {\n prompt: (options: PromptOptions) => Promise<Required<Message>[]>;\n getAvailableModels: () => Promise<string[]>;\n getConversation: (\n options: GetConversationOptions,\n ) => Promise<Required<Message>[]>;\n getConversations: (\n options: GetConversationsOptions,\n ) => Promise<Conversation[]>;\n};\n\nexport const createChatService = async ({\n models,\n logger,\n vectorStore,\n promptBuilder,\n database,\n signals,\n config,\n}: ChatServiceOptions): Promise<ChatService> => {\n logger.info(`Available models: ${models.map(m => m.id).join(', ')}`);\n\n const chatStore = await ChatStore.fromConfig({ database });\n\n const getChatModelById = (id: string) => {\n return models.find(model => model.id === id)?.chatModel;\n };\n\n const streamMessage = async ({\n modelId,\n messages,\n messageId,\n userEntityRef,\n }: StreamOptions) => {\n const model = getChatModelById(modelId);\n\n if (!model) {\n throw new Error(`Model with id ${modelId} not found`);\n }\n\n const promptStream = await model.stream(messages);\n\n const aiMessage: Required<Message> = {\n id: messageId,\n role: 'assistant',\n content: '',\n };\n\n for await (const chunk of promptStream) {\n aiMessage.content += chunk.content ?? '';\n\n chatStore.updateMessage(aiMessage);\n\n signals.publish({\n channel: `ai-assistant.chat.message-stream:${messageId}`,\n message: aiMessage,\n recipients: {\n type: 'user',\n entityRef: userEntityRef,\n },\n });\n }\n };\n\n const addChatMessage: (typeof chatStore)['addChatMessage'] = async (\n messages,\n userRef,\n conversationId,\n ) => {\n const conversationSize = await chatStore.getConversationSize(\n conversationId,\n );\n\n if (conversationSize < 1) {\n await chatStore.createConversation({\n id: conversationId,\n userRef,\n title: 'New Conversation',\n });\n\n await chatStore.addChatMessage(messages, userRef, conversationId);\n return;\n }\n\n const conversation = await chatStore.getConversation(\n conversationId,\n userRef,\n );\n\n if (conversationSize + messages.length < 5) {\n await chatStore.addChatMessage(messages, userRef, conversationId);\n return;\n }\n\n if (conversation.title !== 'New Conversation') {\n await chatStore.addChatMessage(messages, userRef, conversationId);\n return;\n }\n\n const summaryModelId =\n config.getOptionalString('aiAssistant.conversation.summaryModel') ??\n models[0].id;\n const summaryModel = getChatModelById(summaryModelId);\n\n if (!summaryModel) {\n throw new Error(`Model with id ${summaryModelId} not found`);\n }\n\n const conversationMessages = await chatStore.getChatMessages(\n conversationId,\n userRef,\n 5,\n );\n\n const summaryPrompt =\n config.getOptionalString('aiAssistant.conversation.summaryPrompt') ??\n DEFAULT_SUMMARY_PROMPT;\n\n const { text } = await summaryModel.invoke([\n ...conversationMessages,\n {\n role: 'system',\n content: summaryPrompt,\n },\n ]);\n\n conversation.title = text.trim();\n\n await chatStore.updateConversation(conversation);\n await chatStore.addChatMessage(messages, userRef, conversationId);\n };\n\n const prompt: ChatService['prompt'] = async ({\n conversationId,\n messages,\n modelId,\n stream = true,\n userEntityRef,\n }: PromptOptions) => {\n const model = getChatModelById(modelId);\n\n if (!model) {\n throw new Error(`Model with id ${modelId} not found`);\n }\n\n await addChatMessage(messages, userEntityRef, conversationId);\n\n const context = await vectorStore.similaritySearch(\n messages\n .filter(m => m.role === 'user')\n .map(m => m.content)\n .join('\\n'),\n );\n\n const recentConversationMessages = await chatStore.getChatMessages(\n conversationId,\n userEntityRef,\n 10,\n );\n\n const promptMessages = promptBuilder.buildPrompt(\n [...recentConversationMessages, ...messages],\n context,\n );\n\n const messageId: string = uuid();\n\n const aiMessage: Required<Message> = {\n id: messageId,\n role: 'assistant',\n content: '',\n };\n\n await addChatMessage([aiMessage], userEntityRef, conversationId);\n\n if (stream) {\n streamMessage({\n modelId,\n messages: promptMessages,\n messageId,\n userEntityRef,\n });\n\n return [aiMessage];\n }\n const { text } = await model.invoke(promptMessages);\n\n aiMessage.content = text;\n\n await chatStore.updateMessage(aiMessage);\n\n return [aiMessage];\n };\n\n const getAvailableModels: ChatService['getAvailableModels'] = async () => {\n return models.map(x => x.id);\n };\n\n const getConversation: ChatService['getConversation'] = async (\n options: GetConversationOptions,\n ) => {\n const { conversationId, userEntityRef } = options;\n\n const conversation = await chatStore.getChatMessages(\n conversationId,\n userEntityRef,\n );\n\n return conversation;\n };\n\n const getConversations: ChatService['getConversations'] = async ({\n userEntityRef,\n }: GetConversationsOptions) => {\n const conversations = await chatStore.getConversations(userEntityRef);\n\n return conversations;\n };\n return {\n prompt,\n getAvailableModels,\n getConversation,\n getConversations,\n };\n};\n"],"names":["chatStore","ChatStore","DEFAULT_SUMMARY_PROMPT","uuid"],"mappings":";;;;;;AAgEO,MAAM,oBAAoB,OAAO;AAAA,EACtC,MAAA;AAAA,EACA,MAAA;AAAA,EACA,WAAA;AAAA,EACA,aAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,KAAgD;AAC9C,EAAA,MAAA,CAAO,IAAA,CAAK,CAAA,kBAAA,EAAqB,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,EAAE,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAE,CAAA;AAEnE,EAAA,MAAMA,cAAY,MAAMC,mBAAA,CAAU,UAAA,CAAW,EAAE,UAAU,CAAA;AAEzD,EAAA,MAAM,gBAAA,GAAmB,CAAC,EAAA,KAAe;AACvC,IAAA,OAAO,OAAO,IAAA,CAAK,CAAA,KAAA,KAAS,KAAA,CAAM,EAAA,KAAO,EAAE,CAAA,EAAG,SAAA;AAAA,EAChD,CAAA;AAEA,EAAA,MAAM,gBAAgB,OAAO;AAAA,IAC3B,OAAA;AAAA,IACA,QAAA;AAAA,IACA,SAAA;AAAA,IACA;AAAA,GACF,KAAqB;AACnB,IAAA,MAAM,KAAA,GAAQ,iBAAiB,OAAO,CAAA;AAEtC,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,cAAA,EAAiB,OAAO,CAAA,UAAA,CAAY,CAAA;AAAA,IACtD;AAEA,IAAA,MAAM,YAAA,GAAe,MAAM,KAAA,CAAM,MAAA,CAAO,QAAQ,CAAA;AAEhD,IAAA,MAAM,SAAA,GAA+B;AAAA,MACnC,EAAA,EAAI,SAAA;AAAA,MACJ,IAAA,EAAM,WAAA;AAAA,MACN,OAAA,EAAS;AAAA,KACX;AAEA,IAAA,WAAA,MAAiB,SAAS,YAAA,EAAc;AACtC,MAAA,SAAA,CAAU,OAAA,IAAW,MAAM,OAAA,IAAW,EAAA;AAEtC,MAAAD,WAAA,CAAU,cAAc,SAAS,CAAA;AAEjC,MAAA,OAAA,CAAQ,OAAA,CAAQ;AAAA,QACd,OAAA,EAAS,oCAAoC,SAAS,CAAA,CAAA;AAAA,QACtD,OAAA,EAAS,SAAA;AAAA,QACT,UAAA,EAAY;AAAA,UACV,IAAA,EAAM,MAAA;AAAA,UACN,SAAA,EAAW;AAAA;AACb,OACD,CAAA;AAAA,IACH;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,cAAA,GAAuD,OAC3D,QAAA,EACA,OAAA,EACA,cAAA,KACG;AACH,IAAA,MAAM,gBAAA,GAAmB,MAAMA,WAAA,CAAU,mBAAA;AAAA,MACvC;AAAA,KACF;AAEA,IAAA,IAAI,mBAAmB,CAAA,EAAG;AACxB,MAAA,MAAMA,YAAU,kBAAA,CAAmB;AAAA,QACjC,EAAA,EAAI,cAAA;AAAA,QACJ,OAAA;AAAA,QACA,KAAA,EAAO;AAAA,OACR,CAAA;AAED,MAAA,MAAMA,WAAA,CAAU,cAAA,CAAe,QAAA,EAAU,OAAA,EAAS,cAAc,CAAA;AAChE,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,YAAA,GAAe,MAAMA,WAAA,CAAU,eAAA;AAAA,MACnC,cAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,IAAI,gBAAA,GAAmB,QAAA,CAAS,MAAA,GAAS,CAAA,EAAG;AAC1C,MAAA,MAAMA,WAAA,CAAU,cAAA,CAAe,QAAA,EAAU,OAAA,EAAS,cAAc,CAAA;AAChE,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,YAAA,CAAa,UAAU,kBAAA,EAAoB;AAC7C,MAAA,MAAMA,WAAA,CAAU,cAAA,CAAe,QAAA,EAAU,OAAA,EAAS,cAAc,CAAA;AAChE,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,iBACJ,MAAA,CAAO,iBAAA,CAAkB,uCAAuC,CAAA,IAChE,MAAA,CAAO,CAAC,CAAA,CAAE,EAAA;AACZ,IAAA,MAAM,YAAA,GAAe,iBAAiB,cAAc,CAAA;AAEpD,IAAA,IAAI,CAAC,YAAA,EAAc;AACjB,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,cAAA,EAAiB,cAAc,CAAA,UAAA,CAAY,CAAA;AAAA,IAC7D;AAEA,IAAA,MAAM,oBAAA,GAAuB,MAAMA,WAAA,CAAU,eAAA;AAAA,MAC3C,cAAA;AAAA,MACA,OAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,MAAM,aAAA,GACJ,MAAA,CAAO,iBAAA,CAAkB,wCAAwC,CAAA,IACjEE,8BAAA;AAEF,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,aAAa,MAAA,CAAO;AAAA,MACzC,GAAG,oBAAA;AAAA,MACH;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,OAAA,EAAS;AAAA;AACX,KACD,CAAA;AAED,IAAA,YAAA,CAAa,KAAA,GAAQ,KAAK,IAAA,EAAK;AAE/B,IAAA,MAAMF,WAAA,CAAU,mBAAmB,YAAY,CAAA;AAC/C,IAAA,MAAMA,WAAA,CAAU,cAAA,CAAe,QAAA,EAAU,OAAA,EAAS,cAAc,CAAA;AAAA,EAClE,CAAA;AAEA,EAAA,MAAM,SAAgC,OAAO;AAAA,IAC3C,cAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAA,GAAS,IAAA;AAAA,IACT;AAAA,GACF,KAAqB;AACnB,IAAA,MAAM,KAAA,GAAQ,iBAAiB,OAAO,CAAA;AAEtC,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,cAAA,EAAiB,OAAO,CAAA,UAAA,CAAY,CAAA;AAAA,IACtD;AAEA,IAAA,MAAM,cAAA,CAAe,QAAA,EAAU,aAAA,EAAe,cAAc,CAAA;AAE5D,IAAA,MAAM,OAAA,GAAU,MAAM,WAAA,CAAY,gBAAA;AAAA,MAChC,QAAA,CACG,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,IAAA,KAAS,MAAM,CAAA,CAC7B,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,CAAA,CAClB,KAAK,IAAI;AAAA,KACd;AAEA,IAAA,MAAM,0BAAA,GAA6B,MAAMA,WAAA,CAAU,eAAA;AAAA,MACjD,cAAA;AAAA,MACA,aAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,MAAM,iBAAiB,aAAA,CAAc,WAAA;AAAA,MACnC,CAAC,GAAG,0BAAA,EAA4B,GAAG,QAAQ,CAAA;AAAA,MAC3C;AAAA,KACF;AAEA,IAAA,MAAM,YAAoBG,OAAA,EAAK;AAE/B,IAAA,MAAM,SAAA,GAA+B;AAAA,MACnC,EAAA,EAAI,SAAA;AAAA,MACJ,IAAA,EAAM,WAAA;AAAA,MACN,OAAA,EAAS;AAAA,KACX;AAEA,IAAA,MAAM,cAAA,CAAe,CAAC,SAAS,CAAA,EAAG,eAAe,cAAc,CAAA;AAE/D,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,aAAA,CAAc;AAAA,QACZ,OAAA;AAAA,QACA,QAAA,EAAU,cAAA;AAAA,QACV,SAAA;AAAA,QACA;AAAA,OACD,CAAA;AAED,MAAA,OAAO,CAAC,SAAS,CAAA;AAAA,IACnB;AACA,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,KAAA,CAAM,OAAO,cAAc,CAAA;AAElD,IAAA,SAAA,CAAU,OAAA,GAAU,IAAA;AAEpB,IAAA,MAAMH,WAAA,CAAU,cAAc,SAAS,CAAA;AAEvC,IAAA,OAAO,CAAC,SAAS,CAAA;AAAA,EACnB,CAAA;AAEA,EAAA,MAAM,qBAAwD,YAAY;AACxE,IAAA,OAAO,MAAA,CAAO,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,EAAE,CAAA;AAAA,EAC7B,CAAA;AAEA,EAAA,MAAM,eAAA,GAAkD,OACtD,OAAA,KACG;AACH,IAAA,MAAM,EAAE,cAAA,EAAgB,aAAA,EAAc,GAAI,OAAA;AAE1C,IAAA,MAAM,YAAA,GAAe,MAAMA,WAAA,CAAU,eAAA;AAAA,MACnC,cAAA;AAAA,MACA;AAAA,KACF;AAEA,IAAA,OAAO,YAAA;AAAA,EACT,CAAA;AAEA,EAAA,MAAM,mBAAoD,OAAO;AAAA,IAC/D;AAAA,GACF,KAA+B;AAC7B,IAAA,MAAM,aAAA,GAAgB,MAAMA,WAAA,CAAU,gBAAA,CAAiB,aAAa,CAAA;AAEpE,IAAA,OAAO,aAAA;AAAA,EACT,CAAA;AACA,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,kBAAA;AAAA,IACA,eAAA;AAAA,IACA;AAAA,GACF;AACF;;;;"}
|
|
1
|
+
{"version":3,"file":"chat.cjs.js","sources":["../../src/services/chat.ts"],"sourcesContent":["import { Model } from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport {\n LoggerService,\n RootConfigService,\n DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport { ChatStore } from '../database/chat-store';\nimport {\n Conversation,\n Message,\n JsonObject,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { SignalsService } from '@backstage/plugin-signals-node';\nimport {\n DEFAULT_SYSTEM_PROMPT,\n DEFAULT_TOOL_GUIDELINE,\n} from '../constants/prompts';\nimport { Tool } from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { DynamicStructuredTool } from '@langchain/core/tools';\nimport { createReactAgent } from '@langchain/langgraph/prebuilt';\nimport { SystemMessagePromptTemplate } from '@langchain/core/prompts';\nimport { createSummarizerService } from './summarizer';\nimport { v4 as uuid } from 'uuid';\nimport { ToolMessage } from '@langchain/core/messages';\n\nexport type ChatServiceOptions = {\n models: Model[];\n tools: Tool[];\n logger: LoggerService;\n config: RootConfigService;\n database: DatabaseService;\n signals: SignalsService;\n};\n\ntype PromptOptions = {\n modelId: string;\n messages: Message[];\n conversationId: string;\n stream?: boolean;\n userEntityRef: string;\n};\n\ntype GetConversationOptions = {\n conversationId: string;\n userEntityRef: string;\n};\n\ntype GetConversationsOptions = {\n userEntityRef: string;\n};\n\nexport type ChatService = {\n prompt: (options: PromptOptions) => Promise<Required<Message>[]>;\n getAvailableModels: () => Promise<string[]>;\n getConversation: (\n options: GetConversationOptions,\n ) => Promise<Required<Message>[]>;\n getConversations: (\n options: GetConversationsOptions,\n ) => Promise<Conversation[]>;\n addMessages: (\n messages: Message[],\n userRef: string,\n conversationId: string,\n recentConversationMessages?: Message[],\n ) => Promise<void>;\n};\n\nexport const createChatService = async ({\n models,\n tools,\n logger,\n database,\n signals,\n config,\n}: ChatServiceOptions): Promise<ChatService> => {\n logger.info(`Available models: ${models.map(m => m.id).join(', ')}`);\n\n const system =\n config.getOptionalString('aiAssistant.prompt.system') ||\n DEFAULT_SYSTEM_PROMPT;\n\n const toolGuideline =\n config.getOptionalString('aiAssistant.prompt.toolGuideline') ||\n DEFAULT_TOOL_GUIDELINE;\n\n const chatStore = await ChatStore.fromConfig({ database });\n const summarizer = await createSummarizerService({ config, models });\n\n const agentTools = tools.map(tool => new DynamicStructuredTool(tool));\n\n const systemPromptTemplate = SystemMessagePromptTemplate.fromTemplate(`\n PURPOSE:\n {basePrompt}\n\n TOOL USAGE GUIDELINES:\n {toolGuideline}\n\n Available tools:\n {toolList}\n\n Context:\n {context}`);\n\n const addMessages: ChatService['addMessages'] = async (\n messages,\n userRef,\n conversationId,\n recentConversationMessages,\n ) => {\n // If we have recentConversationMessages, use them; otherwise, fetch the last 5 messages\n const recentMessages =\n recentConversationMessages ||\n (await chatStore.getChatMessages(conversationId, userRef, 5, ['tool']));\n\n const conversationSize = (recentMessages?.length ?? 0) + messages.length;\n\n if (recentMessages.length === 0) {\n const conversation: Conversation = {\n id: conversationId,\n title: 'New Conversation',\n userRef,\n };\n chatStore.createConversation(conversation);\n chatStore.addChatMessage(messages, userRef, conversationId);\n\n signals.publish({\n channel: `ai-assistant.chat.conversation-details-update`,\n message: { conversation },\n recipients: {\n type: 'user',\n entityRef: userRef,\n },\n });\n return;\n }\n\n if (conversationSize < 5) {\n chatStore.addChatMessage(messages, userRef, conversationId);\n return;\n }\n\n const conversation = await chatStore.getConversation(\n conversationId,\n userRef,\n );\n\n if (conversation.title !== 'New Conversation') {\n chatStore.addChatMessage(messages, userRef, conversationId);\n return;\n }\n\n const summary = await summarizer.summarize(recentMessages, '25 characters');\n\n conversation.title = summary;\n\n chatStore.updateConversation(conversation);\n chatStore.addChatMessage(messages, userRef, conversationId);\n\n signals.publish({\n channel: `ai-assistant.chat.conversation-details-update`,\n message: { conversation },\n recipients: {\n type: 'user',\n entityRef: userRef,\n },\n });\n };\n\n const prompt: ChatService['prompt'] = async ({\n conversationId,\n messages,\n modelId,\n stream = true,\n userEntityRef,\n }: PromptOptions) => {\n const model = models.find(m => m.id === modelId)?.chatModel;\n\n if (!model) {\n throw new Error(`Model with id ${modelId} not found`);\n }\n\n const aiMessage: Required<Message> = {\n id: uuid(),\n role: 'ai',\n content: '',\n metadata: {},\n };\n\n const streamFn = async () => {\n const recentConversationMessages = await chatStore.getChatMessages(\n conversationId,\n userEntityRef,\n 10,\n ['tool'],\n );\n\n const systemPrompt = await systemPromptTemplate.formatMessages({\n basePrompt: system,\n toolGuideline,\n toolList: agentTools\n .map(tool => `- ${tool.name}: ${tool.description}`)\n .join('\\n'),\n context: `none`,\n });\n\n const agent = createReactAgent({\n llm: model,\n tools: agentTools,\n prompt: systemPrompt[0].text,\n });\n\n const promptStream = await agent.stream(\n {\n messages: [...recentConversationMessages, ...messages],\n },\n { streamMode: 'messages' },\n );\n\n const responseMessages: Required<Message>[] = [];\n let firstMessageId: string | undefined;\n\n for await (const [\n message,\n { __pregel_task_id: messageId },\n ] of promptStream) {\n if (!firstMessageId) {\n firstMessageId = messageId;\n }\n\n const id = messageId === firstMessageId ? aiMessage.id : messageId;\n\n const index = responseMessages.findIndex(m => m.id === id);\n const role = message.getType();\n\n if (index === -1) {\n const content =\n typeof message.content === 'string'\n ? message.content\n : JSON.stringify(message.content);\n\n const toolName =\n role === 'tool' ? (message as ToolMessage).name : undefined;\n const metadata: JsonObject = {};\n\n if (toolName) {\n metadata.name = toolName;\n }\n\n responseMessages.push({\n role,\n content,\n id,\n metadata,\n });\n }\n\n if (index !== -1) {\n responseMessages[index] = {\n ...responseMessages[index],\n content: (responseMessages[index].content += message.content),\n };\n }\n\n signals.publish({\n channel: `ai-assistant.chat.conversation-stream:${conversationId}`,\n message: { messages: responseMessages.filter(m => m.content) },\n recipients: {\n type: 'user',\n entityRef: userEntityRef,\n },\n });\n }\n\n addMessages(\n [...messages, ...responseMessages],\n userEntityRef,\n conversationId,\n recentConversationMessages,\n );\n\n return responseMessages;\n };\n\n const result = streamFn();\n\n return stream ? [aiMessage] : result;\n };\n\n const getAvailableModels: ChatService['getAvailableModels'] = async () => {\n return models.map(x => x.id);\n };\n\n const getConversation: ChatService['getConversation'] = async (\n options: GetConversationOptions,\n ) => {\n const { conversationId, userEntityRef } = options;\n\n const conversation = await chatStore.getChatMessages(\n conversationId,\n userEntityRef,\n );\n\n return conversation;\n };\n\n const getConversations: ChatService['getConversations'] = async ({\n userEntityRef,\n }: GetConversationsOptions) => {\n const conversations = await chatStore.getConversations(userEntityRef);\n\n return conversations;\n };\n return {\n prompt,\n getAvailableModels,\n getConversation,\n getConversations,\n addMessages,\n };\n};\n"],"names":["tools","DEFAULT_SYSTEM_PROMPT","DEFAULT_TOOL_GUIDELINE","chatStore","ChatStore","summarizer","createSummarizerService","DynamicStructuredTool","SystemMessagePromptTemplate","conversation","uuid","createReactAgent"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.cjs.js","sources":["../../../../src/services/router/middleware/validation.ts"],"sourcesContent":["import express from 'express';\nimport {
|
|
1
|
+
{"version":3,"file":"validation.cjs.js","sources":["../../../../src/services/router/middleware/validation.ts"],"sourcesContent":["import express from 'express';\nimport { ZodObject } from 'zod';\n\ntype ValidationKey = 'body' | 'query' | 'params' | 'headers';\n\nexport const validation = (schema: ZodObject, key: ValidationKey) => {\n return (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n const parsed = schema.safeParse(req[key]);\n if (!parsed.success) {\n const errors = parsed.error.issues.map(\n issue =>\n `Validation Error:Field ${issue.path.join('.')} - ${issue.message}`,\n );\n res.status(400).send({ errors });\n return;\n }\n req[key] = parsed.data;\n next();\n };\n};\n"],"names":[],"mappings":";;AAKO,MAAM,UAAA,GAAa,CAAC,MAAA,EAAmB,GAAA,KAAuB;AACnE,EAAA,OAAO,CACL,GAAA,EACA,GAAA,EACA,IAAA,KACG;AACH,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,SAAA,CAAU,GAAA,CAAI,GAAG,CAAC,CAAA;AACxC,IAAA,IAAI,CAAC,OAAO,OAAA,EAAS;AACnB,MAAA,MAAM,MAAA,GAAS,MAAA,CAAO,KAAA,CAAM,MAAA,CAAO,GAAA;AAAA,QACjC,CAAA,KAAA,KACE,0BAA0B,KAAA,CAAM,IAAA,CAAK,KAAK,GAAG,CAAC,CAAA,GAAA,EAAM,KAAA,CAAM,OAAO,CAAA;AAAA,OACrE;AACA,MAAA,GAAA,CAAI,OAAO,GAAG,CAAA,CAAE,IAAA,CAAK,EAAE,QAAQ,CAAA;AAC/B,MAAA;AAAA,IACF;AACA,IAAA,GAAA,CAAI,GAAG,IAAI,MAAA,CAAO,IAAA;AAClB,IAAA,IAAA,EAAK;AAAA,EACP,CAAA;AACF;;;;"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var prompts = require('../constants/prompts.cjs.js');
|
|
4
|
+
var prompts$1 = require('@langchain/core/prompts');
|
|
5
|
+
|
|
6
|
+
const createSummarizerService = async ({
|
|
7
|
+
config,
|
|
8
|
+
models
|
|
9
|
+
}) => {
|
|
10
|
+
const summaryModelId = config.getOptionalString("aiAssistant.conversation.summaryModel") ?? models[0].id;
|
|
11
|
+
const summaryPrompt = config.getOptionalString("aiAssistant.conversation.summaryPrompt") ?? prompts.DEFAULT_SUMMARY_PROMPT;
|
|
12
|
+
const model = models.find((m) => m.id === summaryModelId);
|
|
13
|
+
if (!model) {
|
|
14
|
+
throw new Error(`Summary model with id ${summaryModelId} not found`);
|
|
15
|
+
}
|
|
16
|
+
const llm = model.chatModel;
|
|
17
|
+
const summaryPromptTemplate = prompts$1.SystemMessagePromptTemplate.fromTemplate(`
|
|
18
|
+
PURPOSE:
|
|
19
|
+
{summaryPrompt}
|
|
20
|
+
Summarize the conversation in {summaryLength}
|
|
21
|
+
|
|
22
|
+
Conversation:
|
|
23
|
+
{conversation}
|
|
24
|
+
`);
|
|
25
|
+
const summarize = async (messages, summaryLength = "as few words as possible") => {
|
|
26
|
+
const conversationMessages = messages.filter(
|
|
27
|
+
(msg) => msg.role === "ai" || msg.role === "human"
|
|
28
|
+
);
|
|
29
|
+
const prompt = await summaryPromptTemplate.formatMessages({
|
|
30
|
+
summaryPrompt,
|
|
31
|
+
summaryLength,
|
|
32
|
+
conversation: conversationMessages.map((msg) => `${msg.role}: ${msg.content}`).join("\n")
|
|
33
|
+
});
|
|
34
|
+
const { text } = await llm.invoke(prompt);
|
|
35
|
+
return text.trim();
|
|
36
|
+
};
|
|
37
|
+
return { summarize };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
exports.createSummarizerService = createSummarizerService;
|
|
41
|
+
//# sourceMappingURL=summarizer.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summarizer.cjs.js","sources":["../../src/services/summarizer.ts"],"sourcesContent":["import { RootConfigService } from '@backstage/backend-plugin-api';\nimport { Model } from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { DEFAULT_SUMMARY_PROMPT } from '../constants/prompts';\nimport { SystemMessagePromptTemplate } from '@langchain/core/prompts';\nimport { Message } from '@sweetoburrito/backstage-plugin-ai-assistant-common';\n\ntype SummarizerService = {\n summarize: (\n conversationMessages: Message[],\n summaryLength?: string,\n ) => Promise<string>;\n};\n\ntype SummarizerServiceOptions = {\n config: RootConfigService;\n models: Model[];\n};\n\nexport const createSummarizerService = async ({\n config,\n models,\n}: SummarizerServiceOptions): Promise<SummarizerService> => {\n const summaryModelId =\n config.getOptionalString('aiAssistant.conversation.summaryModel') ??\n models[0].id;\n\n const summaryPrompt =\n config.getOptionalString('aiAssistant.conversation.summaryPrompt') ??\n DEFAULT_SUMMARY_PROMPT;\n\n const model = models.find(m => m.id === summaryModelId);\n\n if (!model) {\n throw new Error(`Summary model with id ${summaryModelId} not found`);\n }\n\n const llm = model.chatModel;\n\n const summaryPromptTemplate = SystemMessagePromptTemplate.fromTemplate(`\n PURPOSE:\n {summaryPrompt}\n Summarize the conversation in {summaryLength}\n\n Conversation:\n {conversation}\n `);\n\n const summarize: SummarizerService['summarize'] = async (\n messages,\n summaryLength = 'as few words as possible',\n ) => {\n const conversationMessages = messages.filter(\n msg => msg.role === 'ai' || msg.role === 'human',\n );\n\n const prompt = await summaryPromptTemplate.formatMessages({\n summaryPrompt,\n summaryLength,\n conversation: conversationMessages\n .map(msg => `${msg.role}: ${msg.content}`)\n .join('\\n'),\n });\n\n const { text } = await llm.invoke(prompt);\n\n return text.trim();\n };\n\n return { summarize };\n};\n"],"names":["DEFAULT_SUMMARY_PROMPT","SystemMessagePromptTemplate"],"mappings":";;;;;AAkBO,MAAM,0BAA0B,OAAO;AAAA,EAC5C,MAAA;AAAA,EACA;AACF,CAAA,KAA4D;AAC1D,EAAA,MAAM,iBACJ,MAAA,CAAO,iBAAA,CAAkB,uCAAuC,CAAA,IAChE,MAAA,CAAO,CAAC,CAAA,CAAE,EAAA;AAEZ,EAAA,MAAM,aAAA,GACJ,MAAA,CAAO,iBAAA,CAAkB,wCAAwC,CAAA,IACjEA,8BAAA;AAEF,EAAA,MAAM,QAAQ,MAAA,CAAO,IAAA,CAAK,CAAA,CAAA,KAAK,CAAA,CAAE,OAAO,cAAc,CAAA;AAEtD,EAAA,IAAI,CAAC,KAAA,EAAO;AACV,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,cAAc,CAAA,UAAA,CAAY,CAAA;AAAA,EACrE;AAEA,EAAA,MAAM,MAAM,KAAA,CAAM,SAAA;AAElB,EAAA,MAAM,qBAAA,GAAwBC,sCAA4B,YAAA,CAAa;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA,EAAA,CAOtE,CAAA;AAED,EAAA,MAAM,SAAA,GAA4C,OAChD,QAAA,EACA,aAAA,GAAgB,0BAAA,KACb;AACH,IAAA,MAAM,uBAAuB,QAAA,CAAS,MAAA;AAAA,MACpC,CAAA,GAAA,KAAO,GAAA,CAAI,IAAA,KAAS,IAAA,IAAQ,IAAI,IAAA,KAAS;AAAA,KAC3C;AAEA,IAAA,MAAM,MAAA,GAAS,MAAM,qBAAA,CAAsB,cAAA,CAAe;AAAA,MACxD,aAAA;AAAA,MACA,aAAA;AAAA,MACA,YAAA,EAAc,oBAAA,CACX,GAAA,CAAI,CAAA,GAAA,KAAO,CAAA,EAAG,GAAA,CAAI,IAAI,CAAA,EAAA,EAAK,GAAA,CAAI,OAAO,CAAA,CAAE,CAAA,CACxC,KAAK,IAAI;AAAA,KACb,CAAA;AAED,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,GAAA,CAAI,OAAO,MAAM,CAAA;AAExC,IAAA,OAAO,KAAK,IAAA,EAAK;AAAA,EACnB,CAAA;AAEA,EAAA,OAAO,EAAE,SAAA,EAAU;AACrB;;;;"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backstagePluginAiAssistantNode = require('@sweetoburrito/backstage-plugin-ai-assistant-node');
|
|
4
|
+
var z = require('zod');
|
|
5
|
+
|
|
6
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var z__default = /*#__PURE__*/_interopDefaultCompat(z);
|
|
9
|
+
|
|
10
|
+
const createSearchKnowledgeTool = ({
|
|
11
|
+
vectorStore
|
|
12
|
+
}) => {
|
|
13
|
+
const knowledgeTool = backstagePluginAiAssistantNode.createAssistantTool({
|
|
14
|
+
tool: {
|
|
15
|
+
name: "searchKnowledge",
|
|
16
|
+
description: `Search the internal knowledge base containing company specific information.
|
|
17
|
+
|
|
18
|
+
Use this tool when users ask about:
|
|
19
|
+
- General questions about the company or internal information
|
|
20
|
+
|
|
21
|
+
Do NOT use for general knowledge that doesn't require company-specific information.`,
|
|
22
|
+
schema: z__default.default.object({
|
|
23
|
+
query: z__default.default.string().describe("The query to search for."),
|
|
24
|
+
filter: z__default.default.object({
|
|
25
|
+
source: z__default.default.string().optional().describe("Source to filter by."),
|
|
26
|
+
id: z__default.default.string().optional().describe("ID to filter by.")
|
|
27
|
+
}).optional().describe("Filters to apply to the search."),
|
|
28
|
+
amount: z__default.default.number().min(1).optional().describe("The number of results to return.")
|
|
29
|
+
}),
|
|
30
|
+
func: async ({ query, filter, amount }) => {
|
|
31
|
+
const results = await vectorStore.similaritySearch(
|
|
32
|
+
query,
|
|
33
|
+
filter,
|
|
34
|
+
amount
|
|
35
|
+
);
|
|
36
|
+
if (results.length === 0) {
|
|
37
|
+
return "No relevant information found.";
|
|
38
|
+
}
|
|
39
|
+
return results.map((r) => r.content).join("\n---\n");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return knowledgeTool;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
exports.createSearchKnowledgeTool = createSearchKnowledgeTool;
|
|
47
|
+
//# sourceMappingURL=searchKnowledge.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"searchKnowledge.cjs.js","sources":["../../../src/services/tools/searchKnowledge.ts"],"sourcesContent":["import {\n createAssistantTool,\n Tool,\n VectorStore,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport z from 'zod';\n\ntype CreateSearchKnowledgeToolOptions = {\n vectorStore: VectorStore;\n};\n\nexport const createSearchKnowledgeTool = ({\n vectorStore,\n}: CreateSearchKnowledgeToolOptions): Tool => {\n const knowledgeTool = createAssistantTool({\n tool: {\n name: 'searchKnowledge',\n description: `Search the internal knowledge base containing company specific information.\n\nUse this tool when users ask about:\n- General questions about the company or internal information\n\nDo NOT use for general knowledge that doesn't require company-specific information.`,\n schema: z.object({\n query: z.string().describe('The query to search for.'),\n filter: z\n .object({\n source: z.string().optional().describe('Source to filter by.'),\n id: z.string().optional().describe('ID to filter by.'),\n })\n .optional()\n .describe('Filters to apply to the search.'),\n amount: z\n .number()\n .min(1)\n .optional()\n .describe('The number of results to return.'),\n }),\n func: async ({ query, filter, amount }) => {\n const results = await vectorStore.similaritySearch(\n query,\n filter,\n amount,\n );\n if (results.length === 0) {\n return 'No relevant information found.';\n }\n return results.map(r => r.content).join('\\n---\\n');\n },\n },\n });\n\n return knowledgeTool;\n};\n"],"names":["createAssistantTool","z"],"mappings":";;;;;;;;;AAWO,MAAM,4BAA4B,CAAC;AAAA,EACxC;AACF,CAAA,KAA8C;AAC5C,EAAA,MAAM,gBAAgBA,kDAAA,CAAoB;AAAA,IACxC,IAAA,EAAM;AAAA,MACJ,IAAA,EAAM,iBAAA;AAAA,MACN,WAAA,EAAa,CAAA;;AAAA;AAAA;;AAAA,mFAAA,CAAA;AAAA,MAMb,MAAA,EAAQC,mBAAE,MAAA,CAAO;AAAA,QACf,KAAA,EAAOA,kBAAA,CAAE,MAAA,EAAO,CAAE,SAAS,0BAA0B,CAAA;AAAA,QACrD,MAAA,EAAQA,mBACL,MAAA,CAAO;AAAA,UACN,QAAQA,kBAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,sBAAsB,CAAA;AAAA,UAC7D,IAAIA,kBAAA,CAAE,MAAA,GAAS,QAAA,EAAS,CAAE,SAAS,kBAAkB;AAAA,SACtD,CAAA,CACA,QAAA,EAAS,CACT,SAAS,iCAAiC,CAAA;AAAA,QAC7C,MAAA,EAAQA,kBAAA,CACL,MAAA,EAAO,CACP,GAAA,CAAI,CAAC,CAAA,CACL,QAAA,EAAS,CACT,QAAA,CAAS,kCAAkC;AAAA,OAC/C,CAAA;AAAA,MACD,MAAM,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,QAAO,KAAM;AACzC,QAAA,MAAM,OAAA,GAAU,MAAM,WAAA,CAAY,gBAAA;AAAA,UAChC,KAAA;AAAA,UACA,MAAA;AAAA,UACA;AAAA,SACF;AACA,QAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACxB,UAAA,OAAO,gCAAA;AAAA,QACT;AACA,QAAA,OAAO,QAAQ,GAAA,CAAI,CAAA,CAAA,KAAK,EAAE,OAAO,CAAA,CAAE,KAAK,SAAS,CAAA;AAAA,MACnD;AAAA;AACF,GACD,CAAA;AAED,EAAA,OAAO,aAAA;AACT;;;;"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param {import('knex').knex} knex
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
exports.down = async knex => {
|
|
7
|
+
// update all messages that have the role human back to role user
|
|
8
|
+
await knex('message').where('role', 'human').update({ role: 'user' });
|
|
9
|
+
|
|
10
|
+
// update all messages that have the role ai back to role assistant
|
|
11
|
+
await knex('message').where('role', 'ai').update({ role: 'assistant' });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
*
|
|
16
|
+
* @param {import('knex').knex} knex
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
exports.up = async knex => {
|
|
20
|
+
// update all messages that have the role user to role human
|
|
21
|
+
await knex('message').where('role', 'user').update({ role: 'human' });
|
|
22
|
+
|
|
23
|
+
// update all messages that have the role assistant to role ai
|
|
24
|
+
await knex('message').where('role', 'assistant').update({ role: 'ai' });
|
|
25
|
+
|
|
26
|
+
// add metadata jsonb column to message
|
|
27
|
+
await knex.schema.alterTable('message', table => {
|
|
28
|
+
table.jsonb('metadata').defaultTo({});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await knex('message').update({ metadata: {} });
|
|
32
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sweetoburrito/backstage-plugin-ai-assistant-backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "dist/index.cjs.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -38,14 +38,15 @@
|
|
|
38
38
|
"@backstage/errors": "backstage:^",
|
|
39
39
|
"@backstage/plugin-signals-node": "backstage:^",
|
|
40
40
|
"@langchain/core": "^0.3.72",
|
|
41
|
+
"@langchain/langgraph": "^0.4.9",
|
|
41
42
|
"@langchain/textsplitters": "^0.1.0",
|
|
42
|
-
"@sweetoburrito/backstage-plugin-ai-assistant-common": "^0.
|
|
43
|
-
"@sweetoburrito/backstage-plugin-ai-assistant-node": "^0.
|
|
43
|
+
"@sweetoburrito/backstage-plugin-ai-assistant-common": "^0.5.0",
|
|
44
|
+
"@sweetoburrito/backstage-plugin-ai-assistant-node": "^0.5.0",
|
|
44
45
|
"express": "^4.17.1",
|
|
45
46
|
"express-promise-router": "^4.1.0",
|
|
46
47
|
"knex": "^3.1.0",
|
|
47
48
|
"uuid": "^11.1.0",
|
|
48
|
-
"zod": "^
|
|
49
|
+
"zod": "^4.1.11"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
52
|
"@backstage/backend-test-utils": "backstage:^",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"@sweetoburrito/backstage-plugin-ai-assistant-backend-module-embeddings-provider-ollama": "workspace:^",
|
|
61
62
|
"@sweetoburrito/backstage-plugin-ai-assistant-backend-module-ingestor-azure-devops": "workspace:^",
|
|
62
63
|
"@sweetoburrito/backstage-plugin-ai-assistant-backend-module-ingestor-catalog": "workspace:^",
|
|
64
|
+
"@sweetoburrito/backstage-plugin-ai-assistant-backend-module-ingestor-github": "workspace:^",
|
|
63
65
|
"@sweetoburrito/backstage-plugin-ai-assistant-backend-module-model-provider-azure-ai": "workspace:^",
|
|
64
66
|
"@sweetoburrito/backstage-plugin-ai-assistant-backend-module-model-provider-ollama": "workspace:^",
|
|
65
67
|
"@types/express": "^4.0.0",
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var prompts = require('../constants/prompts.cjs.js');
|
|
4
|
-
|
|
5
|
-
const createPromptBuilder = ({
|
|
6
|
-
config
|
|
7
|
-
}) => {
|
|
8
|
-
const system = config.getOptionalString("aiAssistant.prompt.system") || prompts.DEFAULT_SYSTEM_PROMPT;
|
|
9
|
-
const getContext = (context) => {
|
|
10
|
-
return `
|
|
11
|
-
Context:
|
|
12
|
-
${context.map((doc) => JSON.stringify(doc)).join("\n")}
|
|
13
|
-
`;
|
|
14
|
-
};
|
|
15
|
-
const buildPrompt = (chatHistory, promptContext) => {
|
|
16
|
-
const context = getContext(promptContext);
|
|
17
|
-
return [
|
|
18
|
-
{
|
|
19
|
-
role: "system",
|
|
20
|
-
content: system.concat(context)
|
|
21
|
-
},
|
|
22
|
-
...chatHistory
|
|
23
|
-
];
|
|
24
|
-
};
|
|
25
|
-
return {
|
|
26
|
-
buildPrompt
|
|
27
|
-
};
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
exports.createPromptBuilder = createPromptBuilder;
|
|
31
|
-
//# sourceMappingURL=prompt.cjs.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prompt.cjs.js","sources":["../../src/services/prompt.ts"],"sourcesContent":["import { RootConfigService } from '@backstage/backend-plugin-api';\nimport { EmbeddingDocument } from '@sweetoburrito/backstage-plugin-ai-assistant-node';\n\nimport { Message } from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { DEFAULT_SYSTEM_PROMPT } from '../constants/prompts';\n\ntype PromptBuilderOptions = {\n config: RootConfigService;\n};\n\nexport type PromptBuilder = {\n buildPrompt: (\n chatHistory: Message[],\n promptContext: EmbeddingDocument[],\n ) => Message[];\n};\n\nexport const createPromptBuilder = ({\n config,\n}: PromptBuilderOptions): PromptBuilder => {\n const system =\n config.getOptionalString('aiAssistant.prompt.system') ||\n DEFAULT_SYSTEM_PROMPT;\n\n const getContext = (context: EmbeddingDocument[]) => {\n return `\n Context:\n ${context.map(doc => JSON.stringify(doc)).join('\\n')}\n `;\n };\n\n const buildPrompt: PromptBuilder['buildPrompt'] = (\n chatHistory,\n promptContext,\n ) => {\n const context = getContext(promptContext);\n\n return [\n {\n role: 'system',\n content: system.concat(context),\n },\n ...chatHistory,\n ];\n };\n\n return {\n buildPrompt,\n };\n};\n"],"names":["DEFAULT_SYSTEM_PROMPT"],"mappings":";;;;AAiBO,MAAM,sBAAsB,CAAC;AAAA,EAClC;AACF,CAAA,KAA2C;AACzC,EAAA,MAAM,MAAA,GACJ,MAAA,CAAO,iBAAA,CAAkB,2BAA2B,CAAA,IACpDA,6BAAA;AAEF,EAAA,MAAM,UAAA,GAAa,CAAC,OAAA,KAAiC;AACnD,IAAA,OAAO;AAAA;AAAA,IAAA,EAEL,OAAA,CAAQ,GAAA,CAAI,CAAA,GAAA,KAAO,IAAA,CAAK,SAAA,CAAU,GAAG,CAAC,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC;AAAA,IAAA,CAAA;AAAA,EAEtD,CAAA;AAEA,EAAA,MAAM,WAAA,GAA4C,CAChD,WAAA,EACA,aAAA,KACG;AACH,IAAA,MAAM,OAAA,GAAU,WAAW,aAAa,CAAA;AAExC,IAAA,OAAO;AAAA,MACL;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,OAAO;AAAA,OAChC;AAAA,MACA,GAAG;AAAA,KACL;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL;AAAA,GACF;AACF;;;;"}
|