@sweetoburrito/backstage-plugin-ai-assistant-backend 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/config.d.ts CHANGED
@@ -8,6 +8,10 @@ export interface Config {
8
8
  prefix?: string;
9
9
  suffix?: string;
10
10
  };
11
+ conversation?: {
12
+ summaryModel?: string;
13
+ summaryPrompt?: string;
14
+ };
11
15
  storage?: {
12
16
  pgVector?: {
13
17
  /**
@@ -0,0 +1,19 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_SUMMARY_PROMPT = "Generate a short title for this conversation based on the messages. Return only the title. The title should be less than 25 characters.";
4
+ const DEFAULT_SYSTEM_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
+ Rules:
8
+ 1. Always base your answers on the provided context. Do not make up information.
9
+ 2. When relevant, cite or reference the source information provided in the context.
10
+ 3. Format answers clearly and concisely. Use bullet points for lists when appropriate.
11
+ 4. Maintain a professional, friendly, and helpful tone.
12
+ 5. Return only the relevant information without any filler or unnecessary details.
13
+ 6. If you don't know the answer, admit it and suggest ways to find the information.
14
+ 7. Always return a well-structured response using markdown.
15
+ `;
16
+
17
+ exports.DEFAULT_SUMMARY_PROMPT = DEFAULT_SUMMARY_PROMPT;
18
+ exports.DEFAULT_SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT;
19
+ //# 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 'Generate a short title for this conversation based on the messages. Return only the title. The title should be less than 25 characters.';\n\nexport const DEFAULT_SYSTEM_PROMPT = `\nYou are a helpful assistant that answers questions based on provided context from various documents. The context may come from sources such as internal wikis, code repositories, technical documentation, or other structured or unstructured data.\n\nRules:\n1. Always base your answers on the provided context. Do not make up information.\n2. When relevant, cite or reference the source information provided in the context.\n3. Format answers clearly and concisely. Use bullet points for lists when appropriate.\n4. Maintain a professional, friendly, and helpful tone.\n5. Return only the relevant information without any filler or unnecessary details.\n6. If you don't know the answer, admit it and suggest ways to find the information.\n7. Always return a well-structured response using markdown.\n`;\n"],"names":[],"mappings":";;AAAO,MAAM,sBAAA,GACX;AAEK,MAAM,qBAAA,GAAwB;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;;;"}
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const TABLE_NAME = "conversation";
3
+ const MESSAGE_TABLE_NAME = "message";
4
+ const CONVERSATION_TABLE_NAME = "conversation";
4
5
  class ChatStore {
5
6
  /**
6
7
  * Creates an instance of ChatStore.
@@ -13,11 +14,14 @@ class ChatStore {
13
14
  const client = await database.getClient();
14
15
  return new ChatStore(client);
15
16
  }
16
- table() {
17
- return this.client(TABLE_NAME);
17
+ messageTable() {
18
+ return this.client(MESSAGE_TABLE_NAME);
19
+ }
20
+ conversationTable() {
21
+ return this.client(CONVERSATION_TABLE_NAME);
18
22
  }
19
23
  async getChatMessages(conversationId, userRef, limit) {
20
- let query = this.table().where({ conversation_id: conversationId, userRef }).select("*").orderBy("created_at", "asc");
24
+ let query = this.messageTable().where({ conversation_id: conversationId, userRef }).select("*").orderBy("created_at", "asc");
21
25
  if (typeof limit === "number") {
22
26
  query = query.limit(limit).orderBy("created_at", "desc");
23
27
  }
@@ -38,14 +42,52 @@ class ChatStore {
38
42
  userRef,
39
43
  created_at: this.client.fn.now()
40
44
  }));
41
- await this.table().insert(rows);
45
+ await this.messageTable().insert(rows);
42
46
  }
43
47
  async updateMessage(message) {
44
- await this.table().where({ id: message.id }).update({
48
+ await this.messageTable().where({ id: message.id }).update({
45
49
  role: message.role,
46
50
  content: message.content
47
51
  });
48
52
  }
53
+ async getConversationSize(conversationId) {
54
+ const count = await this.messageTable().where({ conversation_id: conversationId }).count("* as count").first();
55
+ return count?.count ? Number(count.count) : 0;
56
+ }
57
+ async getConversation(conversationId, userRef) {
58
+ const row = await this.conversationTable().where({ id: conversationId, userRef }).first();
59
+ if (!row) {
60
+ throw new Error("Conversation not found");
61
+ }
62
+ const conversation = {
63
+ id: row.id,
64
+ title: row.title,
65
+ userRef: row.userRef
66
+ };
67
+ return conversation;
68
+ }
69
+ async createConversation(conversation) {
70
+ await this.conversationTable().insert({
71
+ id: conversation.id,
72
+ title: conversation.title,
73
+ userRef: conversation.userRef
74
+ });
75
+ }
76
+ async updateConversation(conversation) {
77
+ await this.conversationTable().where({ id: conversation.id }).update({
78
+ title: conversation.title,
79
+ userRef: conversation.userRef
80
+ });
81
+ }
82
+ async getConversations(userRef) {
83
+ const rows = await this.conversationTable().where({ userRef }).select("*").orderBy("created_at", "desc");
84
+ const conversations = rows.map((row) => ({
85
+ id: row.id,
86
+ title: row.title,
87
+ userRef: row.userRef
88
+ }));
89
+ return conversations;
90
+ }
49
91
  }
50
92
 
51
93
  exports.ChatStore = ChatStore;
@@ -1 +1 @@
1
- {"version":3,"file":"chat-store.cjs.js","sources":["../../src/database/chat-store.ts"],"sourcesContent":["import { DatabaseService } from '@backstage/backend-plugin-api';\nimport { Message } from '@sweetoburrito/backstage-plugin-ai-assistant-common';\n\nimport { Knex } from 'knex';\n\nconst 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 table() {\n return this.client(TABLE_NAME);\n }\n\n async getChatMessages(\n conversationId: string,\n userRef: string,\n limit?: number,\n ): Promise<Required<Message>[]> {\n let query = this.table()\n .where({ conversation_id: conversationId, userRef })\n .select('*')\n .orderBy('created_at', 'asc');\n\n if (typeof limit === 'number') {\n query = query.limit(limit).orderBy('created_at', 'desc');\n }\n\n const rows = await query;\n\n const chatMessages: Required<Message>[] = rows.map(row => ({\n role: row.role,\n content: row.content,\n id: row.id,\n }));\n\n return chatMessages;\n }\n\n async addChatMessage(\n messages: Message[],\n userRef: string,\n conversationId: string,\n ): Promise<void> {\n const rows = messages.map(msg => ({\n id: msg.id,\n conversation_id: conversationId,\n role: msg.role,\n content: msg.content,\n userRef,\n created_at: this.client.fn.now(),\n }));\n\n await this.table().insert(rows);\n }\n\n async updateMessage(message: Required<Message>) {\n await this.table().where({ id: message.id }).update({\n role: message.role,\n content: message.content,\n });\n }\n}\n"],"names":[],"mappings":";;AAKA,MAAM,UAAA,GAAa,cAAA;AAMZ,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,KAAA,GAAQ;AACN,IAAA,OAAO,IAAA,CAAK,OAAO,UAAU,CAAA;AAAA,EAC/B;AAAA,EAEA,MAAM,eAAA,CACJ,cAAA,EACA,OAAA,EACA,KAAA,EAC8B;AAC9B,IAAA,IAAI,QAAQ,IAAA,CAAK,KAAA,EAAM,CACpB,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,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;AAAA,KACV,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,OAAA;AAAA,MACA,UAAA,EAAY,IAAA,CAAK,MAAA,CAAO,EAAA,CAAG,GAAA;AAAI,KACjC,CAAE,CAAA;AAEF,IAAA,MAAM,IAAA,CAAK,KAAA,EAAM,CAAE,MAAA,CAAO,IAAI,CAAA;AAAA,EAChC;AAAA,EAEA,MAAM,cAAc,OAAA,EAA4B;AAC9C,IAAA,MAAM,IAAA,CAAK,KAAA,EAAM,CAAE,KAAA,CAAM,EAAE,IAAI,OAAA,CAAQ,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO;AAAA,MAClD,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAS,OAAA,CAAQ;AAAA,KAClB,CAAA;AAAA,EACH;AACF;;;;"}
1
+ {"version":3,"file":"chat-store.cjs.js","sources":["../../src/database/chat-store.ts"],"sourcesContent":["import { DatabaseService } from '@backstage/backend-plugin-api';\nimport {\n Message,\n Conversation,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\n\nimport { Knex } from 'knex';\n\nconst MESSAGE_TABLE_NAME = 'message';\nconst CONVERSATION_TABLE_NAME = 'conversation';\n\nexport type ChatStoreOptions = {\n database: DatabaseService;\n};\n\nexport class ChatStore {\n /**\n * Creates an instance of ChatStore.\n * @param client - The Knex client to interact with the PostgreSQL database.\n */\n constructor(private readonly client: Knex) {}\n\n static async fromConfig({ database }: ChatStoreOptions) {\n const client = await database.getClient();\n return new ChatStore(client);\n }\n\n messageTable() {\n return this.client(MESSAGE_TABLE_NAME);\n }\n\n conversationTable() {\n return this.client(CONVERSATION_TABLE_NAME);\n }\n\n async getChatMessages(\n conversationId: string,\n userRef: string,\n limit?: number,\n ): Promise<Required<Message>[]> {\n let query = this.messageTable()\n .where({ conversation_id: conversationId, userRef })\n .select('*')\n .orderBy('created_at', 'asc');\n\n if (typeof limit === 'number') {\n query = query.limit(limit).orderBy('created_at', 'desc');\n }\n\n const rows = await query;\n\n const chatMessages: Required<Message>[] = rows.map(row => ({\n role: row.role,\n content: row.content,\n id: row.id,\n }));\n\n return chatMessages;\n }\n\n async addChatMessage(\n messages: Message[],\n userRef: string,\n conversationId: string,\n ): Promise<void> {\n const rows = messages.map(msg => ({\n id: msg.id,\n conversation_id: conversationId,\n role: msg.role,\n content: msg.content,\n userRef,\n created_at: this.client.fn.now(),\n }));\n\n await this.messageTable().insert(rows);\n }\n\n async updateMessage(message: Required<Message>) {\n await this.messageTable().where({ id: message.id }).update({\n role: message.role,\n content: message.content,\n });\n }\n\n async getConversationSize(conversationId: string) {\n const count = await this.messageTable()\n .where({ conversation_id: conversationId })\n .count('* as count')\n .first();\n\n return count?.count ? Number(count.count) : 0;\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,KAAA,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,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;AAAA,KACV,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,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;AAAA,KAClB,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,oBAAoB,cAAA,EAAwB;AAChD,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,YAAA,GACtB,KAAA,CAAM,EAAE,eAAA,EAAiB,cAAA,EAAgB,CAAA,CACzC,KAAA,CAAM,YAAY,EAClB,KAAA,EAAM;AAET,IAAA,OAAO,KAAA,EAAO,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,KAAK,CAAA,GAAI,CAAA;AAAA,EAC9C;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;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"pg-vector-store.cjs.js","sources":["../../src/database/pg-vector-store.ts"],"sourcesContent":["import {\n DatabaseService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport {\n VectorStore,\n EmbeddingDocument,\n EmbeddingDocumentMetadata,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { Embeddings } from '@langchain/core/embeddings';\nimport { Knex } from 'knex';\n\nexport type PgVectorStoreOptions = {\n database: DatabaseService;\n logger: LoggerService;\n config: RootConfigService;\n};\n\nexport class PgVectorStore implements VectorStore {\n private readonly tableName: string = 'embeddings';\n private embeddings?: Embeddings;\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: Embeddings) {\n if (this.embeddings) {\n this.logger.warn('Embeddings already connected, overwriting.');\n }\n this.embeddings = embeddings;\n }\n\n table() {\n return this.client(this.tableName);\n }\n\n /**\n * Add documents to the vector store.\n *\n * @param {EmbeddingDocument[]} documents - The array of documents to be added.\n * @throws {Error} When no embeddings are configured for the vector store.\n * @returns {Promise<void>} Resolves when the documents have been added successfully.\n */\n async addDocuments(documents: EmbeddingDocument[]): Promise<void> {\n if (documents.length === 0) {\n return;\n }\n const texts = documents.map(({ content }) => content);\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n\n const vectors = await this.embeddings.embedDocuments(texts);\n this.logger.info(\n `Received ${vectors.length} vectors from embeddings creation.`,\n );\n this.addVectors(vectors, documents);\n }\n\n /**\n * Adds vectors to the database along with corresponding documents.\n *\n * @param {number[][]} vectors - The vectors to be added.\n * @param {EmbeddingDoc[]} documents - The corresponding documents.\n * @return {Promise<void>} - A promise that resolves when the vectors are added successfully.\n * @throws {Error} - If there is an error inserting the vectors.\n */\n private async addVectors(\n vectors: number[][],\n documents: EmbeddingDocument[],\n ): Promise<void> {\n try {\n const rows = [];\n for (let i = 0; i < vectors.length; i += 1) {\n const embedding = vectors[i];\n const embeddingString = `[${embedding.join(',')}]`;\n const values = {\n content: documents[i].content.replace(/\\0/g, ''),\n vector: embeddingString.replace(/\\0/g, ''),\n metadata: documents[i].metadata,\n };\n rows.push(values);\n }\n\n await this.client.batchInsert(this.tableName, rows, this.chunkSize);\n } catch (e) {\n this.logger.error((e as Error).message);\n throw new Error(`Error inserting: ${(e as Error).message}`);\n }\n }\n\n /**\n * Deletes records from the database table by their ids.\n *\n * @param {string[]} ids - The array of ids of the records to be deleted.\n * @returns {Promise<void>} - A promise that resolves when the deletion is complete.\n */\n private async deleteById(ids: string[]) {\n await this.table().delete().whereIn('id', ids);\n }\n\n /**\n * Deletes rows from the table based on the specified filter.\n *\n * @param {EmbeddingDocMetadata} filter - The filter to apply for deletion.\n * @returns {Promise} - A Promise that resolves when the deletion is complete.\n */\n private async deleteByFilter(filter: EmbeddingDocumentMetadata) {\n const queryString = `\n DELETE FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n `;\n return this.client.raw(queryString, { filter });\n }\n\n /**\n * Deletes documents based on the provided deletion parameters.\n * Either `ids` or `filter` must be specified.\n *\n * @param {Object} deletionParams - The deletion parameters.\n * @param {Array<string>} [deletionParams.ids] - The document IDs to delete.\n * @param {EmbeddingDocMetadata} [deletionParams.filter] - The filter to match documents to be deleted.\n *\n * @return {Promise<void>} - A Promise that resolves once the documents have been deleted.\n */\n async deleteDocuments(deletionParams: {\n ids?: string[];\n filter?: EmbeddingDocumentMetadata;\n }): Promise<void> {\n const { ids, filter } = deletionParams;\n\n if (!(ids || filter)) {\n throw new Error(\n 'You must specify either ids or a filter when deleting documents.',\n );\n }\n\n if (ids && filter) {\n throw new Error(\n 'You cannot specify both ids and a filter when deleting documents.',\n );\n }\n\n if (ids) {\n await this.deleteById(ids);\n } else if (filter) {\n await this.deleteByFilter(filter);\n }\n }\n\n /**\n * Finds the most similar documents to a given query vector, along with their similarity scores.\n *\n * @param {number[]} query - The query vector to compare against.\n * @param {number} amount - The maximum number of results to return.\n * @param {EmbeddingDocumentMetadata} [filter] - Optional filter to limit the search results.\n * @returns {Promise<[EmbeddingDocument, number][]>} - An array of document similarity results, where each\n * result is a tuple containing the document and its similarity score.\n */\n private async similaritySearchVectorWithScore(\n query: number[],\n amount: number,\n filter?: EmbeddingDocumentMetadata,\n ): Promise<[EmbeddingDocument, number][]> {\n const embeddingString = `[${query.join(',')}]`;\n const queryString = `\n SELECT *, vector <=> :embeddingString as \"_distance\"\n FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n ORDER BY \"_distance\" ASC\n LIMIT :amount\n `;\n\n const documents = (\n await this.client.raw(queryString, {\n embeddingString,\n filter: JSON.stringify(filter ?? {}),\n amount,\n })\n ).rows;\n\n const results = [] as [EmbeddingDocument, number][];\n for (const doc of documents) {\n // eslint-ignore-next-line\n if (doc._distance !== null && doc.content !== null) {\n const document = {\n content: doc.content,\n metadata: doc.metadata,\n };\n results.push([document, doc._distance]);\n }\n }\n return results;\n }\n\n /**\n * Performs a similarity search using the given query and filter.\n *\n * @param {string} query - The query to perform the similarity search on.\n * @param {EmbeddingDocMetadata} filter - The filter to apply to the search results.\n * @param {number} [amount=4] - The number of results to return.\n * @return {Promise<EmbeddingDoc[]>} - A promise that resolves to an array of RoadieEmbeddingDoc objects representing the search results.\n * @throws {Error} - Throws an error if there are no embeddings configured for the vector store.\n */\n async similaritySearch(\n query: string,\n filter: EmbeddingDocumentMetadata,\n amount: number = this.amount,\n ): Promise<EmbeddingDocument[]> {\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n const results = await this.similaritySearchVectorWithScore(\n await this.embeddings.embedQuery(query),\n amount,\n filter,\n );\n\n return results.map(result => result[0]);\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pg-vector-store.cjs.js","sources":["../../src/database/pg-vector-store.ts"],"sourcesContent":["import {\n DatabaseService,\n LoggerService,\n RootConfigService,\n} from '@backstage/backend-plugin-api';\nimport {\n VectorStore,\n EmbeddingDocument,\n EmbeddingDocumentMetadata,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport { Embeddings } from '@langchain/core/embeddings';\nimport { Knex } from 'knex';\n\nexport type PgVectorStoreOptions = {\n database: DatabaseService;\n logger: LoggerService;\n config: RootConfigService;\n};\n\nexport class PgVectorStore implements VectorStore {\n private readonly tableName: string = 'embeddings';\n private embeddings?: Omit<Embeddings, 'caller'>;\n\n /**\n * Creates an instance of PgVectorStore.\n * @param client - The Knex client to interact with the PostgreSQL database.\n * @param [amount=4] - The number of embeddings to store.\n * @param [chunkSize=500] - The size of each chunk of embeddings.\n */\n constructor(\n private readonly client: Knex,\n private readonly logger: LoggerService,\n private readonly amount: number = 4,\n private readonly chunkSize: number = 500,\n ) {}\n\n static async fromConfig({ config, database, logger }: PgVectorStoreOptions) {\n const client = await database.getClient();\n const chunkSize = config.getOptionalNumber(\n 'aiAssistant.storage.pgVector.chunkSize',\n );\n const amount = config.getOptionalNumber(\n 'aiAssistant.storage.pgVector.amount',\n );\n\n return new PgVectorStore(client, logger, amount, chunkSize);\n }\n\n connectEmbeddings(embeddings: Omit<Embeddings, 'caller'>) {\n if (this.embeddings) {\n this.logger.warn('Embeddings already connected, overwriting.');\n }\n this.embeddings = embeddings;\n }\n\n table() {\n return this.client(this.tableName);\n }\n\n /**\n * Add documents to the vector store.\n *\n * @param {EmbeddingDocument[]} documents - The array of documents to be added.\n * @throws {Error} When no embeddings are configured for the vector store.\n * @returns {Promise<void>} Resolves when the documents have been added successfully.\n */\n async addDocuments(documents: EmbeddingDocument[]): Promise<void> {\n if (documents.length === 0) {\n return;\n }\n const texts = documents.map(({ content }) => content);\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n\n const vectors = await this.embeddings.embedDocuments(texts);\n this.logger.info(\n `Received ${vectors.length} vectors from embeddings creation.`,\n );\n this.addVectors(vectors, documents);\n }\n\n /**\n * Adds vectors to the database along with corresponding documents.\n *\n * @param {number[][]} vectors - The vectors to be added.\n * @param {EmbeddingDoc[]} documents - The corresponding documents.\n * @return {Promise<void>} - A promise that resolves when the vectors are added successfully.\n * @throws {Error} - If there is an error inserting the vectors.\n */\n private async addVectors(\n vectors: number[][],\n documents: EmbeddingDocument[],\n ): Promise<void> {\n try {\n const rows = [];\n for (let i = 0; i < vectors.length; i += 1) {\n const embedding = vectors[i];\n const embeddingString = `[${embedding.join(',')}]`;\n const values = {\n content: documents[i].content.replace(/\\0/g, ''),\n vector: embeddingString.replace(/\\0/g, ''),\n metadata: documents[i].metadata,\n };\n rows.push(values);\n }\n\n await this.client.batchInsert(this.tableName, rows, this.chunkSize);\n } catch (e) {\n this.logger.error((e as Error).message);\n throw new Error(`Error inserting: ${(e as Error).message}`);\n }\n }\n\n /**\n * Deletes records from the database table by their ids.\n *\n * @param {string[]} ids - The array of ids of the records to be deleted.\n * @returns {Promise<void>} - A promise that resolves when the deletion is complete.\n */\n private async deleteById(ids: string[]) {\n await this.table().delete().whereIn('id', ids);\n }\n\n /**\n * Deletes rows from the table based on the specified filter.\n *\n * @param {EmbeddingDocMetadata} filter - The filter to apply for deletion.\n * @returns {Promise} - A Promise that resolves when the deletion is complete.\n */\n private async deleteByFilter(filter: EmbeddingDocumentMetadata) {\n const queryString = `\n DELETE FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n `;\n return this.client.raw(queryString, { filter });\n }\n\n /**\n * Deletes documents based on the provided deletion parameters.\n * Either `ids` or `filter` must be specified.\n *\n * @param {Object} deletionParams - The deletion parameters.\n * @param {Array<string>} [deletionParams.ids] - The document IDs to delete.\n * @param {EmbeddingDocMetadata} [deletionParams.filter] - The filter to match documents to be deleted.\n *\n * @return {Promise<void>} - A Promise that resolves once the documents have been deleted.\n */\n async deleteDocuments(deletionParams: {\n ids?: string[];\n filter?: EmbeddingDocumentMetadata;\n }): Promise<void> {\n const { ids, filter } = deletionParams;\n\n if (!(ids || filter)) {\n throw new Error(\n 'You must specify either ids or a filter when deleting documents.',\n );\n }\n\n if (ids && filter) {\n throw new Error(\n 'You cannot specify both ids and a filter when deleting documents.',\n );\n }\n\n if (ids) {\n await this.deleteById(ids);\n } else if (filter) {\n await this.deleteByFilter(filter);\n }\n }\n\n /**\n * Finds the most similar documents to a given query vector, along with their similarity scores.\n *\n * @param {number[]} query - The query vector to compare against.\n * @param {number} amount - The maximum number of results to return.\n * @param {EmbeddingDocumentMetadata} [filter] - Optional filter to limit the search results.\n * @returns {Promise<[EmbeddingDocument, number][]>} - An array of document similarity results, where each\n * result is a tuple containing the document and its similarity score.\n */\n private async similaritySearchVectorWithScore(\n query: number[],\n amount: number,\n filter?: EmbeddingDocumentMetadata,\n ): Promise<[EmbeddingDocument, number][]> {\n const embeddingString = `[${query.join(',')}]`;\n const queryString = `\n SELECT *, vector <=> :embeddingString as \"_distance\"\n FROM ${this.tableName}\n WHERE metadata::jsonb @> :filter\n ORDER BY \"_distance\" ASC\n LIMIT :amount\n `;\n\n const documents = (\n await this.client.raw(queryString, {\n embeddingString,\n filter: JSON.stringify(filter ?? {}),\n amount,\n })\n ).rows;\n\n const results = [] as [EmbeddingDocument, number][];\n for (const doc of documents) {\n // eslint-ignore-next-line\n if (doc._distance !== null && doc.content !== null) {\n const document = {\n content: doc.content,\n metadata: doc.metadata,\n };\n results.push([document, doc._distance]);\n }\n }\n return results;\n }\n\n /**\n * Performs a similarity search using the given query and filter.\n *\n * @param {string} query - The query to perform the similarity search on.\n * @param {EmbeddingDocMetadata} filter - The filter to apply to the search results.\n * @param {number} [amount=4] - The number of results to return.\n * @return {Promise<EmbeddingDoc[]>} - A promise that resolves to an array of RoadieEmbeddingDoc objects representing the search results.\n * @throws {Error} - Throws an error if there are no embeddings configured for the vector store.\n */\n async similaritySearch(\n query: string,\n filter: EmbeddingDocumentMetadata,\n amount: number = this.amount,\n ): Promise<EmbeddingDocument[]> {\n if (!this.embeddings) {\n throw new Error('No Embeddings configured for the vector store.');\n }\n const results = await this.similaritySearchVectorWithScore(\n await this.embeddings.embedQuery(query),\n amount,\n filter,\n );\n\n return results.map(result => result[0]);\n }\n}\n"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@
2
2
 
3
3
  var uuid = require('uuid');
4
4
  var chatStore = require('../database/chat-store.cjs.js');
5
+ var prompts = require('../constants/prompts.cjs.js');
5
6
 
6
7
  const createChatService = async ({
7
8
  models,
@@ -9,7 +10,8 @@ const createChatService = async ({
9
10
  vectorStore,
10
11
  promptBuilder,
11
12
  database,
12
- signals
13
+ signals,
14
+ config
13
15
  }) => {
14
16
  logger.info(`Available models: ${models.map((m) => m.id).join(", ")}`);
15
17
  const chatStore$1 = await chatStore.ChatStore.fromConfig({ database });
@@ -45,6 +47,53 @@ const createChatService = async ({
45
47
  });
46
48
  }
47
49
  };
50
+ const addChatMessage = async (messages, userRef, conversationId) => {
51
+ const conversationSize = await chatStore$1.getConversationSize(
52
+ conversationId
53
+ );
54
+ if (conversationSize < 1) {
55
+ await chatStore$1.createConversation({
56
+ id: conversationId,
57
+ userRef,
58
+ title: "New Conversation"
59
+ });
60
+ await chatStore$1.addChatMessage(messages, userRef, conversationId);
61
+ return;
62
+ }
63
+ const conversation = await chatStore$1.getConversation(
64
+ conversationId,
65
+ userRef
66
+ );
67
+ if (conversationSize + messages.length < 5) {
68
+ await chatStore$1.addChatMessage(messages, userRef, conversationId);
69
+ return;
70
+ }
71
+ if (conversation.title !== "New Conversation") {
72
+ await chatStore$1.addChatMessage(messages, userRef, conversationId);
73
+ return;
74
+ }
75
+ const summaryModelId = config.getOptionalString("aiAssistant.conversation.summaryModel") ?? models[0].id;
76
+ const summaryModel = getChatModelById(summaryModelId);
77
+ if (!summaryModel) {
78
+ throw new Error(`Model with id ${summaryModelId} not found`);
79
+ }
80
+ const conversationMessages = await chatStore$1.getChatMessages(
81
+ conversationId,
82
+ userRef,
83
+ 5
84
+ );
85
+ const summaryPrompt = config.getOptionalString("aiAssistant.conversation.summaryPrompt") ?? prompts.DEFAULT_SUMMARY_PROMPT;
86
+ const { text } = await summaryModel.invoke([
87
+ ...conversationMessages,
88
+ {
89
+ role: "system",
90
+ content: summaryPrompt
91
+ }
92
+ ]);
93
+ conversation.title = text.trim();
94
+ await chatStore$1.updateConversation(conversation);
95
+ await chatStore$1.addChatMessage(messages, userRef, conversationId);
96
+ };
48
97
  const prompt = async ({
49
98
  conversationId,
50
99
  messages,
@@ -56,7 +105,7 @@ const createChatService = async ({
56
105
  if (!model) {
57
106
  throw new Error(`Model with id ${modelId} not found`);
58
107
  }
59
- chatStore$1.addChatMessage(messages, userEntityRef, conversationId);
108
+ await addChatMessage(messages, userEntityRef, conversationId);
60
109
  const context = await vectorStore.similaritySearch(
61
110
  messages.filter((m) => m.role === "user").map((m) => m.content).join("\n")
62
111
  );
@@ -75,7 +124,7 @@ const createChatService = async ({
75
124
  role: "assistant",
76
125
  content: ""
77
126
  };
78
- await chatStore$1.addChatMessage([aiMessage], userEntityRef, conversationId);
127
+ await addChatMessage([aiMessage], userEntityRef, conversationId);
79
128
  if (stream) {
80
129
  streamMessage({
81
130
  modelId,
@@ -101,10 +150,17 @@ const createChatService = async ({
101
150
  );
102
151
  return conversation;
103
152
  };
153
+ const getConversations = async ({
154
+ userEntityRef
155
+ }) => {
156
+ const conversations = await chatStore$1.getConversations(userEntityRef);
157
+ return conversations;
158
+ };
104
159
  return {
105
160
  prompt,
106
161
  getAvailableModels,
107
- getConversation
162
+ getConversation,
163
+ getConversations
108
164
  };
109
165
  };
110
166
 
@@ -1 +1 @@
1
- {"version":3,"file":"chat.cjs.js","sources":["../../src/services/chat.ts"],"sourcesContent":["import {\n Model,\n VectorStore,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport {\n LoggerService,\n RootConfigService,\n DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport { PromptBuilder } from './prompt';\nimport { v4 as uuid } from 'uuid';\nimport { ChatStore } from '../database/chat-store';\nimport { Message } from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { SignalsService } from '@backstage/plugin-signals-node';\n\nexport type ChatServiceOptions = {\n models: Model[];\n logger: LoggerService;\n vectorStore: VectorStore;\n config: RootConfigService;\n promptBuilder: PromptBuilder;\n database: DatabaseService;\n signals: SignalsService;\n};\n\ntype StreamOptions = {\n modelId: string;\n messages: Message[];\n messageId: string;\n userEntityRef: string;\n};\n\ntype PromptOptions = {\n modelId: string;\n messages: Message[];\n conversationId: string;\n stream?: boolean;\n userEntityRef: string;\n};\n\ntype GetConversationOptions = {\n conversationId: string;\n userEntityRef: string;\n};\n\nexport type ChatService = {\n prompt: (options: PromptOptions) => Promise<Required<Message>[]>;\n getAvailableModels: () => Promise<string[]>;\n getConversation: (\n options: GetConversationOptions,\n ) => Promise<Required<Message>[]>;\n};\n\nexport const createChatService = async ({\n models,\n logger,\n vectorStore,\n promptBuilder,\n database,\n signals,\n}: ChatServiceOptions): Promise<ChatService> => {\n logger.info(`Available models: ${models.map(m => m.id).join(', ')}`);\n\n const chatStore = await ChatStore.fromConfig({ database });\n\n const getChatModelById = (id: string) => {\n return models.find(model => model.id === id)?.chatModel;\n };\n\n const streamMessage = async ({\n modelId,\n messages,\n messageId,\n userEntityRef,\n }: StreamOptions) => {\n const model = getChatModelById(modelId);\n\n if (!model) {\n throw new Error(`Model with id ${modelId} not found`);\n }\n\n const promptStream = await model.stream(messages);\n\n const aiMessage: Required<Message> = {\n id: messageId,\n role: 'assistant',\n content: '',\n };\n\n for await (const chunk of promptStream) {\n aiMessage.content += chunk.content ?? '';\n\n chatStore.updateMessage(aiMessage);\n\n signals.publish({\n channel: `ai-assistant.chat.message-stream:${messageId}`,\n message: aiMessage,\n recipients: {\n type: 'user',\n entityRef: userEntityRef,\n },\n });\n }\n };\n\n const prompt: ChatService['prompt'] = async ({\n conversationId,\n messages,\n modelId,\n stream = true,\n userEntityRef,\n }: PromptOptions) => {\n const model = getChatModelById(modelId);\n\n if (!model) {\n throw new Error(`Model with id ${modelId} not found`);\n }\n\n chatStore.addChatMessage(messages, userEntityRef, conversationId);\n\n const context = await vectorStore.similaritySearch(\n messages\n .filter(m => m.role === 'user')\n .map(m => m.content)\n .join('\\n'),\n );\n\n const recentConversationMessages = await chatStore.getChatMessages(\n conversationId,\n userEntityRef,\n 10,\n );\n\n const promptMessages = promptBuilder.buildPrompt(\n [...recentConversationMessages, ...messages],\n context,\n );\n\n const messageId: string = uuid();\n\n const aiMessage: Required<Message> = {\n id: messageId,\n role: 'assistant',\n content: '',\n };\n\n await chatStore.addChatMessage([aiMessage], userEntityRef, conversationId);\n\n if (stream) {\n streamMessage({\n modelId,\n messages: promptMessages,\n messageId,\n userEntityRef,\n });\n\n return [aiMessage];\n }\n const { text } = await model.invoke(promptMessages);\n\n aiMessage.content = text;\n\n await chatStore.updateMessage(aiMessage);\n\n return [aiMessage];\n };\n\n const getAvailableModels: ChatService['getAvailableModels'] = async () => {\n return models.map(x => x.id);\n };\n\n const getConversation: ChatService['getConversation'] = async (\n options: GetConversationOptions,\n ) => {\n const { conversationId, userEntityRef } = options;\n\n const conversation = await chatStore.getChatMessages(\n conversationId,\n userEntityRef,\n );\n\n return conversation;\n };\n\n return {\n prompt,\n getAvailableModels,\n getConversation,\n };\n};\n"],"names":["chatStore","ChatStore","uuid"],"mappings":"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"}
1
+ {"version":3,"file":"chat.cjs.js","sources":["../../src/services/chat.ts"],"sourcesContent":["import {\n Model,\n VectorStore,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-node';\nimport {\n LoggerService,\n RootConfigService,\n DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport { PromptBuilder } from './prompt';\nimport { v4 as uuid } from 'uuid';\nimport { ChatStore } from '../database/chat-store';\nimport {\n Conversation,\n Message,\n} from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { SignalsService } from '@backstage/plugin-signals-node';\nimport { DEFAULT_SUMMARY_PROMPT } from '../constants/prompts';\n\nexport type ChatServiceOptions = {\n models: Model[];\n logger: LoggerService;\n vectorStore: VectorStore;\n config: RootConfigService;\n promptBuilder: PromptBuilder;\n database: DatabaseService;\n signals: SignalsService;\n};\n\ntype StreamOptions = {\n modelId: string;\n messages: Message[];\n messageId: string;\n userEntityRef: string;\n};\n\ntype PromptOptions = {\n modelId: string;\n messages: Message[];\n conversationId: string;\n stream?: boolean;\n userEntityRef: string;\n};\n\ntype GetConversationOptions = {\n conversationId: string;\n userEntityRef: string;\n};\n\ntype GetConversationsOptions = {\n userEntityRef: string;\n};\n\nexport type ChatService = {\n prompt: (options: PromptOptions) => Promise<Required<Message>[]>;\n getAvailableModels: () => Promise<string[]>;\n getConversation: (\n options: GetConversationOptions,\n ) => Promise<Required<Message>[]>;\n getConversations: (\n options: GetConversationsOptions,\n ) => Promise<Conversation[]>;\n};\n\nexport const createChatService = async ({\n models,\n logger,\n vectorStore,\n promptBuilder,\n database,\n signals,\n config,\n}: ChatServiceOptions): Promise<ChatService> => {\n logger.info(`Available models: ${models.map(m => m.id).join(', ')}`);\n\n const chatStore = await ChatStore.fromConfig({ database });\n\n const getChatModelById = (id: string) => {\n return models.find(model => model.id === id)?.chatModel;\n };\n\n const streamMessage = async ({\n modelId,\n messages,\n messageId,\n userEntityRef,\n }: StreamOptions) => {\n const model = getChatModelById(modelId);\n\n if (!model) {\n throw new Error(`Model with id ${modelId} not found`);\n }\n\n const promptStream = await model.stream(messages);\n\n const aiMessage: Required<Message> = {\n id: messageId,\n role: 'assistant',\n content: '',\n };\n\n for await (const chunk of promptStream) {\n aiMessage.content += chunk.content ?? '';\n\n chatStore.updateMessage(aiMessage);\n\n signals.publish({\n channel: `ai-assistant.chat.message-stream:${messageId}`,\n message: aiMessage,\n recipients: {\n type: 'user',\n entityRef: userEntityRef,\n },\n });\n }\n };\n\n const addChatMessage: (typeof chatStore)['addChatMessage'] = async (\n messages,\n userRef,\n conversationId,\n ) => {\n const conversationSize = await chatStore.getConversationSize(\n conversationId,\n );\n\n if (conversationSize < 1) {\n await chatStore.createConversation({\n id: conversationId,\n userRef,\n title: 'New Conversation',\n });\n\n await chatStore.addChatMessage(messages, userRef, conversationId);\n return;\n }\n\n const conversation = await chatStore.getConversation(\n conversationId,\n userRef,\n );\n\n if (conversationSize + messages.length < 5) {\n await chatStore.addChatMessage(messages, userRef, conversationId);\n return;\n }\n\n if (conversation.title !== 'New Conversation') {\n await chatStore.addChatMessage(messages, userRef, conversationId);\n return;\n }\n\n const summaryModelId =\n config.getOptionalString('aiAssistant.conversation.summaryModel') ??\n models[0].id;\n const summaryModel = getChatModelById(summaryModelId);\n\n if (!summaryModel) {\n throw new Error(`Model with id ${summaryModelId} not found`);\n }\n\n const conversationMessages = await chatStore.getChatMessages(\n conversationId,\n userRef,\n 5,\n );\n\n const summaryPrompt =\n config.getOptionalString('aiAssistant.conversation.summaryPrompt') ??\n DEFAULT_SUMMARY_PROMPT;\n\n const { text } = await summaryModel.invoke([\n ...conversationMessages,\n {\n role: 'system',\n content: summaryPrompt,\n },\n ]);\n\n conversation.title = text.trim();\n\n await chatStore.updateConversation(conversation);\n await chatStore.addChatMessage(messages, userRef, conversationId);\n };\n\n const prompt: ChatService['prompt'] = async ({\n conversationId,\n messages,\n modelId,\n stream = true,\n userEntityRef,\n }: PromptOptions) => {\n const model = getChatModelById(modelId);\n\n if (!model) {\n throw new Error(`Model with id ${modelId} not found`);\n }\n\n await addChatMessage(messages, userEntityRef, conversationId);\n\n const context = await vectorStore.similaritySearch(\n messages\n .filter(m => m.role === 'user')\n .map(m => m.content)\n .join('\\n'),\n );\n\n const recentConversationMessages = await chatStore.getChatMessages(\n conversationId,\n userEntityRef,\n 10,\n );\n\n const promptMessages = promptBuilder.buildPrompt(\n [...recentConversationMessages, ...messages],\n context,\n );\n\n const messageId: string = uuid();\n\n const aiMessage: Required<Message> = {\n id: messageId,\n role: 'assistant',\n content: '',\n };\n\n await addChatMessage([aiMessage], userEntityRef, conversationId);\n\n if (stream) {\n streamMessage({\n modelId,\n messages: promptMessages,\n messageId,\n userEntityRef,\n });\n\n return [aiMessage];\n }\n const { text } = await model.invoke(promptMessages);\n\n aiMessage.content = text;\n\n await chatStore.updateMessage(aiMessage);\n\n return [aiMessage];\n };\n\n const getAvailableModels: ChatService['getAvailableModels'] = async () => {\n return models.map(x => x.id);\n };\n\n const getConversation: ChatService['getConversation'] = async (\n options: GetConversationOptions,\n ) => {\n const { conversationId, userEntityRef } = options;\n\n const conversation = await chatStore.getChatMessages(\n conversationId,\n userEntityRef,\n );\n\n return conversation;\n };\n\n const getConversations: ChatService['getConversations'] = async ({\n userEntityRef,\n }: GetConversationsOptions) => {\n const conversations = await chatStore.getConversations(userEntityRef);\n\n return conversations;\n };\n return {\n prompt,\n getAvailableModels,\n getConversation,\n getConversations,\n };\n};\n"],"names":["chatStore","ChatStore","DEFAULT_SUMMARY_PROMPT","uuid"],"mappings":"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"}
@@ -1,21 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const DEFAULT_SYSTEM_PROMPT = `
4
- 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.
3
+ var prompts = require('../constants/prompts.cjs.js');
5
4
 
6
- Rules:
7
- 1. Always base your answers on the provided context. Do not make up information.
8
- 2. When relevant, cite or reference the source information provided in the context.
9
- 3. Format answers clearly and concisely. Use bullet points for lists when appropriate.
10
- 4. Maintain a professional, friendly, and helpful tone.
11
- 5. Return only the relevant information without any filler or unnecessary details.
12
- 6. If you don't know the answer, admit it and suggest ways to find the information.
13
- 7. Always return a well-structured response using markdown.
14
- `;
15
5
  const createPromptBuilder = ({
16
6
  config
17
7
  }) => {
18
- const system = config.getOptionalString("aiAssistant.prompt.system") || DEFAULT_SYSTEM_PROMPT;
8
+ const system = config.getOptionalString("aiAssistant.prompt.system") || prompts.DEFAULT_SYSTEM_PROMPT;
19
9
  const getContext = (context) => {
20
10
  return `
21
11
  Context:
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.cjs.js","sources":["../../src/services/prompt.ts"],"sourcesContent":["import { RootConfigService } from '@backstage/backend-plugin-api';\nimport { EmbeddingDocument } from '@sweetoburrito/backstage-plugin-ai-assistant-node';\n\nimport { Message } from '@sweetoburrito/backstage-plugin-ai-assistant-common';\n\ntype PromptBuilderOptions = {\n config: RootConfigService;\n};\n\nexport type PromptBuilder = {\n buildPrompt: (\n chatHistory: Message[],\n promptContext: EmbeddingDocument[],\n ) => Message[];\n};\n\nconst DEFAULT_SYSTEM_PROMPT = `\nYou are a helpful assistant that answers questions based on provided context from various documents. The context may come from sources such as internal wikis, code repositories, technical documentation, or other structured or unstructured data.\n\nRules:\n1. Always base your answers on the provided context. Do not make up information.\n2. When relevant, cite or reference the source information provided in the context.\n3. Format answers clearly and concisely. Use bullet points for lists when appropriate.\n4. Maintain a professional, friendly, and helpful tone.\n5. Return only the relevant information without any filler or unnecessary details.\n6. If you don't know the answer, admit it and suggest ways to find the information.\n7. Always return a well-structured response using markdown.\n`;\n\nexport const createPromptBuilder = ({\n config,\n}: PromptBuilderOptions): PromptBuilder => {\n const system =\n config.getOptionalString('aiAssistant.prompt.system') ||\n DEFAULT_SYSTEM_PROMPT;\n\n const getContext = (context: EmbeddingDocument[]) => {\n return `\n Context:\n ${context.map(doc => JSON.stringify(doc)).join('\\n')}\n `;\n };\n\n const buildPrompt: PromptBuilder['buildPrompt'] = (\n chatHistory,\n promptContext,\n ) => {\n const context = getContext(promptContext);\n\n return [\n {\n role: 'system',\n content: system.concat(context),\n },\n ...chatHistory,\n ];\n };\n\n return {\n buildPrompt,\n };\n};\n"],"names":[],"mappings":";;AAgBA,MAAM,qBAAA,GAAwB;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAavB,MAAM,sBAAsB,CAAC;AAAA,EAClC;AACF,CAAA,KAA2C;AACzC,EAAA,MAAM,MAAA,GACJ,MAAA,CAAO,iBAAA,CAAkB,2BAA2B,CAAA,IACpD,qBAAA;AAEF,EAAA,MAAM,UAAA,GAAa,CAAC,OAAA,KAAiC;AACnD,IAAA,OAAO;AAAA;AAAA,IAAA,EAEL,OAAA,CAAQ,GAAA,CAAI,CAAA,GAAA,KAAO,IAAA,CAAK,SAAA,CAAU,GAAG,CAAC,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC;AAAA,IAAA,CAAA;AAAA,EAEtD,CAAA;AAEA,EAAA,MAAM,WAAA,GAA4C,CAChD,WAAA,EACA,aAAA,KACG;AACH,IAAA,MAAM,OAAA,GAAU,WAAW,aAAa,CAAA;AAExC,IAAA,OAAO;AAAA,MACL;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,OAAO;AAAA,OAChC;AAAA,MACA,GAAG;AAAA,KACL;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL;AAAA,GACF;AACF;;;;"}
1
+ {"version":3,"file":"prompt.cjs.js","sources":["../../src/services/prompt.ts"],"sourcesContent":["import { RootConfigService } from '@backstage/backend-plugin-api';\nimport { EmbeddingDocument } from '@sweetoburrito/backstage-plugin-ai-assistant-node';\n\nimport { Message } from '@sweetoburrito/backstage-plugin-ai-assistant-common';\nimport { DEFAULT_SYSTEM_PROMPT } from '../constants/prompts';\n\ntype PromptBuilderOptions = {\n config: RootConfigService;\n};\n\nexport type PromptBuilder = {\n buildPrompt: (\n chatHistory: Message[],\n promptContext: EmbeddingDocument[],\n ) => Message[];\n};\n\nexport const createPromptBuilder = ({\n config,\n}: PromptBuilderOptions): PromptBuilder => {\n const system =\n config.getOptionalString('aiAssistant.prompt.system') ||\n DEFAULT_SYSTEM_PROMPT;\n\n const getContext = (context: EmbeddingDocument[]) => {\n return `\n Context:\n ${context.map(doc => JSON.stringify(doc)).join('\\n')}\n `;\n };\n\n const buildPrompt: PromptBuilder['buildPrompt'] = (\n chatHistory,\n promptContext,\n ) => {\n const context = getContext(promptContext);\n\n return [\n {\n role: 'system',\n content: system.concat(context),\n },\n ...chatHistory,\n ];\n };\n\n return {\n buildPrompt,\n };\n};\n"],"names":["DEFAULT_SYSTEM_PROMPT"],"mappings":";;;;AAiBO,MAAM,sBAAsB,CAAC;AAAA,EAClC;AACF,CAAA,KAA2C;AACzC,EAAA,MAAM,MAAA,GACJ,MAAA,CAAO,iBAAA,CAAkB,2BAA2B,CAAA,IACpDA,6BAAA;AAEF,EAAA,MAAM,UAAA,GAAa,CAAC,OAAA,KAAiC;AACnD,IAAA,OAAO;AAAA;AAAA,IAAA,EAEL,OAAA,CAAQ,GAAA,CAAI,CAAA,GAAA,KAAO,IAAA,CAAK,SAAA,CAAU,GAAG,CAAC,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC;AAAA,IAAA,CAAA;AAAA,EAEtD,CAAA;AAEA,EAAA,MAAM,WAAA,GAA4C,CAChD,WAAA,EACA,aAAA,KACG;AACH,IAAA,MAAM,OAAA,GAAU,WAAW,aAAa,CAAA;AAExC,IAAA,OAAO;AAAA,MACL;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,OAAA,EAAS,MAAA,CAAO,MAAA,CAAO,OAAO;AAAA,OAChC;AAAA,MACA,GAAG;AAAA,KACL;AAAA,EACF,CAAA;AAEA,EAAA,OAAO;AAAA,IACL;AAAA,GACF;AACF;;;;"}
@@ -48,6 +48,14 @@ async function createChatRouter(options) {
48
48
  const chatSchema = z__default.default.object({
49
49
  id: z__default.default.string().uuid()
50
50
  });
51
+ router.get("/conversations", async (req, res) => {
52
+ const credentials = await httpAuth.credentials(req);
53
+ const { userEntityRef } = await userInfo.getUserInfo(credentials);
54
+ const conversations = await chat.getConversations({
55
+ userEntityRef
56
+ });
57
+ res.json({ conversations });
58
+ });
51
59
  router.get("/:id", validation.validation(chatSchema, "params"), async (req, res) => {
52
60
  const { id } = req.params;
53
61
  const credentials = await httpAuth.credentials(req);
@@ -1 +1 @@
1
- {"version":3,"file":"chat.cjs.js","sources":["../../../src/services/router/chat.ts"],"sourcesContent":["import express from 'express';\nimport Router from 'express-promise-router';\nimport { ChatService } from '../chat';\nimport z from 'zod';\nimport { validation } from './middleware/validation';\nimport { v4 as uuid } from 'uuid';\nimport {\n DatabaseService,\n HttpAuthService,\n UserInfoService,\n} from '@backstage/backend-plugin-api';\n\nexport type ChatRouterOptions = {\n chat: ChatService;\n database: DatabaseService;\n httpAuth: HttpAuthService;\n userInfo: UserInfoService;\n};\n\nexport async function createChatRouter(\n options: ChatRouterOptions,\n): Promise<express.Router> {\n const { chat, httpAuth, userInfo } = options;\n\n const router = Router();\n\n const messageSchema = z.object({\n messages: z.array(\n z.object({\n id: z.string().uuid().optional().default(uuid),\n role: z.string(),\n content: z.string(),\n }),\n ),\n modelId: z.string(),\n conversationId: z.string().uuid().optional().default(uuid),\n stream: z.boolean().optional(),\n });\n\n router.post(\n '/message',\n validation(messageSchema, 'body'),\n async (req, res) => {\n const { messages, conversationId, modelId, stream } = req.body;\n\n const credentials = await httpAuth.credentials(req);\n const { userEntityRef } = await userInfo.getUserInfo(credentials);\n\n const responseMessages = await chat.prompt({\n modelId,\n messages,\n conversationId,\n stream,\n userEntityRef,\n });\n\n res.json({\n messages: responseMessages,\n conversationId,\n });\n },\n );\n\n const chatSchema = z.object({\n id: z.string().uuid(),\n });\n\n router.get('/:id', validation(chatSchema, 'params'), async (req, res) => {\n const { id } = req.params;\n\n const credentials = await httpAuth.credentials(req);\n const { userEntityRef } = await userInfo.getUserInfo(credentials);\n\n const conversation = await chat.getConversation({\n conversationId: id,\n userEntityRef,\n });\n\n res.json({ conversation });\n });\n\n return router;\n}\n"],"names":["Router","z","uuid","validation"],"mappings":";;;;;;;;;;;;AAmBA,eAAsB,iBACpB,OAAA,EACyB;AACzB,EAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAU,QAAA,EAAS,GAAI,OAAA;AAErC,EAAA,MAAM,SAASA,uBAAA,EAAO;AAEtB,EAAA,MAAM,aAAA,GAAgBC,mBAAE,MAAA,CAAO;AAAA,IAC7B,UAAUA,kBAAA,CAAE,KAAA;AAAA,MACVA,mBAAE,MAAA,CAAO;AAAA,QACP,EAAA,EAAIA,mBAAE,MAAA,EAAO,CAAE,MAAK,CAAE,QAAA,EAAS,CAAE,OAAA,CAAQC,OAAI,CAAA;AAAA,QAC7C,IAAA,EAAMD,mBAAE,MAAA,EAAO;AAAA,QACf,OAAA,EAASA,mBAAE,MAAA;AAAO,OACnB;AAAA,KACH;AAAA,IACA,OAAA,EAASA,mBAAE,MAAA,EAAO;AAAA,IAClB,cAAA,EAAgBA,mBAAE,MAAA,EAAO,CAAE,MAAK,CAAE,QAAA,EAAS,CAAE,OAAA,CAAQC,OAAI,CAAA;AAAA,IACzD,MAAA,EAAQD,kBAAA,CAAE,OAAA,EAAQ,CAAE,QAAA;AAAS,GAC9B,CAAA;AAED,EAAA,MAAA,CAAO,IAAA;AAAA,IACL,UAAA;AAAA,IACAE,qBAAA,CAAW,eAAe,MAAM,CAAA;AAAA,IAChC,OAAO,KAAK,GAAA,KAAQ;AAClB,MAAA,MAAM,EAAE,QAAA,EAAU,cAAA,EAAgB,OAAA,EAAS,MAAA,KAAW,GAAA,CAAI,IAAA;AAE1D,MAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,MAAA,MAAM,EAAE,aAAA,EAAc,GAAI,MAAM,QAAA,CAAS,YAAY,WAAW,CAAA;AAEhE,MAAA,MAAM,gBAAA,GAAmB,MAAM,IAAA,CAAK,MAAA,CAAO;AAAA,QACzC,OAAA;AAAA,QACA,QAAA;AAAA,QACA,cAAA;AAAA,QACA,MAAA;AAAA,QACA;AAAA,OACD,CAAA;AAED,MAAA,GAAA,CAAI,IAAA,CAAK;AAAA,QACP,QAAA,EAAU,gBAAA;AAAA,QACV;AAAA,OACD,CAAA;AAAA,IACH;AAAA,GACF;AAEA,EAAA,MAAM,UAAA,GAAaF,mBAAE,MAAA,CAAO;AAAA,IAC1B,EAAA,EAAIA,kBAAA,CAAE,MAAA,EAAO,CAAE,IAAA;AAAK,GACrB,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,QAAQE,qBAAA,CAAW,UAAA,EAAY,QAAQ,CAAA,EAAG,OAAO,KAAK,GAAA,KAAQ;AACvE,IAAA,MAAM,EAAE,EAAA,EAAG,GAAI,GAAA,CAAI,MAAA;AAEnB,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,IAAA,MAAM,EAAE,aAAA,EAAc,GAAI,MAAM,QAAA,CAAS,YAAY,WAAW,CAAA;AAEhE,IAAA,MAAM,YAAA,GAAe,MAAM,IAAA,CAAK,eAAA,CAAgB;AAAA,MAC9C,cAAA,EAAgB,EAAA;AAAA,MAChB;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,YAAA,EAAc,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;"}
1
+ {"version":3,"file":"chat.cjs.js","sources":["../../../src/services/router/chat.ts"],"sourcesContent":["import express from 'express';\nimport Router from 'express-promise-router';\nimport { ChatService } from '../chat';\nimport z from 'zod';\nimport { validation } from './middleware/validation';\nimport { v4 as uuid } from 'uuid';\nimport {\n DatabaseService,\n HttpAuthService,\n UserInfoService,\n} from '@backstage/backend-plugin-api';\n\nexport type ChatRouterOptions = {\n chat: ChatService;\n database: DatabaseService;\n httpAuth: HttpAuthService;\n userInfo: UserInfoService;\n};\n\nexport async function createChatRouter(\n options: ChatRouterOptions,\n): Promise<express.Router> {\n const { chat, httpAuth, userInfo } = options;\n\n const router = Router();\n\n const messageSchema = z.object({\n messages: z.array(\n z.object({\n id: z.string().uuid().optional().default(uuid),\n role: z.string(),\n content: z.string(),\n }),\n ),\n modelId: z.string(),\n conversationId: z.string().uuid().optional().default(uuid),\n stream: z.boolean().optional(),\n });\n\n router.post(\n '/message',\n validation(messageSchema, 'body'),\n async (req, res) => {\n const { messages, conversationId, modelId, stream } = req.body;\n\n const credentials = await httpAuth.credentials(req);\n const { userEntityRef } = await userInfo.getUserInfo(credentials);\n\n const responseMessages = await chat.prompt({\n modelId,\n messages,\n conversationId,\n stream,\n userEntityRef,\n });\n\n res.json({\n messages: responseMessages,\n conversationId,\n });\n },\n );\n\n const chatSchema = z.object({\n id: z.string().uuid(),\n });\n\n router.get('/conversations', async (req, res) => {\n const credentials = await httpAuth.credentials(req);\n const { userEntityRef } = await userInfo.getUserInfo(credentials);\n\n const conversations = await chat.getConversations({\n userEntityRef,\n });\n\n res.json({ conversations });\n });\n\n router.get('/:id', validation(chatSchema, 'params'), async (req, res) => {\n const { id } = req.params;\n\n const credentials = await httpAuth.credentials(req);\n const { userEntityRef } = await userInfo.getUserInfo(credentials);\n\n const conversation = await chat.getConversation({\n conversationId: id,\n userEntityRef,\n });\n\n res.json({ conversation });\n });\n\n return router;\n}\n"],"names":["Router","z","uuid","validation"],"mappings":";;;;;;;;;;;;AAmBA,eAAsB,iBACpB,OAAA,EACyB;AACzB,EAAA,MAAM,EAAE,IAAA,EAAM,QAAA,EAAU,QAAA,EAAS,GAAI,OAAA;AAErC,EAAA,MAAM,SAASA,uBAAA,EAAO;AAEtB,EAAA,MAAM,aAAA,GAAgBC,mBAAE,MAAA,CAAO;AAAA,IAC7B,UAAUA,kBAAA,CAAE,KAAA;AAAA,MACVA,mBAAE,MAAA,CAAO;AAAA,QACP,EAAA,EAAIA,mBAAE,MAAA,EAAO,CAAE,MAAK,CAAE,QAAA,EAAS,CAAE,OAAA,CAAQC,OAAI,CAAA;AAAA,QAC7C,IAAA,EAAMD,mBAAE,MAAA,EAAO;AAAA,QACf,OAAA,EAASA,mBAAE,MAAA;AAAO,OACnB;AAAA,KACH;AAAA,IACA,OAAA,EAASA,mBAAE,MAAA,EAAO;AAAA,IAClB,cAAA,EAAgBA,mBAAE,MAAA,EAAO,CAAE,MAAK,CAAE,QAAA,EAAS,CAAE,OAAA,CAAQC,OAAI,CAAA;AAAA,IACzD,MAAA,EAAQD,kBAAA,CAAE,OAAA,EAAQ,CAAE,QAAA;AAAS,GAC9B,CAAA;AAED,EAAA,MAAA,CAAO,IAAA;AAAA,IACL,UAAA;AAAA,IACAE,qBAAA,CAAW,eAAe,MAAM,CAAA;AAAA,IAChC,OAAO,KAAK,GAAA,KAAQ;AAClB,MAAA,MAAM,EAAE,QAAA,EAAU,cAAA,EAAgB,OAAA,EAAS,MAAA,KAAW,GAAA,CAAI,IAAA;AAE1D,MAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,MAAA,MAAM,EAAE,aAAA,EAAc,GAAI,MAAM,QAAA,CAAS,YAAY,WAAW,CAAA;AAEhE,MAAA,MAAM,gBAAA,GAAmB,MAAM,IAAA,CAAK,MAAA,CAAO;AAAA,QACzC,OAAA;AAAA,QACA,QAAA;AAAA,QACA,cAAA;AAAA,QACA,MAAA;AAAA,QACA;AAAA,OACD,CAAA;AAED,MAAA,GAAA,CAAI,IAAA,CAAK;AAAA,QACP,QAAA,EAAU,gBAAA;AAAA,QACV;AAAA,OACD,CAAA;AAAA,IACH;AAAA,GACF;AAEA,EAAA,MAAM,UAAA,GAAaF,mBAAE,MAAA,CAAO;AAAA,IAC1B,EAAA,EAAIA,kBAAA,CAAE,MAAA,EAAO,CAAE,IAAA;AAAK,GACrB,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,gBAAA,EAAkB,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC/C,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,IAAA,MAAM,EAAE,aAAA,EAAc,GAAI,MAAM,QAAA,CAAS,YAAY,WAAW,CAAA;AAEhE,IAAA,MAAM,aAAA,GAAgB,MAAM,IAAA,CAAK,gBAAA,CAAiB;AAAA,MAChD;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,aAAA,EAAe,CAAA;AAAA,EAC5B,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,QAAQE,qBAAA,CAAW,UAAA,EAAY,QAAQ,CAAA,EAAG,OAAO,KAAK,GAAA,KAAQ;AACvE,IAAA,MAAM,EAAE,EAAA,EAAG,GAAI,GAAA,CAAI,MAAA;AAEnB,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAG,CAAA;AAClD,IAAA,MAAM,EAAE,aAAA,EAAc,GAAI,MAAM,QAAA,CAAS,YAAY,WAAW,CAAA;AAEhE,IAAA,MAAM,YAAA,GAAe,MAAM,IAAA,CAAK,eAAA,CAAgB;AAAA,MAC9C,cAAA,EAAgB,EAAA;AAAA,MAChB;AAAA,KACD,CAAA;AAED,IAAA,GAAA,CAAI,IAAA,CAAK,EAAE,YAAA,EAAc,CAAA;AAAA,EAC3B,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;"}
@@ -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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweetoburrito/backstage-plugin-ai-assistant-backend",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "license": "Apache-2.0",
5
5
  "main": "dist/index.cjs.js",
6
6
  "types": "dist/index.d.ts",
@@ -39,7 +39,7 @@
39
39
  "@backstage/plugin-signals-node": "backstage:^",
40
40
  "@langchain/core": "^0.3.72",
41
41
  "@langchain/textsplitters": "^0.1.0",
42
- "@sweetoburrito/backstage-plugin-ai-assistant-common": "^0.2.2",
42
+ "@sweetoburrito/backstage-plugin-ai-assistant-common": "^0.3.0",
43
43
  "@sweetoburrito/backstage-plugin-ai-assistant-node": "^0.3.2",
44
44
  "express": "^4.17.1",
45
45
  "express-promise-router": "^4.1.0",
@@ -53,12 +53,14 @@
53
53
  "@backstage/plugin-auth-backend": "backstage:^",
54
54
  "@backstage/plugin-auth-backend-module-guest-provider": "backstage:^",
55
55
  "@backstage/plugin-catalog-backend": "backstage:^",
56
+ "@backstage/plugin-events-backend": "backstage:^",
56
57
  "@backstage/plugin-signals-backend": "backstage:^",
57
58
  "@backstage/types": "backstage:^",
58
- "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-embeddings-provider-ollama": "^0.3.2",
59
- "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-ingestor-catalog": "^0.3.2",
60
- "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-model-provider-azure-ai": "^0.3.2",
61
- "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-model-provider-ollama": "^0.3.2",
59
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-embeddings-provider-azure-open-ai": "workspace:^",
60
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-embeddings-provider-ollama": "workspace:^",
61
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-ingestor-catalog": "workspace:^",
62
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-model-provider-azure-ai": "workspace:^",
63
+ "@sweetoburrito/backstage-plugin-ai-assistant-backend-module-model-provider-ollama": "workspace:^",
62
64
  "@types/express": "^4.0.0",
63
65
  "@types/supertest": "^2.0.12",
64
66
  "supertest": "^6.2.4"