@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
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # ai-assistant
2
+
3
+ This plugin backend was templated using the Backstage CLI. You should replace this text with a description of your plugin backend.
4
+
5
+ ## Installation
6
+
7
+ This plugin is installed via the `@internal/plugin-ai-assistant-backend` package. To install it to your backend package, run the following command:
8
+
9
+ ```bash
10
+ # From your root directory
11
+ yarn --cwd packages/backend add @internal/plugin-ai-assistant-backend
12
+ ```
13
+
14
+ Then add the plugin to your backend in `packages/backend/src/index.ts`:
15
+
16
+ ```ts
17
+ const backend = createBackend();
18
+ // ...
19
+ backend.add(import('@internal/plugin-ai-assistant-backend'));
20
+ ```
21
+
22
+ ## Development
23
+
24
+ This plugin backend can be started in a standalone mode from directly in this
25
+ package with `yarn start`. It is a limited setup that is most convenient when
26
+ developing the plugin backend itself.
27
+
28
+ If you want to run the entire project, including the frontend, run `yarn start` from the root directory.
package/config.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { HumanDuration } from '@backstage/types';
2
+ import { SchedulerServiceTaskScheduleDefinitionConfig } from '@backstage/backend-plugin-api';
3
+
4
+ export interface Config {
5
+ aiAssistant: {
6
+ prompt?: {
7
+ system?: string;
8
+ prefix?: string;
9
+ suffix?: string;
10
+ };
11
+ conversation?: {
12
+ summaryModel?: string;
13
+ summaryPrompt?: string;
14
+ };
15
+ storage?: {
16
+ pgVector?: {
17
+ /**
18
+ * The size of the chunk to flush when storing embeddings to the DB
19
+ */
20
+ chunkSize?: number;
21
+
22
+ /**
23
+ * The default amount of embeddings to return when querying vectors with similarity search
24
+ */
25
+ amount?: number;
26
+ };
27
+ };
28
+ ingestion?: {
29
+ schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
30
+ };
31
+ };
32
+ }
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
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
+ const DEFAULT_IDENTITY_PROMPT = `
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
+ `;
7
+ const DEFAULT_FORMATTING_PROMPT = `
8
+ CRITICAL FORMATTING RULES - MUST ALWAYS FOLLOW:
9
+ 1. **ALWAYS use proper markdown formatting in ALL responses**
10
+ 2. **NEVER output plain URLs** - ALWAYS convert them to clickable markdown links using [description](url) syntax
11
+ 3. **For images, ALWAYS use markdown image syntax**: ![alt text](image-url)
12
+ 4. **For all URLs, ALWAYS format as**: [descriptive text](url) - never just paste the raw URL
13
+ 5. Use headings (##, ###), bullet points, numbered lists, and **bold**/*italic* text appropriately
14
+ 6. Format code with backticks: \`inline code\` or \`\`\`language for code blocks
15
+ 7. Structure responses clearly with proper spacing and organization
16
+ `;
17
+ const DEFAULT_SYSTEM_PROMPT = `
18
+ Content Rules:
19
+ 1. Always base your answers on the provided context. Do not make up information.
20
+ 2. When relevant, cite or reference the source information provided in the context.
21
+ 3. Maintain a professional, friendly, and helpful tone.
22
+ 4. Return only the relevant information without any filler or unnecessary details.
23
+ 5. If you don't know the answer, admit it and suggest ways to find the information.
24
+ 6. **Actively use available tools** to enhance your responses
25
+ 7. Adapt your approach based on the specific tools and capabilities available in the current session
26
+ 8. When you do not have the information needed to answer, use the tools provided to gather more context before responding.
27
+ `;
28
+ const DEFAULT_TOOL_GUIDELINE = `
29
+ TOOL USAGE GUIDELINES:
30
+ - Only use tools when explicitly needed to answer the user's question
31
+ - Read tool descriptions carefully before using them
32
+ - If you can answer without tools, do so
33
+ - IMPORTANT: When using tools, always explain why you're using each tool
34
+ - Use tools in logical sequence, not randomly
35
+ - If a tool fails, try an alternative approach before using another tool
36
+ `;
37
+
38
+ exports.DEFAULT_FORMATTING_PROMPT = DEFAULT_FORMATTING_PROMPT;
39
+ exports.DEFAULT_IDENTITY_PROMPT = DEFAULT_IDENTITY_PROMPT;
40
+ exports.DEFAULT_SUMMARY_PROMPT = DEFAULT_SUMMARY_PROMPT;
41
+ exports.DEFAULT_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT;
42
+ exports.DEFAULT_TOOL_GUIDELINE = DEFAULT_TOOL_GUIDELINE;
43
+ //# sourceMappingURL=prompts.cjs.js.map
@@ -0,0 +1 @@
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_IDENTITY_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`;\n\nexport const DEFAULT_FORMATTING_PROMPT = `\nCRITICAL FORMATTING RULES - MUST ALWAYS FOLLOW:\n1. **ALWAYS use proper markdown formatting in ALL responses**\n2. **NEVER output plain URLs** - ALWAYS convert them to clickable markdown links using [description](url) syntax\n3. **For images, ALWAYS use markdown image syntax**: ![alt text](image-url)\n4. **For all URLs, ALWAYS format as**: [descriptive text](url) - never just paste the raw URL\n5. Use headings (##, ###), bullet points, numbered lists, and **bold**/*italic* text appropriately\n6. Format code with backticks: \\`inline code\\` or \\`\\`\\`language for code blocks\n7. Structure responses clearly with proper spacing and organization\n`;\n\nexport const DEFAULT_SYSTEM_PROMPT = `\nContent Rules:\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. Maintain a professional, friendly, and helpful tone.\n4. Return only the relevant information without any filler or unnecessary details.\n5. If you don't know the answer, admit it and suggest ways to find the information.\n6. **Actively use available tools** to enhance your responses\n7. Adapt your approach based on the specific tools and capabilities available in the current session\n8. When you do not have the information needed to answer, use the tools provided to gather more context before responding.\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- IMPORTANT: 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,uBAAA,GAA0B;AAAA;AAAA;AAIhC,MAAM,yBAAA,GAA4B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAWlC,MAAM,qBAAA,GAAwB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAY9B,MAAM,sBAAA,GAAyB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;;;;;;"}
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const MESSAGE_TABLE_NAME = "message";
4
+ const CONVERSATION_TABLE_NAME = "conversation";
5
+ class ChatStore {
6
+ /**
7
+ * Creates an instance of ChatStore.
8
+ * @param client - The Knex client to interact with the PostgreSQL database.
9
+ */
10
+ constructor(client) {
11
+ this.client = client;
12
+ }
13
+ static async fromConfig({ database }) {
14
+ const client = await database.getClient();
15
+ return new ChatStore(client);
16
+ }
17
+ messageTable() {
18
+ return this.client(MESSAGE_TABLE_NAME);
19
+ }
20
+ conversationTable() {
21
+ return this.client(CONVERSATION_TABLE_NAME);
22
+ }
23
+ async getChatMessages(conversationId, userRef, limit, excludeRoles) {
24
+ let query = this.messageTable().where({ conversation_id: conversationId, userRef }).select("*").orderBy("created_at", "asc");
25
+ if (typeof limit === "number") {
26
+ query = query.limit(limit).orderBy("created_at", "desc");
27
+ }
28
+ if (excludeRoles && excludeRoles.length > 0) {
29
+ query = query.whereNotIn("role", excludeRoles);
30
+ }
31
+ const rows = await query;
32
+ const chatMessages = rows.map((row) => ({
33
+ role: row.role,
34
+ content: row.content,
35
+ id: row.id,
36
+ metadata: row.metadata
37
+ }));
38
+ return chatMessages;
39
+ }
40
+ async addChatMessage(messages, userRef, conversationId) {
41
+ const rows = messages.map((msg) => ({
42
+ id: msg.id,
43
+ conversation_id: conversationId,
44
+ role: msg.role,
45
+ content: msg.content,
46
+ metadata: msg.metadata,
47
+ userRef,
48
+ created_at: this.client.fn.now()
49
+ }));
50
+ await this.messageTable().insert(rows);
51
+ }
52
+ async updateMessage(message) {
53
+ await this.messageTable().where({ id: message.id }).update({
54
+ role: message.role,
55
+ content: message.content,
56
+ metadata: message.metadata
57
+ });
58
+ }
59
+ async getConversation(conversationId, userRef) {
60
+ const row = await this.conversationTable().where({ id: conversationId, userRef }).first();
61
+ if (!row) {
62
+ throw new Error("Conversation not found");
63
+ }
64
+ const conversation = {
65
+ id: row.id,
66
+ title: row.title,
67
+ userRef: row.userRef
68
+ };
69
+ return conversation;
70
+ }
71
+ async createConversation(conversation) {
72
+ await this.conversationTable().insert({
73
+ id: conversation.id,
74
+ title: conversation.title,
75
+ userRef: conversation.userRef
76
+ });
77
+ }
78
+ async updateConversation(conversation) {
79
+ await this.conversationTable().where({ id: conversation.id }).update({
80
+ title: conversation.title,
81
+ userRef: conversation.userRef
82
+ });
83
+ }
84
+ async getConversations(userRef) {
85
+ const rows = await this.conversationTable().where({ userRef }).select("*").orderBy("created_at", "desc");
86
+ const conversations = rows.map((row) => ({
87
+ id: row.id,
88
+ title: row.title,
89
+ userRef: row.userRef
90
+ }));
91
+ return conversations;
92
+ }
93
+ }
94
+
95
+ exports.ChatStore = ChatStore;
96
+ //# sourceMappingURL=chat-store.cjs.js.map
@@ -0,0 +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 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":";;AAQA,MAAM,kBAAA,GAAqB,SAAA;AAC3B,MAAM,uBAAA,GAA0B,cAAA;AAMzB,MAAM,SAAA,CAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKrB,YAA6B,MAAA,EAAc;AAAd,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AAAA,EAAe;AAAA,EAE5C,aAAa,UAAA,CAAW,EAAE,QAAA,EAAS,EAAqB;AACtD,IAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,SAAA,EAAU;AACxC,IAAA,OAAO,IAAI,UAAU,MAAM,CAAA;AAAA,EAC7B;AAAA,EAEA,YAAA,GAAe;AACb,IAAA,OAAO,IAAA,CAAK,OAAO,kBAAkB,CAAA;AAAA,EACvC;AAAA,EAEA,iBAAA,GAAoB;AAClB,IAAA,OAAO,IAAA,CAAK,OAAO,uBAAuB,CAAA;AAAA,EAC5C;AAAA,EAEA,MAAM,eAAA,CACJ,cAAA,EACA,OAAA,EACA,OACA,YAAA,EAC8B;AAC9B,IAAA,IAAI,QAAQ,IAAA,CAAK,YAAA,EAAa,CAC3B,KAAA,CAAM,EAAE,eAAA,EAAiB,cAAA,EAAgB,OAAA,EAAS,EAClD,MAAA,CAAO,GAAG,CAAA,CACV,OAAA,CAAQ,cAAc,KAAK,CAAA;AAE9B,IAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAC7B,MAAA,KAAA,GAAQ,MAAM,KAAA,CAAM,KAAK,CAAA,CAAE,OAAA,CAAQ,cAAc,MAAM,CAAA;AAAA,IACzD;AAEA,IAAA,IAAI,YAAA,IAAgB,YAAA,CAAa,MAAA,GAAS,CAAA,EAAG;AAC3C,MAAA,KAAA,GAAQ,KAAA,CAAM,UAAA,CAAW,MAAA,EAAQ,YAAY,CAAA;AAAA,IAC/C;AAEA,IAAA,MAAM,OAAO,MAAM,KAAA;AAEnB,IAAA,MAAM,YAAA,GAAoC,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,MAAQ;AAAA,MACzD,MAAM,GAAA,CAAI,IAAA;AAAA,MACV,SAAS,GAAA,CAAI,OAAA;AAAA,MACb,IAAI,GAAA,CAAI,EAAA;AAAA,MACR,UAAU,GAAA,CAAI;AAAA,KAChB,CAAE,CAAA;AAEF,IAAA,OAAO,YAAA;AAAA,EACT;AAAA,EAEA,MAAM,cAAA,CACJ,QAAA,EACA,OAAA,EACA,cAAA,EACe;AACf,IAAA,MAAM,IAAA,GAAO,QAAA,CAAS,GAAA,CAAI,CAAA,GAAA,MAAQ;AAAA,MAChC,IAAI,GAAA,CAAI,EAAA;AAAA,MACR,eAAA,EAAiB,cAAA;AAAA,MACjB,MAAM,GAAA,CAAI,IAAA;AAAA,MACV,SAAS,GAAA,CAAI,OAAA;AAAA,MACb,UAAU,GAAA,CAAI,QAAA;AAAA,MACd,OAAA;AAAA,MACA,UAAA,EAAY,IAAA,CAAK,MAAA,CAAO,EAAA,CAAG,GAAA;AAAI,KACjC,CAAE,CAAA;AAEF,IAAA,MAAM,IAAA,CAAK,YAAA,EAAa,CAAE,MAAA,CAAO,IAAI,CAAA;AAAA,EACvC;AAAA,EAEA,MAAM,cAAc,OAAA,EAA4B;AAC9C,IAAA,MAAM,IAAA,CAAK,YAAA,EAAa,CAAE,KAAA,CAAM,EAAE,IAAI,OAAA,CAAQ,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO;AAAA,MACzD,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAS,OAAA,CAAQ,OAAA;AAAA,MACjB,UAAU,OAAA,CAAQ;AAAA,KACnB,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,eAAA,CACJ,cAAA,EACA,OAAA,EACuB;AACvB,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,iBAAA,EAAkB,CACtC,KAAA,CAAM,EAAE,EAAA,EAAI,cAAA,EAAgB,OAAA,EAAS,CAAA,CACrC,KAAA,EAAM;AAET,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,MAAM,IAAI,MAAM,wBAAwB,CAAA;AAAA,IAC1C;AAEA,IAAA,MAAM,YAAA,GAA6B;AAAA,MACjC,IAAI,GAAA,CAAI,EAAA;AAAA,MACR,OAAO,GAAA,CAAI,KAAA;AAAA,MACX,SAAS,GAAA,CAAI;AAAA,KACf;AAEA,IAAA,OAAO,YAAA;AAAA,EACT;AAAA,EAEA,MAAM,mBAAmB,YAAA,EAA4B;AACnD,IAAA,MAAM,IAAA,CAAK,iBAAA,EAAkB,CAAE,MAAA,CAAO;AAAA,MACpC,IAAI,YAAA,CAAa,EAAA;AAAA,MACjB,OAAO,YAAA,CAAa,KAAA;AAAA,MACpB,SAAS,YAAA,CAAa;AAAA,KACvB,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,mBAAmB,YAAA,EAA4B;AACnD,IAAA,MAAM,IAAA,CAAK,iBAAA,EAAkB,CAAE,KAAA,CAAM,EAAE,IAAI,YAAA,CAAa,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO;AAAA,MACnE,OAAO,YAAA,CAAa,KAAA;AAAA,MACpB,SAAS,YAAA,CAAa;AAAA,KACvB,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,iBAAiB,OAAA,EAA0C;AAC/D,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,iBAAA,GACrB,KAAA,CAAM,EAAE,OAAA,EAAS,EACjB,MAAA,CAAO,GAAG,CAAA,CACV,OAAA,CAAQ,cAAc,MAAM,CAAA;AAE/B,IAAA,MAAM,aAAA,GAAgC,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,MAAQ;AAAA,MACrD,IAAI,GAAA,CAAI,EAAA;AAAA,MACR,OAAO,GAAA,CAAI,KAAA;AAAA,MACX,SAAS,GAAA,CAAI;AAAA,KACf,CAAE,CAAA;AAEF,IAAA,OAAO,aAAA;AAAA,EACT;AACF;;;;"}
@@ -0,0 +1,16 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+
5
+ async function applyDatabaseMigrations(knex) {
6
+ const migrationsDir = backendPluginApi.resolvePackagePath(
7
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend",
8
+ "migrations"
9
+ );
10
+ await knex.migrate.latest({
11
+ directory: migrationsDir
12
+ });
13
+ }
14
+
15
+ exports.applyDatabaseMigrations = applyDatabaseMigrations;
16
+ //# sourceMappingURL=migrations.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.cjs.js","sources":["../../src/database/migrations.ts"],"sourcesContent":["import { resolvePackagePath } from '@backstage/backend-plugin-api';\nimport { Knex } from 'knex';\n\nexport async function applyDatabaseMigrations(knex: Knex): Promise<void> {\n const migrationsDir = resolvePackagePath(\n '@sweetoburrito/backstage-plugin-ai-assistant-backend',\n 'migrations',\n );\n\n await knex.migrate.latest({\n directory: migrationsDir,\n });\n}\n"],"names":["resolvePackagePath"],"mappings":";;;;AAGA,eAAsB,wBAAwB,IAAA,EAA2B;AACvE,EAAA,MAAM,aAAA,GAAgBA,mCAAA;AAAA,IACpB,sDAAA;AAAA,IACA;AAAA,GACF;AAEA,EAAA,MAAM,IAAA,CAAK,QAAQ,MAAA,CAAO;AAAA,IACxB,SAAA,EAAW;AAAA,GACZ,CAAA;AACH;;;;"}
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ class PgVectorStore {
4
+ /**
5
+ * Creates an instance of PgVectorStore.
6
+ * @param client - The Knex client to interact with the PostgreSQL database.
7
+ * @param [amount=4] - The number of embeddings to store.
8
+ * @param [chunkSize=500] - The size of each chunk of embeddings.
9
+ */
10
+ constructor(client, logger, amount = 4, chunkSize = 500) {
11
+ this.client = client;
12
+ this.logger = logger;
13
+ this.amount = amount;
14
+ this.chunkSize = chunkSize;
15
+ }
16
+ tableName = "embeddings";
17
+ embeddings;
18
+ static async fromConfig({ config, database, logger }) {
19
+ const client = await database.getClient();
20
+ const chunkSize = config.getOptionalNumber(
21
+ "aiAssistant.storage.pgVector.chunkSize"
22
+ );
23
+ const amount = config.getOptionalNumber(
24
+ "aiAssistant.storage.pgVector.amount"
25
+ );
26
+ return new PgVectorStore(client, logger, amount, chunkSize);
27
+ }
28
+ connectEmbeddings(embeddings) {
29
+ if (this.embeddings) {
30
+ this.logger.warn("Embeddings already connected, overwriting.");
31
+ }
32
+ this.embeddings = embeddings;
33
+ }
34
+ table() {
35
+ return this.client(this.tableName);
36
+ }
37
+ /**
38
+ * Add documents to the vector store.
39
+ *
40
+ * @param {EmbeddingDocument[]} documents - The array of documents to be added.
41
+ * @throws {Error} When no embeddings are configured for the vector store.
42
+ * @returns {Promise<void>} Resolves when the documents have been added successfully.
43
+ */
44
+ async addDocuments(documents) {
45
+ if (documents.length === 0) {
46
+ return;
47
+ }
48
+ const texts = documents.map(({ content }) => content);
49
+ if (!this.embeddings) {
50
+ throw new Error("No Embeddings configured for the vector store.");
51
+ }
52
+ const vectors = await this.embeddings.embedDocuments(texts);
53
+ this.logger.info(
54
+ `Received ${vectors.length} vectors from embeddings creation.`
55
+ );
56
+ this.addVectors(vectors, documents);
57
+ }
58
+ /**
59
+ * Adds vectors to the database along with corresponding documents.
60
+ *
61
+ * @param {number[][]} vectors - The vectors to be added.
62
+ * @param {EmbeddingDoc[]} documents - The corresponding documents.
63
+ * @return {Promise<void>} - A promise that resolves when the vectors are added successfully.
64
+ * @throws {Error} - If there is an error inserting the vectors.
65
+ */
66
+ async addVectors(vectors, documents) {
67
+ try {
68
+ const rows = [];
69
+ for (let i = 0; i < vectors.length; i += 1) {
70
+ const embedding = vectors[i];
71
+ const embeddingString = `[${embedding.join(",")}]`;
72
+ const values = {
73
+ content: documents[i].content.replace(/\0/g, ""),
74
+ vector: embeddingString.replace(/\0/g, ""),
75
+ metadata: documents[i].metadata
76
+ };
77
+ rows.push(values);
78
+ }
79
+ await this.client.batchInsert(this.tableName, rows, this.chunkSize);
80
+ } catch (e) {
81
+ this.logger.error(e.message);
82
+ throw new Error(`Error inserting: ${e.message}`);
83
+ }
84
+ }
85
+ /**
86
+ * Deletes records from the database table by their ids.
87
+ *
88
+ * @param {string[]} ids - The array of ids of the records to be deleted.
89
+ * @returns {Promise<void>} - A promise that resolves when the deletion is complete.
90
+ */
91
+ async deleteById(ids) {
92
+ await this.table().delete().whereIn("id", ids);
93
+ }
94
+ /**
95
+ * Deletes rows from the table based on the specified filter.
96
+ *
97
+ * @param {EmbeddingDocMetadata} filter - The filter to apply for deletion.
98
+ * @returns {Promise} - A Promise that resolves when the deletion is complete.
99
+ */
100
+ async deleteByFilter(filter) {
101
+ const queryString = `
102
+ DELETE FROM ${this.tableName}
103
+ WHERE metadata::jsonb @> :filter
104
+ `;
105
+ return this.client.raw(queryString, { filter });
106
+ }
107
+ /**
108
+ * Deletes documents based on the provided deletion parameters.
109
+ * Either `ids` or `filter` must be specified.
110
+ *
111
+ * @param {Object} deletionParams - The deletion parameters.
112
+ * @param {Array<string>} [deletionParams.ids] - The document IDs to delete.
113
+ * @param {EmbeddingDocMetadata} [deletionParams.filter] - The filter to match documents to be deleted.
114
+ *
115
+ * @return {Promise<void>} - A Promise that resolves once the documents have been deleted.
116
+ */
117
+ async deleteDocuments(deletionParams) {
118
+ const { ids, filter } = deletionParams;
119
+ if (!(ids || filter)) {
120
+ throw new Error(
121
+ "You must specify either ids or a filter when deleting documents."
122
+ );
123
+ }
124
+ if (ids && filter) {
125
+ throw new Error(
126
+ "You cannot specify both ids and a filter when deleting documents."
127
+ );
128
+ }
129
+ if (ids) {
130
+ await this.deleteById(ids);
131
+ } else if (filter) {
132
+ await this.deleteByFilter(filter);
133
+ }
134
+ }
135
+ /**
136
+ * Finds the most similar documents to a given query vector, along with their similarity scores.
137
+ *
138
+ * @param {number[]} query - The query vector to compare against.
139
+ * @param {number} amount - The maximum number of results to return.
140
+ * @param {EmbeddingDocumentMetadata} [filter] - Optional filter to limit the search results.
141
+ * @returns {Promise<[EmbeddingDocument, number][]>} - An array of document similarity results, where each
142
+ * result is a tuple containing the document and its similarity score.
143
+ */
144
+ async similaritySearchVectorWithScore(query, amount, filter) {
145
+ const embeddingString = `[${query.join(",")}]`;
146
+ const queryString = `
147
+ SELECT *, vector <=> :embeddingString as "_distance"
148
+ FROM ${this.tableName}
149
+ WHERE metadata::jsonb @> :filter
150
+ ORDER BY "_distance" ASC
151
+ LIMIT :amount
152
+ `;
153
+ const documents = (await this.client.raw(queryString, {
154
+ embeddingString,
155
+ filter: JSON.stringify(filter ?? {}),
156
+ amount
157
+ })).rows;
158
+ const results = [];
159
+ for (const doc of documents) {
160
+ if (doc._distance !== null && doc.content !== null) {
161
+ const document = {
162
+ content: doc.content,
163
+ metadata: doc.metadata
164
+ };
165
+ results.push([document, doc._distance]);
166
+ }
167
+ }
168
+ return results;
169
+ }
170
+ /**
171
+ * Performs a similarity search using the given query and filter.
172
+ *
173
+ * @param {string} query - The query to perform the similarity search on.
174
+ * @param {EmbeddingDocMetadata} filter - The filter to apply to the search results.
175
+ * @param {number} [amount=4] - The number of results to return.
176
+ * @return {Promise<EmbeddingDoc[]>} - A promise that resolves to an array of RoadieEmbeddingDoc objects representing the search results.
177
+ * @throws {Error} - Throws an error if there are no embeddings configured for the vector store.
178
+ */
179
+ async similaritySearch(query, filter, amount = this.amount) {
180
+ if (!this.embeddings) {
181
+ throw new Error("No Embeddings configured for the vector store.");
182
+ }
183
+ const results = await this.similaritySearchVectorWithScore(
184
+ await this.embeddings.embedQuery(query),
185
+ amount,
186
+ filter
187
+ );
188
+ return results.map((result) => result[0]);
189
+ }
190
+ }
191
+
192
+ exports.PgVectorStore = PgVectorStore;
193
+ //# sourceMappingURL=pg-vector-store.cjs.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,10 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var plugin = require('./plugin.cjs.js');
6
+
7
+
8
+
9
+ exports.default = plugin.aiAssistantPlugin;
10
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
@@ -0,0 +1,10 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+
3
+ /**
4
+ * aiAssistantPlugin backend plugin
5
+ *
6
+ * @public
7
+ */
8
+ declare const aiAssistantPlugin: _backstage_backend_plugin_api.BackendFeature;
9
+
10
+ export { aiAssistantPlugin as default };
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var index = require('./services/router/index.cjs.js');
5
+ var backstagePluginAiAssistantNode = require('@sweetoburrito/backstage-plugin-ai-assistant-node');
6
+ var ingestor = require('./services/ingestor.cjs.js');
7
+ var chat = require('./services/chat.cjs.js');
8
+ var migrations = require('./database/migrations.cjs.js');
9
+ var pgVectorStore = require('./database/pg-vector-store.cjs.js');
10
+ var pluginSignalsNode = require('@backstage/plugin-signals-node');
11
+ var searchKnowledge = require('./services/tools/searchKnowledge.cjs.js');
12
+ var pluginCatalogNode = require('@backstage/plugin-catalog-node');
13
+ var langfuse = require('./services/langfuse.cjs.js');
14
+
15
+ const aiAssistantPlugin = backendPluginApi.createBackendPlugin({
16
+ pluginId: "ai-assistant",
17
+ register(env) {
18
+ const ingestors = [];
19
+ const models = [];
20
+ const tools = [];
21
+ let embeddingsProvider;
22
+ env.registerExtensionPoint(backstagePluginAiAssistantNode.dataIngestorExtensionPoint, {
23
+ registerIngestor: (ingestor) => {
24
+ const existingIngestor = ingestors.find((i) => i.id === ingestor.id);
25
+ if (existingIngestor) {
26
+ throw new Error(
27
+ `Ingestor with id ${ingestor.id} is already registered.`
28
+ );
29
+ }
30
+ ingestors.push(ingestor);
31
+ }
32
+ });
33
+ env.registerExtensionPoint(backstagePluginAiAssistantNode.embeddingsProviderExtensionPoint, {
34
+ register: (provider) => {
35
+ embeddingsProvider = provider;
36
+ }
37
+ });
38
+ env.registerExtensionPoint(backstagePluginAiAssistantNode.modelProviderExtensionPoint, {
39
+ register: (model) => {
40
+ const existingModel = models.find((m) => m.id === model.id);
41
+ if (existingModel) {
42
+ throw new Error(`Model with id ${model.id} is already registered.`);
43
+ }
44
+ models.push(model);
45
+ }
46
+ });
47
+ env.registerExtensionPoint(backstagePluginAiAssistantNode.toolExtensionPoint, {
48
+ register: (tool) => {
49
+ const existingTool = tools.find((t) => t.name === tool.name);
50
+ if (existingTool) {
51
+ throw new Error(`Tool with name ${tool.name} is already registered.`);
52
+ }
53
+ tools.push(tool);
54
+ }
55
+ });
56
+ env.registerInit({
57
+ deps: {
58
+ httpRouter: backendPluginApi.coreServices.httpRouter,
59
+ database: backendPluginApi.coreServices.database,
60
+ logger: backendPluginApi.coreServices.logger,
61
+ config: backendPluginApi.coreServices.rootConfig,
62
+ scheduler: backendPluginApi.coreServices.scheduler,
63
+ httpAuth: backendPluginApi.coreServices.httpAuth,
64
+ userInfo: backendPluginApi.coreServices.userInfo,
65
+ signals: pluginSignalsNode.signalsServiceRef,
66
+ catalog: pluginCatalogNode.catalogServiceRef,
67
+ cache: backendPluginApi.coreServices.cache,
68
+ auth: backendPluginApi.coreServices.auth
69
+ },
70
+ async init(options) {
71
+ const { httpRouter, database, config, logger } = options;
72
+ const langfuseEnabled = langfuse.initLangfuse(config, logger);
73
+ const client = await database.getClient();
74
+ await migrations.applyDatabaseMigrations(client);
75
+ const vectorStore = await pgVectorStore.PgVectorStore.fromConfig(options);
76
+ if (!embeddingsProvider) {
77
+ throw new Error("No Embeddings Provider was registered.");
78
+ }
79
+ vectorStore.connectEmbeddings(await embeddingsProvider.getEmbeddings());
80
+ const dataIngestionPipeline = ingestor.createDataIngestionPipeline({
81
+ ...options,
82
+ vectorStore,
83
+ ingestors
84
+ });
85
+ const searchKnowledgeTool = searchKnowledge.createSearchKnowledgeTool({ vectorStore });
86
+ tools.push(searchKnowledgeTool);
87
+ const chat$1 = await chat.createChatService({
88
+ ...options,
89
+ models,
90
+ tools,
91
+ langfuseEnabled
92
+ });
93
+ httpRouter.use(await index.createRouter({ ...options, chat: chat$1 }));
94
+ dataIngestionPipeline.start();
95
+ }
96
+ });
97
+ }
98
+ });
99
+
100
+ exports.aiAssistantPlugin = aiAssistantPlugin;
101
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +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 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';\nimport { catalogServiceRef } from '@backstage/plugin-catalog-node';\nimport { initLangfuse } from './services/langfuse';\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 catalog: catalogServiceRef,\n cache: coreServices.cache,\n auth: coreServices.auth,\n },\n\n async init(options) {\n const { httpRouter, database, config, logger } = options;\n\n const langfuseEnabled = initLangfuse(config, logger);\n\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 langfuseEnabled,\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","catalogServiceRef","initLangfuse","applyDatabaseMigrations","PgVectorStore","createDataIngestionPipeline","createSearchKnowledgeTool","chat","createChatService","createRouter"],"mappings":"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"}