Skip to content

Commit

Permalink
Merge pull request #120 from DashHub-ai/feature/internal-projects-sup…
Browse files Browse the repository at this point in the history
…port

Add support for attaching files to messages
  • Loading branch information
Mati365 authored Dec 26, 2024
2 parents 2feb7c3 + 837ff27 commit 50ee094
Show file tree
Hide file tree
Showing 65 changed files with 896 additions and 224 deletions.
31 changes: 31 additions & 0 deletions apps/backend/src/migrations/0025-add-internal-projects-fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Kysely } from 'kysely';

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

await db.schema
.createIndex('projects_files_message_id_index')
.on('projects_files')
.column('message_id')
.execute();

await db.schema
.alterTable('projects')
.addColumn('internal', 'boolean', col => col.notNull().defaultTo(false))
.execute();
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('projects')
.dropColumn('internal')
.execute();

await db.schema
.alterTable('projects_files')
.dropColumn('message_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 @@ -23,6 +23,7 @@ import * as dropUniqueNameFromS3Assets from './0021-drop-unique-name-from-s3-ass
import * as addIdToProjectFilesTable from './0022-add-id-to-project-files-table';
import * as addProjectsEmbeddingsTable from './0023-add-projects-embeddings-table';
import * as dropUnusedImagesTable from './0024-drop-unused-images-table';
import * as addInternalProjectsFields from './0025-add-internal-projects-fields';

export const DB_MIGRATIONS = {
'0000-add-users-tables': addUsersTables,
Expand Down Expand Up @@ -50,4 +51,5 @@ export const DB_MIGRATIONS = {
'0022-add-id-to-project-files-table': addIdToProjectFilesTable,
'0023-add-projects-embeddings-table': addProjectsEmbeddingsTable,
'0024-drop-unused-images-table': dropUnusedImagesTable,
'0025-add-internal-projects-fields': addInternalProjectsFields,
};
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
sdkSchemaValidator,
serializeSdkResponseTE,
} from '../../helpers';
import { tryExtractFiles } from '../../helpers/try-extract-files';
import { AuthorizedController } from '../shared/authorized.controller';

@injectable()
Expand Down Expand Up @@ -120,15 +121,22 @@ export class ChatsController extends AuthorizedController {
)
.post(
'/:id/messages',
sdkSchemaValidator('json', SdkCreateMessageInputV),
async context => pipe(
context.req.valid('json'),
message => messagesService.asUser(context.var.jwt).create({
message,
await context.req.parseBody(),
tryExtractFiles(
SdkCreateMessageInputV.omit({
files: true,
}),
),
TE.chainW(({ content, files }) => messagesService.asUser(context.var.jwt).create({
files: [...files ?? []],
message: {
content,
},
chat: {
id: context.req.param('id'),
},
}),
})),
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<ChatsSdk['createMessage']>>(context),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class ProjectsController extends AuthorizedController {
TE.chainW(({ buffer, mimeType, fileName }) =>
projectsFilesService.asUser(context.var.jwt).uploadFile({
projectId: Number(context.req.param('projectId')),
name: fileName,
fileName,
buffer,
mimeType,
}),
Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/modules/api/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './reject-unsafe-sdk-errors';
export * from './respond-with-tagged-error';
export * from './sdk-hono-schema-validator';
export * from './serialize-sdk-response-te';
export * from './try-extract-files';
export * from './try-extract-single-file';
58 changes: 58 additions & 0 deletions apps/backend/src/modules/api/helpers/try-extract-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Buffer } from 'node:buffer';

import { either as E, taskEither as TE } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { z } from 'zod';

import { tryParseUsingZodSchema } from '@llm/commons';
import { SdkInvalidFileFormatError, SdkInvalidRequestError } from '@llm/sdk';

import type { ExtractedFile } from './try-extract-single-file';

export function tryExtractFiles<T extends z.ZodRawShape>(
schema?: z.ZodObject<T>,
): (body: Record<string, any>) => TE.TaskEither<
SdkInvalidFileFormatError | SdkInvalidRequestError,
z.infer<z.ZodObject<T>> & { files: readonly ExtractedFile[]; }
> {
const baseSchema = z.object({});
const finalSchema = schema
? baseSchema.merge(schema)
: baseSchema;

return (body: Record<string, any>) => pipe(
TE.fromEither(
pipe(
body,
tryParseUsingZodSchema(finalSchema),
E.mapLeft(error => new SdkInvalidRequestError(error.context)),
),
),
TE.chainW(parsedPayload => pipe(
extractAllFilesFromObject(body),
TE.traverseArray(file => (
TE.tryCatch(
async (): Promise<ExtractedFile> => ({
buffer: Buffer.from(await file.arrayBuffer()),
mimeType: file.type,
fileName: file.name,
}),
() => new SdkInvalidFileFormatError({
name: file.name,
mimeType: file.type,
}),
)),
),
TE.map(extractedFiles => ({
...parsedPayload as z.infer<z.ZodObject<T>>,
files: extractedFiles,
})),
)),
);
}

function extractAllFilesFromObject(obj: Record<string, any>) {
return Object
.values(obj)
.filter(value => value instanceof File);
}
15 changes: 15 additions & 0 deletions apps/backend/src/modules/chats/chats.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createUnarchiveRecordsQuery,
DatabaseConnectionRepo,
DatabaseError,
TableId,
TableUuid,
TransactionalAttrs,
tryGetFirstOrNotExists,
Expand Down Expand Up @@ -44,6 +45,8 @@ export class ChatsRepo extends createProtectedDatabaseRepo('chats') {

unarchiveRecords = createUnarchiveRecordsQuery(this.baseRepo.queryFactoryAttrs);

findById = this.baseRepo.findById;

create = (
{
forwardTransaction,
Expand Down Expand Up @@ -145,6 +148,7 @@ export class ChatsRepo extends createProtectedDatabaseRepo('chats') {

'projects.id as project_id',
'projects.name as project_name',
'projects.internal as project_internal',
])
.limit(ids.length)
.execute(),
Expand All @@ -169,6 +173,7 @@ export class ChatsRepo extends createProtectedDatabaseRepo('chats') {

project_id: projectId,
project_name: projectName,
project_internal: projectInternal,

...item
}): ChatTableRowWithRelations => ({
Expand All @@ -177,6 +182,7 @@ export class ChatsRepo extends createProtectedDatabaseRepo('chats') {
? {
id: projectId,
name: projectName,
internal: !!projectInternal,
}
: null,
organization: {
Expand All @@ -203,6 +209,15 @@ export class ChatsRepo extends createProtectedDatabaseRepo('chats') {
);
};

assignToProject = ({ forwardTransaction, id, projectId }: TransactionalAttrs<{ id: TableUuid; projectId: TableId; }>) =>
this.baseRepo.update({
forwardTransaction,
id,
value: {
projectId,
},
});

update = ({ forwardTransaction, id, value }: TransactionalAttrs<{ id: TableUuid; value: SdkUpdateChatInputT; }>) => {
const { summary } = value;

Expand Down
7 changes: 6 additions & 1 deletion apps/backend/src/modules/chats/chats.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@llm/sdk';

import { WithAuthFirewall } from '../auth';
import { TableRowWithUuid } from '../database';
import { TableId, TableRowWithUuid, TableUuid } from '../database';
import { ChatsFirewall } from './chats.firewall';
import { ChatsRepo } from './chats.repo';
import { ChatsEsIndexRepo, ChatsEsSearchRepo } from './elasticsearch';
Expand Down Expand Up @@ -50,4 +50,9 @@ export class ChatsService implements WithAuthFirewall<ChatsFirewall> {
this.repo.update({ id, value }),
TE.tap(() => this.esIndexRepo.findAndIndexDocumentById(id)),
);

assignToProject = (id: TableUuid, projectId: TableId) => pipe(
this.repo.assignToProject({ id, projectId }),
TE.tap(() => this.esIndexRepo.findAndIndexDocumentById(id)),
);
}
8 changes: 6 additions & 2 deletions apps/backend/src/modules/chats/chats.tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type ChatsTable =
& {
creator_user_id: ColumnType<TableId, TableId, never>;
organization_id: ColumnType<TableId, TableId, never>;
project_id: ColumnType<TableId, TableId, never> | null;
project_id: TableId | null;
public: boolean;
internal: boolean;
};
Expand All @@ -31,11 +31,15 @@ type ChatSummaryTableRowRelation = DropTableRowAccessTime<
Omit<ChatSummaryTableRow, 'chatId' | 'lastSummarizedMessageId'>
>;

type ChatProjectTableRowRelation = TableRowWithIdName & {
internal: boolean;
};

export type ChatTableRowWithRelations =
& Omit<ChatTableRow, 'organizationId' | 'creatorUserId' | 'projectId'>
& {
summary: ChatSummaryTableRowRelation;
organization: TableRowWithIdName;
project: TableRowWithIdName | null;
project: ChatProjectTableRowRelation | null;
creator: UserTableRowBaseRelation;
};
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ const MessagesAbstractEsIndexRepo = createElasticsearchIndexRepo({
uuid: true,
}),
chat: createIdObjectMapping({}, 'keyword'),
repliedMessage: createIdObjectMapping({}, 'keyword'),
replied_message: createIdObjectMapping({}, 'keyword'),
creator: createIdObjectMapping(),
aiModel: createIdObjectMapping(),
ai_model: createIdObjectMapping(),
app: createIdObjectMapping(),
content: {
type: 'text',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,28 @@ export function createAttachAppAIMessage(app: AttachableApp): string {
`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}.`,
'',
'--- MANDATORY RESPONSE FORMAT ---',
`EVERY response when using this app MUST start with "#app:${app.id}" tag`,
'This rule applies to:',
'- Regular responses',
'- Error messages',
'- Debug mode outputs',
'- Any other type of response',
'Format examples:',
`❌ WRONG: "Here's what I found..."`,
`✅ CORRECT: "#app:${app.id} Here's what I found..."`,
`❌ WRONG: "Sorry, I can't help with that"`,
`✅ CORRECT: "#app:${app.id} Sorry, I can't help with that"`,
'',
'--- FILE CONTEXT HANDLING ---',
'When user asks about project files:',
'- Stay in character as the app while analyzing files',
'- Provide insights and explanations within the app\'s specific domain/purpose',
'- Do not break character or switch to a general assistant mode',
'- Keep using the app\'s designated response format and style',
'- If file analysis is outside app\'s scope, politely explain this while staying in character',
'- Continue using action buttons and app-specific formatting even when discussing files',
'',
'--- EXAMPLE BUTTONS: ENGLISH ---',
'For yes/no questions in English:',
Expand Down
37 changes: 37 additions & 0 deletions apps/backend/src/modules/messages/messages.repo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import camelcaseKeys from 'camelcase-keys';
import { array as A, taskEither as TE } from 'fp-ts';
import { pipe } from 'fp-ts/lib/function';
import { sql } from 'kysely';
import { jsonBuildObject } from 'kysely/helpers/postgres';
import { injectable } from 'tsyringe';

import {
Expand Down Expand Up @@ -46,6 +48,38 @@ export class MessagesRepo extends createDatabaseRepo('messages') {

'reply_users.id as reply_message_creator_user_id',
'reply_users.email as reply_message_creator_email',

eb => eb
.selectFrom('projects_files')
.leftJoin('s3_resources', 's3_resources.id', 'projects_files.s3_resource_id')
.leftJoin('s3_resources_buckets', 's3_resources_buckets.id', 's3_resources.bucket_id')
.where('projects_files.message_id', '=', eb.ref('messages.id'))
.select(eb => [
eb.fn.coalesce(
jsonBuildObject({
files: eb.fn.jsonAgg(
jsonBuildObject({
id: eb.ref('projects_files.id'),
resource: jsonBuildObject({
id: eb.ref('s3_resources.id').$notNull(),
name: eb.ref('s3_resources.name').$notNull(),
type: eb.ref('s3_resources.type').$notNull(),
s3Key: eb.ref('s3_resources.s3_key').$notNull(),
createdAt: eb.ref('s3_resources.created_at').$notNull(),
updatedAt: eb.ref('s3_resources.updated_at').$notNull(),
publicUrl: sql<string>`${eb.ref('s3_resources_buckets.public_base_url')} || '/' || ${eb.ref('s3_resources.s3_key')}`,
bucket: jsonBuildObject({
id: eb.ref('s3_resources_buckets.id').$notNull(),
name: eb.ref('s3_resources_buckets.name').$notNull(),
}),
}),
}),
),
}),
sql`'[]'`,
).as('files'),
])
.as('files_json'),
])
.selectAll('messages')
.limit(ids.length)
Expand All @@ -72,6 +106,8 @@ export class MessagesRepo extends createDatabaseRepo('messages') {
app_id: appId,
app_name: appName,

files_json: filesJson,

...item
}): MessageTableRowWithRelations => ({
...camelcaseKeys(item),
Expand Down Expand Up @@ -109,6 +145,7 @@ export class MessagesRepo extends createDatabaseRepo('messages') {
name: appName,
}
: null,
files: filesJson?.files ?? [],
})),
),
);
Expand Down
Loading

0 comments on commit 50ee094

Please sign in to comment.