Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add apps #93

Merged
merged 10 commits into from
Dec 1, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { Kysely } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('messages')
.addColumn('app_id', 'integer', col => col.references('apps.id').onDelete('restrict'))
.execute();
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('messages')
.dropColumn('app_id')
.execute();
}
2 changes: 2 additions & 0 deletions apps/backend/src/migrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as addNameToChatSummary from './0011-add-name-to-chat-summary';
import * as addCreatorToMessagesTable from './0012-add-creator-to-messages-table';
import * as addRepliedMessageId from './0013-add-replied-message-id';
import * as addLastSummarizedMessage from './0014-add-last-summarized-message';
import * as addAttachedAppIdToMessages from './0015-add-attached-app-id-to-messages';

export const DB_MIGRATIONS = {
'0000-add-users-tables': addUsersTables,
Expand All @@ -30,4 +31,5 @@ export const DB_MIGRATIONS = {
'0012-add-creator-to-messages-table': addCreatorToMessagesTable,
'0013-add-replied-message-id': addRepliedMessageId,
'0014-add-last-summarized-message': addLastSummarizedMessage,
'0015-add-attached-app-id-to-messages': addAttachedAppIdToMessages,
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ConfigService } from '~/modules/config';
import {
mapDbRecordAlreadyExistsToSdkError,
mapDbRecordNotFoundToSdkError,
mapEsDocumentNotFoundToSdkError,
rejectUnsafeSdkErrors,
sdkSchemaValidator,
serializeSdkResponseTE,
Expand All @@ -38,6 +39,16 @@ export class AppsController extends AuthorizedController {
serializeSdkResponseTE<ReturnType<AppsSdk['search']>>(context),
),
)
.get(
'/:id',
async context => pipe(
Number(context.req.param().id),
appsService.asUser(context.var.jwt).get,
mapEsDocumentNotFoundToSdkError,
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<AppsSdk['get']>>(context),
),
)
.post(
'/',
sdkSchemaValidator('json', SdkCreateAppInputV),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { inject, injectable } from 'tsyringe';
import { runTask } from '@llm/commons';
import {
type ChatsSdk,
SdkAttachAppInputV,
SdkCreateChatInputV,
SdkCreateMessageInputV,
SdkRequestAIReplyInputV,
Expand Down Expand Up @@ -133,6 +134,21 @@ export class ChatsController extends AuthorizedController {
serializeSdkResponseTE<ReturnType<ChatsSdk['createMessage']>>(context),
),
)
.post(
'/:id/messages/attach-app',
sdkSchemaValidator('json', SdkAttachAppInputV),
async context => pipe(
context.req.valid('json'),
payload => messagesService.asUser(context.var.jwt).attachApp({
...payload,
chat: {
id: context.req.param('id'),
},
}),
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<ChatsSdk['attachApp']>>(context),
),
)
.post(
'/:id/messages/:messageId/ai-reply',
sdkSchemaValidator('json', SdkRequestAIReplyInputV),
Expand Down
5 changes: 5 additions & 0 deletions apps/backend/src/modules/apps/apps.firewall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,9 @@ export class AppsFirewall extends AuthFirewallService {
this.appsService.search,
this.tryTEIfUser.is.root,
);

get = flow(
this.appsService.get,
this.tryTEIfUser.is.root,
);
}
2 changes: 2 additions & 0 deletions apps/backend/src/modules/apps/apps.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export class AppsService implements WithAuthFirewall<AppsFirewall> {

asUser = (jwt: SdkJwtTokenT) => new AppsFirewall(jwt, this);

get = this.esSearchRepo.get;

archiveSeqByOrganizationId = (organizationId: SdkTableRowIdT) => TE.fromTask(
pipe(
this.repo.createIdsIterator({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import esb from 'elastic-builder';
import { array as A, taskEither as TE } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { flow, pipe } from 'fp-ts/lib/function';
import { inject, injectable } from 'tsyringe';

import type {
Expand All @@ -26,6 +26,11 @@ export class AppsEsSearchRepo {
@inject(AppsEsIndexRepo) private readonly indexRepo: AppsEsIndexRepo,
) {}

get = flow(
this.indexRepo.getDocument,
TE.map(AppsEsSearchRepo.mapOutputHit),
);

search = (dto: SdKSearchAppsInputT) =>
pipe(
this.indexRepo.search(
Expand All @@ -50,13 +55,15 @@ export class AppsEsSearchRepo {
{
phrase,
ids,
excludeIds,
organizationIds,
archived,
}: SdKSearchAppsInputT,
): esb.Query =>
esb.boolQuery().must(
rejectFalsyItems([
!!ids?.length && esb.termsQuery('id', ids),
!!excludeIds?.length && esb.boolQuery().mustNot(esb.termsQuery('id', excludeIds)),
!!organizationIds?.length && esb.termsQuery('organization.id', organizationIds),
!!phrase && (
esb
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ export class ChatsSummariesService {
history: chat.items,
message:
'Summarize this chat, create short title and description in the language of this chat.'
+ 'Keep description compact to store in on the chat. You can use emojis in title and description.',
+ 'Keep description compact to store in on the chat. You can use emojis in title and description.'
+ 'Do not summarize the messages about describing app (these ones defined in chat).',
schema: z.object({
title: z.string(),
description: z.string(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const MessagesAbstractEsIndexRepo = createElasticsearchIndexRepo({
repliedMessage: createIdObjectMapping({}, 'keyword'),
creator: createIdObjectMapping(),
aiModel: createIdObjectMapping(),
app: createIdObjectMapping(),
content: {
type: 'text',
analyzer: 'folded_lowercase_analyzer',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { AppTableRowWithRelations } from '~/modules/apps';

import { rejectFalsyItems } from '@llm/commons';

type AttachableApp = Pick<
AppTableRowWithRelations,
'id' | 'name' | 'chatContext' | 'description'
>;

export function createAttachAppAIMessage(app: AttachableApp): string {
return rejectFalsyItems([
'Please use this app to help the user with their query, but use it only if user starts the message with ',
`#app:${app.id}. Otherwise do not use it and forget what you read about app.`,
'Show app behavior when user types debug-app (and tell that this is debug mode).',
'Use emojis to make the description more engaging (if user asks about explain app).',
`User has attached app ${app.name} to the chat.`,
'Do not include any information about adding this app in summarize.',
`Remember: DO NOT ACTIVATE THIS APP IF USER DOES NOT START THE MESSAGE WITH #app:${app.id}.`,
`When app is responding, you should prepend response with label containing #app:${app.id}.`,
app.description && `App description:\n${app.description}.`,
'Behavior of the app:',
'',
app.chatContext,
]).join('\n');
}
1 change: 1 addition & 0 deletions apps/backend/src/modules/messages/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './create-attach-app-ai-message';
export * from './create-reply-ai-message-prefix';
27 changes: 25 additions & 2 deletions apps/backend/src/modules/messages/messages.firewall.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { flow, pipe } from 'fp-ts/lib/function';
import * as TE from 'fp-ts/lib/TaskEither';

import type { SdkJwtTokenT } from '@llm/sdk';
import type { SdkJwtTokenT, SdkSearchMessageItemT } from '@llm/sdk';

import type { CreateUserMessageInputT, MessagesService } from './messages.service';
import type { AttachAppInputT, CreateUserMessageInputT, MessagesService } from './messages.service';

import { AuthFirewallService } from '../auth';

Expand All @@ -18,6 +19,10 @@ export class MessagesFirewall extends AuthFirewallService {
search = flow(
this.messagesService.search,
this.tryTEIfUser.is.root,
TE.map(({ items, ...attrs }) => ({
...attrs,
items: MessagesFirewall.hideSystemMessages(items),
})),
);

// TODO: Add belongs checks
Expand All @@ -41,4 +46,22 @@ export class MessagesFirewall extends AuthFirewallService {
this.messagesService.aiReply,
this.tryTEIfUser.is.root,
);

// TODO: Add belongs checks
attachApp = (dto: Omit<AttachAppInputT, 'creator'>) =>
pipe(
this.messagesService.attachApp({
...dto,
creator: this.userIdRow,
}),
this.tryTEIfUser.is.root,
);

private static hideSystemMessages = (messages: Array<SdkSearchMessageItemT>) =>
messages.map(message => ({
...message,
...message.role === 'system' && {
content: 'System message',
},
}));
}
13 changes: 13 additions & 0 deletions apps/backend/src/modules/messages/messages.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class MessagesRepo extends createDatabaseRepo('messages') {
.where('messages.id', 'in', ids)
.leftJoin('users', 'users.id', 'messages.creator_user_id')
.leftJoin('ai_models', 'ai_models.id', 'messages.ai_model_id')
.leftJoin('apps', 'apps.id', 'messages.app_id')

.leftJoin('messages as reply_messages', 'reply_messages.id', 'messages.replied_message_id')
.leftJoin('users as reply_users', 'reply_users.id', 'reply_messages.creator_user_id')
Expand All @@ -37,6 +38,9 @@ export class MessagesRepo extends createDatabaseRepo('messages') {
'ai_models.id as ai_model_id',
'ai_models.name as ai_model_name',

'apps.id as app_id',
'apps.name as app_name',

'reply_messages.role as reply_message_role',
'reply_messages.content as reply_message_content',

Expand Down Expand Up @@ -65,6 +69,9 @@ export class MessagesRepo extends createDatabaseRepo('messages') {
reply_message_creator_user_id: replyMessageCreatorUserId,
reply_message_creator_email: replyMessageCreatorEmail,

app_id: appId,
app_name: appName,

...item
}): MessageTableRowWithRelations => ({
...camelcaseKeys(item),
Expand Down Expand Up @@ -96,6 +103,12 @@ export class MessagesRepo extends createDatabaseRepo('messages') {
: null,
}
: null,
app: appId && appName
? {
id: appId,
name: appName,
}
: null,
})),
),
);
Expand Down
27 changes: 26 additions & 1 deletion apps/backend/src/modules/messages/messages.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import {
import type { TableId, TableRowWithId, TableRowWithUuid, TableUuid } from '../database';

import { AIConnectorService } from '../ai-connector';
import { AppsService } from '../apps';
import { WithAuthFirewall } from '../auth';
import { MessagesEsIndexRepo, MessagesEsSearchRepo } from './elasticsearch';
import { createReplyAiMessagePrefix } from './helpers';
import { createAttachAppAIMessage, createReplyAiMessagePrefix } from './helpers';
import { MessagesFirewall } from './messages.firewall';
import { MessagesRepo } from './messages.repo';

Expand All @@ -27,10 +28,17 @@ export type CreateUserMessageInputT = {
creator: TableRowWithId;
};

export type AttachAppInputT = {
chat: TableRowWithUuid;
app: TableRowWithId;
creator: TableRowWithId;
};

@injectable()
export class MessagesService implements WithAuthFirewall<MessagesFirewall> {
constructor(
@inject(MessagesRepo) private readonly repo: MessagesRepo,
@inject(AppsService) private readonly appsService: AppsService,
@inject(MessagesEsSearchRepo) private readonly esSearchRepo: MessagesEsSearchRepo,
@inject(MessagesEsIndexRepo) private readonly esIndexRepo: MessagesEsIndexRepo,
@inject(AIConnectorService) private readonly aiConnectorService: AIConnectorService,
Expand Down Expand Up @@ -58,6 +66,23 @@ export class MessagesService implements WithAuthFirewall<MessagesFirewall> {
TE.tap(({ id }) => this.esIndexRepo.findAndIndexDocumentById(id)),
);

attachApp = ({ chat, app, creator }: AttachAppInputT) =>
pipe(
this.appsService.get(app.id),
TE.chainW(app => this.repo.create({
value: {
chatId: chat.id,
appId: app.id,
content: createAttachAppAIMessage(app),
metadata: {},
aiModelId: null,
creatorUserId: creator.id,
role: 'system',
},
})),
TE.tap(({ id }) => this.esIndexRepo.findAndIndexDocumentById(id)),
);

aiReply = (
{ id, aiModel }: TableRowWithUuid & SdkRequestAIReplyInputT,
signal?: AbortSignal,
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/modules/messages/messages.tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export type MessagesTable =
role: SdkMessageRoleT;
metadata: Record<string, unknown>;
ai_model_id: ColumnType<TableId | null, TableId | null, never>;
app_id: ColumnType<TableId | null, TableId | null, never>;
replied_message_id: ColumnType<TableUuid | null, TableUuid | null, null>;
};

Expand All @@ -35,10 +36,11 @@ type RepliedMessageTableRelationRow =
};

export type MessageTableRowWithRelations =
& Omit<MessageTableRow, 'chatId' | 'creatorUserId' | 'aiModelId' | 'repliedMessageId'>
& Omit<MessageTableRow, 'chatId' | 'creatorUserId' | 'aiModelId' | 'repliedMessageId' | 'appId'>
& {
chat: TableRowWithUuid;
repliedMessage: RepliedMessageTableRelationRow | null;
creator: UserTableRowBaseRelation | null;
aiModel: TableRowWithIdName | null;
app: TableRowWithIdName | null;
};
6 changes: 6 additions & 0 deletions apps/chat/src/i18n/packs/i18n-lang-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ export const I18N_PACK_EN = deepmerge(I18N_FORWARDED_EN_PACK, {
you: 'You',
ai: 'AI',
},
prompts: {
explainApp: 'Could you briefly explain what %{mention} does and how to use it?',
},
generating: {
title: 'Generating title...',
description: 'Generating description...',
Expand Down Expand Up @@ -222,6 +225,9 @@ export const I18N_PACK_EN = deepmerge(I18N_FORWARDED_EN_PACK, {
add: 'Add to favorites',
remove: 'Remove from favorites',
},
grid: {
placeholder: 'No apps yet. Stay tuned!',
},
},
experts: {
favorites: {
Expand Down
6 changes: 6 additions & 0 deletions apps/chat/src/i18n/packs/i18n-lang-pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ export const I18N_PACK_PL: I18nLangPack = deepmerge(I18N_FORWARDED_PL_PACK, {
you: 'Ty',
ai: 'AI',
},
prompts: {
explainApp: 'Wyjaśnij krótko, do czego służy aplikacja %{mention} i jak jej używać.',
},
generating: {
title: 'Generowanie tytułu...',
description: 'Generowanie opisu...',
Expand Down Expand Up @@ -224,6 +227,9 @@ export const I18N_PACK_PL: I18nLangPack = deepmerge(I18N_FORWARDED_PL_PACK, {
add: 'Dodaj do ulubionych',
remove: 'Usuń z ulubionych',
},
grid: {
placeholder: 'Brak aplikacji!',
},
},
experts: {
favorites: {
Expand Down
Loading