@sweetoburrito/backstage-plugin-ai-assistant-backend 0.0.0-snapshot-20251029080430

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.
Files changed (38) hide show
  1. package/README.md +28 -0
  2. package/config.d.ts +32 -0
  3. package/dist/constants/prompts.cjs.js +43 -0
  4. package/dist/constants/prompts.cjs.js.map +1 -0
  5. package/dist/database/chat-store.cjs.js +96 -0
  6. package/dist/database/chat-store.cjs.js.map +1 -0
  7. package/dist/database/migrations.cjs.js +16 -0
  8. package/dist/database/migrations.cjs.js.map +1 -0
  9. package/dist/database/pg-vector-store.cjs.js +193 -0
  10. package/dist/database/pg-vector-store.cjs.js.map +1 -0
  11. package/dist/index.cjs.js +10 -0
  12. package/dist/index.cjs.js.map +1 -0
  13. package/dist/index.d.ts +10 -0
  14. package/dist/plugin.cjs.js +101 -0
  15. package/dist/plugin.cjs.js.map +1 -0
  16. package/dist/services/chat.cjs.js +258 -0
  17. package/dist/services/chat.cjs.js.map +1 -0
  18. package/dist/services/ingestor.cjs.js +89 -0
  19. package/dist/services/ingestor.cjs.js.map +1 -0
  20. package/dist/services/langfuse.cjs.js +39 -0
  21. package/dist/services/langfuse.cjs.js.map +1 -0
  22. package/dist/services/router/chat.cjs.js +73 -0
  23. package/dist/services/router/chat.cjs.js.map +1 -0
  24. package/dist/services/router/index.cjs.js +25 -0
  25. package/dist/services/router/index.cjs.js.map +1 -0
  26. package/dist/services/router/middleware/validation.cjs.js +19 -0
  27. package/dist/services/router/middleware/validation.cjs.js.map +1 -0
  28. package/dist/services/router/models.cjs.js +20 -0
  29. package/dist/services/router/models.cjs.js.map +1 -0
  30. package/dist/services/summarizer.cjs.js +54 -0
  31. package/dist/services/summarizer.cjs.js.map +1 -0
  32. package/dist/services/tools/searchKnowledge.cjs.js +47 -0
  33. package/dist/services/tools/searchKnowledge.cjs.js.map +1 -0
  34. package/migrations/20250822_init.js +47 -0
  35. package/migrations/20250828_chat_history.js +44 -0
  36. package/migrations/20250909_conversations.js +92 -0
  37. package/migrations/20251005_tools.js +32 -0
  38. package/package.json +92 -0
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ var prompts = require('../constants/prompts.cjs.js');
4
+ var prompts$1 = require('@langchain/core/prompts');
5
+ var langchain = require('@langfuse/langchain');
6
+
7
+ const createSummarizerService = async ({
8
+ config,
9
+ models,
10
+ langfuseEnabled
11
+ }) => {
12
+ const summaryModelId = config.getOptionalString("aiAssistant.conversation.summaryModel") ?? models[0].id;
13
+ const summaryPrompt = config.getOptionalString("aiAssistant.conversation.summaryPrompt") ?? prompts.DEFAULT_SUMMARY_PROMPT;
14
+ const model = models.find((m) => m.id === summaryModelId);
15
+ if (!model) {
16
+ throw new Error(`Summary model with id ${summaryModelId} not found`);
17
+ }
18
+ const llm = model.chatModel;
19
+ const langfuseHandler = langfuseEnabled ? new langchain.CallbackHandler({
20
+ userId: "summarizer",
21
+ tags: ["backstage-ai-assistant", "summarizer"]
22
+ }) : void 0;
23
+ const summaryPromptTemplate = prompts$1.SystemMessagePromptTemplate.fromTemplate(`
24
+ PURPOSE:
25
+ {summaryPrompt}
26
+ Summarize the conversation in {summaryLength}
27
+
28
+ Conversation:
29
+ {conversation}
30
+ `);
31
+ const summarize = async (messages, summaryLength = "as few words as possible") => {
32
+ const conversationMessages = messages.filter(
33
+ (msg) => msg.role === "ai" || msg.role === "human"
34
+ );
35
+ const prompt = await summaryPromptTemplate.formatMessages({
36
+ summaryPrompt,
37
+ summaryLength,
38
+ conversation: conversationMessages.map((msg) => `${msg.role}: ${msg.content}`).join("\n")
39
+ });
40
+ const invokeOptions = {
41
+ runName: "conversation-summarizer",
42
+ tags: ["summarizer"]
43
+ };
44
+ if (langfuseEnabled) {
45
+ invokeOptions.callbacks = [langfuseHandler];
46
+ }
47
+ const { text } = await llm.invoke(prompt, invokeOptions);
48
+ return text.trim();
49
+ };
50
+ return { summarize };
51
+ };
52
+
53
+ exports.createSummarizerService = createSummarizerService;
54
+ //# 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';\nimport { CallbackHandler } from '@langfuse/langchain';\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 langfuseEnabled: boolean;\n};\n\nexport const createSummarizerService = async ({\n config,\n models,\n langfuseEnabled,\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 // Initialize Langfuse CallbackHandler for tracing if credentials are available\n const langfuseHandler = langfuseEnabled\n ? new CallbackHandler({\n userId: 'summarizer',\n tags: ['backstage-ai-assistant', 'summarizer'],\n })\n : undefined;\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 invokeOptions: any = {\n runName: 'conversation-summarizer',\n tags: ['summarizer'],\n };\n\n if (langfuseEnabled) {\n invokeOptions.callbacks = [langfuseHandler];\n }\n\n const { text } = await llm.invoke(prompt, invokeOptions);\n\n return text.trim();\n };\n\n return { summarize };\n};\n"],"names":["DEFAULT_SUMMARY_PROMPT","CallbackHandler","SystemMessagePromptTemplate"],"mappings":";;;;;;AAoBO,MAAM,0BAA0B,OAAO;AAAA,EAC5C,MAAA;AAAA,EACA,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;AAGlB,EAAA,MAAM,eAAA,GAAkB,eAAA,GACpB,IAAIC,yBAAA,CAAgB;AAAA,IAClB,MAAA,EAAQ,YAAA;AAAA,IACR,IAAA,EAAM,CAAC,wBAAA,EAA0B,YAAY;AAAA,GAC9C,CAAA,GACD,MAAA;AAEJ,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,aAAA,GAAqB;AAAA,MACzB,OAAA,EAAS,yBAAA;AAAA,MACT,IAAA,EAAM,CAAC,YAAY;AAAA,KACrB;AAEA,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,aAAA,CAAc,SAAA,GAAY,CAAC,eAAe,CAAA;AAAA,IAC5C;AAEA,IAAA,MAAM,EAAE,IAAA,EAAK,GAAI,MAAM,GAAA,CAAI,MAAA,CAAO,QAAQ,aAAa,CAAA;AAEvD,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: "search-knowledge-base",
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: 'search-knowledge-base',\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,uBAAA;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,47 @@
1
+ const TABLE_NAME = 'embeddings';
2
+
3
+ /**
4
+ *
5
+ * @param {import('knex').knex} knex
6
+ */
7
+
8
+ exports.down = async knex => {
9
+ await knex.schema.dropTable('embeddings');
10
+ await knex.raw('drop extension if exists "uuid-ossp"');
11
+ await knex.raw('drop extension if exists "vector"');
12
+ };
13
+
14
+ /**
15
+ *
16
+ * @param {import('knex').knex} knex
17
+ */
18
+
19
+ exports.up = async knex => {
20
+ await knex.raw('create extension if not exists "uuid-ossp"');
21
+ await knex.raw('create extension if not exists "vector"');
22
+ await knex.schema.createTable(TABLE_NAME, table => {
23
+ table.comment(
24
+ 'Stores embeddings of documents from the system to be used as RAG AI injectables. ',
25
+ );
26
+ table
27
+ .uuid('id')
28
+ .notNullable()
29
+ .primary()
30
+ .defaultTo(knex.raw('uuid_generate_v4()'))
31
+ .comment('UUID of the embedding');
32
+ table
33
+ .text('content')
34
+ .notNullable()
35
+ .comment('Actual content of the embedding. Chunks of text/data');
36
+ table
37
+ .jsonb('metadata')
38
+ .notNullable()
39
+ .comment(
40
+ 'Metadata of the embedding. Information like entityRef etc. that can be used to identify links to other parts of the system.',
41
+ );
42
+ });
43
+ await knex.schema.raw(`ALTER TABLE ${TABLE_NAME}
44
+ ADD vector vector NOT NULL ; `);
45
+ await knex.schema.raw(`COMMENT ON COLUMN ${TABLE_NAME}.vector
46
+ IS 'Vector weights of the related content.';`);
47
+ };
@@ -0,0 +1,44 @@
1
+ const TABLE_NAME = 'conversation';
2
+
3
+ /**
4
+ *
5
+ * @param {import('knex').knex} knex
6
+ */
7
+ exports.down = async knex => {
8
+ await knex.schema.dropTable(TABLE_NAME);
9
+ };
10
+
11
+ /**
12
+ *
13
+ * @param {import('knex').knex} knex
14
+ */
15
+ exports.up = async knex => {
16
+ await knex.schema.createTable(TABLE_NAME, table => {
17
+ table.comment(
18
+ 'Stores chat history for conversations with the AI assistant.',
19
+ );
20
+ table
21
+ .uuid('id')
22
+ .notNullable()
23
+ .primary()
24
+ .comment('UUID of the chat message');
25
+ table
26
+ .text('conversation_id')
27
+ .notNullable()
28
+ .comment('Identifier for the conversation this message belongs to');
29
+ table
30
+ .text('role')
31
+ .notNullable()
32
+ .comment("Role of the message sender, e.g., 'user' or 'assistant'");
33
+ table.text('content').notNullable().comment('Content of the chat message');
34
+ table
35
+ .text('userRef')
36
+ .notNullable()
37
+ .comment('Reference to the user who sent the message');
38
+ table
39
+ .timestamp('created_at')
40
+ .notNullable()
41
+ .defaultTo(knex.fn.now())
42
+ .comment('Timestamp when the message was created');
43
+ });
44
+ };
@@ -0,0 +1,92 @@
1
+ const oldMessageTable = 'conversation';
2
+ const newMessageTable = 'message';
3
+ const newConversationTable = 'conversation';
4
+
5
+ /**
6
+ *
7
+ * @param {import('knex').knex} knex
8
+ */
9
+ exports.down = async knex => {
10
+ // Remove link between messages and conversations
11
+ const hasConversationId = await knex.schema.hasColumn(
12
+ newMessageTable,
13
+ 'conversation_id',
14
+ );
15
+ if (hasConversationId) {
16
+ await knex.schema.alterTable(newMessageTable, table => {
17
+ table.dropForeign('conversation_id');
18
+ table.dropColumn('conversation_id');
19
+ });
20
+ }
21
+
22
+ // Drop new conversations table
23
+ const hasNewConversationTable = await knex.schema.hasTable(
24
+ newConversationTable,
25
+ );
26
+ if (hasNewConversationTable) {
27
+ await knex.schema.dropTable(newConversationTable);
28
+ }
29
+
30
+ // Rename message table back to conversation
31
+ const hasMessageTable = await knex.schema.hasTable(newMessageTable);
32
+ if (hasMessageTable) {
33
+ await knex.schema.renameTable(newMessageTable, oldMessageTable);
34
+
35
+ // Rename the constraint back to original name
36
+ await knex.raw(
37
+ 'ALTER TABLE conversation RENAME CONSTRAINT message_pkey TO conversation_pkey',
38
+ );
39
+ }
40
+ };
41
+
42
+ /**
43
+ *
44
+ * @param {import('knex').knex} knex
45
+ */
46
+ exports.up = async knex => {
47
+ // Check if the old conversation table exists and needs to be renamed
48
+ const hasOldConversationTable = await knex.schema.hasTable(oldMessageTable);
49
+ const hasMessageTable = await knex.schema.hasTable(newMessageTable);
50
+
51
+ if (hasOldConversationTable && !hasMessageTable) {
52
+ // First, rename the primary key constraint to avoid conflicts
53
+ await knex.raw(
54
+ 'ALTER TABLE conversation RENAME CONSTRAINT conversation_pkey TO message_pkey',
55
+ );
56
+ // Rename old table to new name
57
+ await knex.schema.renameTable(oldMessageTable, newMessageTable);
58
+ }
59
+
60
+ // Create new conversations table (only if it doesn't exist)
61
+ const hasNewConversationTable = await knex.schema.hasTable(
62
+ newConversationTable,
63
+ );
64
+ if (!hasNewConversationTable) {
65
+ await knex.schema.createTable(newConversationTable, table => {
66
+ table.uuid('id').primary().notNullable();
67
+ table.string('title').notNullable().comment('Title of the conversation');
68
+ table
69
+ .text('userRef')
70
+ .notNullable()
71
+ .comment('Reference to the user who sent the message');
72
+ table.timestamps(true, true);
73
+ });
74
+ }
75
+
76
+ // Add conversation_id column to message table (only if it doesn't exist)
77
+ const hasConversationId = await knex.schema.hasColumn(
78
+ newMessageTable,
79
+ 'conversation_id',
80
+ );
81
+ if (!hasConversationId) {
82
+ await knex.schema.alterTable(newMessageTable, table => {
83
+ table
84
+ .uuid('conversation_id')
85
+ .notNullable()
86
+ .comment('Identifier for the conversation this message belongs to')
87
+ .references('id')
88
+ .inTable(newConversationTable)
89
+ .onDelete('RESTRICT'); // Prevents deleting conversations with messages
90
+ });
91
+ }
92
+ };
@@ -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 ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "@sweetoburrito/backstage-plugin-ai-assistant-backend",
3
+ "version": "0.0.0-snapshot-20251029080430",
4
+ "license": "Apache-2.0",
5
+ "main": "dist/index.cjs.js",
6
+ "types": "dist/index.d.ts",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "main": "dist/index.cjs.js",
10
+ "types": "dist/index.d.ts"
11
+ },
12
+ "backstage": {
13
+ "role": "backend-plugin",
14
+ "pluginId": "ai-assistant",
15
+ "pluginPackages": [
16
+ "@sweetoburrito/backstage-plugin-ai-assistant",
17
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend",
18
+ "@sweetoburrito/backstage-plugin-ai-assistant-common",
19
+ "@sweetoburrito/backstage-plugin-ai-assistant-node"
20
+ ],
21
+ "features": {
22
+ ".": "@backstage/BackendFeature"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "start": "backstage-cli package start",
27
+ "build": "backstage-cli package build",
28
+ "lint": "backstage-cli package lint",
29
+ "test": "backstage-cli package test",
30
+ "clean": "backstage-cli package clean",
31
+ "prepack": "backstage-cli package prepack",
32
+ "postpack": "backstage-cli package postpack"
33
+ },
34
+ "dependencies": {
35
+ "@backstage/backend-defaults": "backstage:^",
36
+ "@backstage/backend-plugin-api": "backstage:^",
37
+ "@backstage/catalog-client": "backstage:^",
38
+ "@backstage/catalog-model": "backstage:^",
39
+ "@backstage/errors": "backstage:^",
40
+ "@backstage/plugin-catalog-node": "backstage:^",
41
+ "@backstage/plugin-signals-node": "backstage:^",
42
+ "@langchain/core": "^0.3.72",
43
+ "@langchain/langgraph": "^0.4.9",
44
+ "@langchain/textsplitters": "^0.1.0",
45
+ "@langfuse/core": "^4.0.0",
46
+ "@langfuse/langchain": "^4.0.0",
47
+ "@langfuse/otel": "^4.3.0",
48
+ "@opentelemetry/api": "^1.9.0",
49
+ "@opentelemetry/sdk-node": "^0.207.0",
50
+ "@sweetoburrito/backstage-plugin-ai-assistant-common": "^0.5.0",
51
+ "@sweetoburrito/backstage-plugin-ai-assistant-node": "^0.5.1",
52
+ "express": "^4.17.1",
53
+ "express-promise-router": "^4.1.0",
54
+ "knex": "^3.1.0",
55
+ "uuid": "^11.1.0",
56
+ "zod": "^4.1.11"
57
+ },
58
+ "devDependencies": {
59
+ "@backstage/backend-test-utils": "backstage:^",
60
+ "@backstage/cli": "backstage:^",
61
+ "@backstage/plugin-auth-backend": "backstage:^",
62
+ "@backstage/plugin-auth-backend-module-guest-provider": "backstage:^",
63
+ "@backstage/plugin-catalog-backend": "backstage:^",
64
+ "@backstage/plugin-events-backend": "backstage:^",
65
+ "@backstage/plugin-signals-backend": "backstage:^",
66
+ "@backstage/types": "backstage:^",
67
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-embeddings-provider-azure-open-ai": "workspace:^",
68
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-embeddings-provider-ollama": "workspace:^",
69
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-ingestor-azure-devops": "workspace:^",
70
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-ingestor-catalog": "workspace:^",
71
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-ingestor-github": "workspace:^",
72
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-model-provider-azure-ai": "workspace:^",
73
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-model-provider-ollama": "workspace:^",
74
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-tool-provider-backstage": "workspace:^",
75
+ "@types/express": "^4.0.0",
76
+ "@types/supertest": "^2.0.12",
77
+ "supertest": "^6.2.4"
78
+ },
79
+ "configSchema": "config.d.ts",
80
+ "files": [
81
+ "dist",
82
+ "migrations",
83
+ "config.d.ts"
84
+ ],
85
+ "typesVersions": {
86
+ "*": {
87
+ "package.json": [
88
+ "package.json"
89
+ ]
90
+ }
91
+ }
92
+ }