@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.
- package/README.md +28 -0
- package/config.d.ts +32 -0
- package/dist/constants/prompts.cjs.js +43 -0
- package/dist/constants/prompts.cjs.js.map +1 -0
- package/dist/database/chat-store.cjs.js +96 -0
- package/dist/database/chat-store.cjs.js.map +1 -0
- package/dist/database/migrations.cjs.js +16 -0
- package/dist/database/migrations.cjs.js.map +1 -0
- package/dist/database/pg-vector-store.cjs.js +193 -0
- package/dist/database/pg-vector-store.cjs.js.map +1 -0
- package/dist/index.cjs.js +10 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/plugin.cjs.js +101 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/services/chat.cjs.js +258 -0
- package/dist/services/chat.cjs.js.map +1 -0
- package/dist/services/ingestor.cjs.js +89 -0
- package/dist/services/ingestor.cjs.js.map +1 -0
- package/dist/services/langfuse.cjs.js +39 -0
- package/dist/services/langfuse.cjs.js.map +1 -0
- package/dist/services/router/chat.cjs.js +73 -0
- package/dist/services/router/chat.cjs.js.map +1 -0
- package/dist/services/router/index.cjs.js +25 -0
- package/dist/services/router/index.cjs.js.map +1 -0
- package/dist/services/router/middleware/validation.cjs.js +19 -0
- package/dist/services/router/middleware/validation.cjs.js.map +1 -0
- package/dist/services/router/models.cjs.js +20 -0
- package/dist/services/router/models.cjs.js.map +1 -0
- package/dist/services/summarizer.cjs.js +54 -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/20250822_init.js +47 -0
- package/migrations/20250828_chat_history.js +44 -0
- package/migrations/20250909_conversations.js +92 -0
- package/migrations/20251005_tools.js +32 -0
- 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
|
+
}
|