diff --git a/apps/admin/src/modules/apps-categories/form/shared/app-category-shared-form-fields.tsx b/apps/admin/src/modules/apps-categories/form/shared/app-category-shared-form-fields.tsx index 4a8e78cb..05eb1cc1 100644 --- a/apps/admin/src/modules/apps-categories/form/shared/app-category-shared-form-fields.tsx +++ b/apps/admin/src/modules/apps-categories/form/shared/app-category-shared-form-fields.tsx @@ -37,6 +37,7 @@ export const AppCategorySharedFormFields = controlled(({ > (({ errors, organizat > pipe( + { + id: context.req.param().chatId, + }, + appsService.asUser(context.var.jwt).summarizeChatToApp, + rejectUnsafeSdkErrors, + serializeSdkResponseTE>(context), + )) .get( '/search', sdkSchemaValidator('query', SdKSearchAppsInputV), diff --git a/apps/backend/src/modules/apps/apps.firewall.ts b/apps/backend/src/modules/apps/apps.firewall.ts index f4ccba26..2ff65f1e 100644 --- a/apps/backend/src/modules/apps/apps.firewall.ts +++ b/apps/backend/src/modules/apps/apps.firewall.ts @@ -43,4 +43,9 @@ export class AppsFirewall extends AuthFirewallService { this.appsService.get, this.tryTEIfUser.is.root, ); + + summarizeChatToApp = flow( + this.appsService.summarizeChatToApp, + this.tryTEIfUser.is.root, + ); } diff --git a/apps/backend/src/modules/apps/apps.service.ts b/apps/backend/src/modules/apps/apps.service.ts index 8c144711..e5e0df4a 100644 --- a/apps/backend/src/modules/apps/apps.service.ts +++ b/apps/backend/src/modules/apps/apps.service.ts @@ -1,13 +1,6 @@ import { taskEither as TE } from 'fp-ts'; import { pipe } from 'fp-ts/lib/function'; -import { inject, injectable } from 'tsyringe'; - -import type { - SdkCreateAppInputT, - SdkJwtTokenT, - SdkTableRowIdT, - SdkUpdateAppInputT, -} from '@llm/sdk'; +import { delay, inject, injectable } from 'tsyringe'; import { asyncIteratorToVoidPromise, @@ -15,6 +8,15 @@ import { tapAsyncIterator, tryOrThrowTE, } from '@llm/commons'; +import { + SdkAppFromChatV, + type SdkCreateAppInputT, + type SdkJwtTokenT, + type SdkTableRowIdT, + type SdkTableRowWithUuidT, + type SdkUpdateAppInputT, +} from '@llm/sdk'; +import { ChatsSummariesService } from '~/modules/chats-summaries'; import type { WithAuthFirewall } from '../auth'; import type { TableId, TableRowWithId } from '../database'; @@ -29,12 +31,24 @@ export class AppsService implements WithAuthFirewall { @inject(AppsRepo) private readonly repo: AppsRepo, @inject(AppsEsSearchRepo) private readonly esSearchRepo: AppsEsSearchRepo, @inject(AppsEsIndexRepo) private readonly esIndexRepo: AppsEsIndexRepo, + @inject(delay(() => ChatsSummariesService)) private readonly chatsSummariesService: Readonly, ) {} asUser = (jwt: SdkJwtTokenT) => new AppsFirewall(jwt, this); get = this.esSearchRepo.get; + summarizeChatToApp = ({ id }: SdkTableRowWithUuidT) => + this.chatsSummariesService.summarizeChatUsingSchema({ + id, + schema: SdkAppFromChatV, + prompt: + 'Summarize this chat to an app. Extract the core functionality and create a system context that defines what this app is.' + + ' If the chat demonstrates advisory patterns, create a system context for a specialized advisor. For teaching patterns - an expert mentor.' + + ' Focus on describing the app\'s purpose, expertise areas, behavior patterns, and how it should interact with users.' + + ' The output will be used directly as a system context, so write it as a clear definition of the app\'s role and capabilities.', + }); + archiveSeqByOrganizationId = (organizationId: SdkTableRowIdT) => TE.fromTask( pipe( this.repo.createIdsIterator({ diff --git a/apps/backend/src/modules/chats-summaries/chats-summaries.repo.ts b/apps/backend/src/modules/chats-summaries/chats-summaries.repo.ts index a523c97b..b3e691f7 100644 --- a/apps/backend/src/modules/chats-summaries/chats-summaries.repo.ts +++ b/apps/backend/src/modules/chats-summaries/chats-summaries.repo.ts @@ -25,7 +25,7 @@ export class ChatsSummariesRepo extends createDatabaseRepo('chat_summaries') { transaction(qb => this .createSummarizeChatsQuery(qb) - .select(({ fn }) => fn.count('id').as('count')) + .select(({ fn }) => fn.count('summary.id').as('count')) .executeTakeFirstOrThrow() .then(result => +result.count as number), ), @@ -37,8 +37,8 @@ export class ChatsSummariesRepo extends createDatabaseRepo('chat_summaries') { pipe( this .createSummarizeChatsQuery() - .select(['id', 'chat_id as chatId']) - .orderBy('created_at', 'asc'), + .select(['summary.id', 'summary.chat_id as chatId']) + .orderBy('summary.created_at', 'asc'), this.queryBuilder.createChunkedIterator({ chunkSize: 100, }), @@ -107,18 +107,22 @@ export class ChatsSummariesRepo extends createDatabaseRepo('chat_summaries') { }; private createSummarizeChatsQuery = (qb: KyselyDatabase = this.db) => qb - .selectFrom(this.table) + .selectFrom(`${this.table} as summary`) + .innerJoin('chats as chat', 'chat.id', 'summary.chat_id') .where(({ and, or, eb }) => and([ + // Check if the chat is not internal + eb('chat.internal', '=', false), + // Check if something can be summarized or([ - eb('content_generated', 'is', true), - eb('name_generated', 'is', true), + eb('summary.content_generated', 'is', true), + eb('summary.name_generated', 'is', true), ]), // Check if the chat was not summarized recently or([ - eb('name_generated_at', 'is', null), - eb('content_generated_at', 'is', null), + eb('summary.name_generated_at', 'is', null), + eb('summary.content_generated_at', 'is', null), eb( sql`GREATEST(name_generated_at, content_generated_at)`, '<', @@ -128,13 +132,13 @@ export class ChatsSummariesRepo extends createDatabaseRepo('chat_summaries') { // Check if there is message younger than the last summary or([ - eb('last_summarized_message_id', 'is', null), + eb('summary.last_summarized_message_id', 'is', null), eb.exists( this.db .selectFrom('messages') - .select('id') - .where('chat_id', '=', eb.ref('chat_summaries.chat_id')) - .where('created_at', '>', sql`GREATEST(name_generated_at, content_generated_at)`), + .select('messages.id') + .where('messages.chat_id', '=', eb.ref('summary.chat_id')) + .where('messages.created_at', '>', sql`GREATEST(name_generated_at, content_generated_at)`), ), ]), @@ -143,10 +147,10 @@ export class ChatsSummariesRepo extends createDatabaseRepo('chat_summaries') { eb.exists( this.db .selectFrom('messages') - .select('id') - .where('chat_id', '=', eb.ref('chat_summaries.chat_id')) - .where('role', '=', 'assistant') - .where('ai_model_id', 'is not', null), + .select('messages.id') + .where('messages.chat_id', '=', eb.ref('summary.chat_id')) + .where('messages.role', '=', 'assistant') + .where('messages.ai_model_id', 'is not', null), ), ])); diff --git a/apps/backend/src/modules/chats-summaries/chats-summaries.service.ts b/apps/backend/src/modules/chats-summaries/chats-summaries.service.ts index 43c8121e..fd6738db 100644 --- a/apps/backend/src/modules/chats-summaries/chats-summaries.service.ts +++ b/apps/backend/src/modules/chats-summaries/chats-summaries.service.ts @@ -33,7 +33,7 @@ export class ChatsSummariesService { summarizeChats = ({ ids }: { ids: TableUuid[]; }) => pipe( ids, - A.map(id => this.summarizeChat({ id })), + A.map(id => this.summarizeChatAndUpdate({ id })), TE.sequenceSeqArray, ); @@ -50,7 +50,7 @@ export class ChatsSummariesService { await pipe( chats, A.map(summary => pipe( - this.summarizeChat({ + this.summarizeChatAndUpdate({ id: summary.chatId, }), tapTaskEitherError((error) => { @@ -65,7 +65,16 @@ export class ChatsSummariesService { ))), ); - private summarizeChat = ({ id }: TableRowWithUuid) => pipe( + summarizeChatUsingSchema = ( + { + id, + schema, + prompt, + }: TableRowWithUuid & { + prompt: string; + schema: S; + }, + ) => pipe( TE.Do, TE.bind('chat', () => this.messagesService.searchByChatId(id)), TE.bindW('aiModel', ({ chat }) => { @@ -77,29 +86,37 @@ export class ChatsSummariesService { return TE.left(new MissingAIModelInChatError(chat)); }), - TE.bindW('summarize', ({ chat, aiModel }) => pipe( + TE.chainW(({ chat, aiModel }) => pipe( this.aiConnectorService.executeInstructedPrompt({ aiModel, history: chat.items, - message: + message: prompt, + schema, + }), + )), + ); + + private summarizeChatAndUpdate = ({ id }: TableRowWithUuid) => pipe( + this.summarizeChatUsingSchema({ + id, + schema: z.object({ + title: z.string(), + description: z.string(), + }), + prompt: '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.' + 'Do not summarize the messages about describing app (these ones defined in chat).', - schema: z.object({ - title: z.string(), - description: z.string(), - }), - }), - TE.orElse((error) => { - this.logger.error('Failed to summarize chat', { error }); + }), + TE.orElseW((error) => { + this.logger.error('Failed to summarize chat', { error }); - return TE.of({ - title: 'Untitled chat', - description: 'Cannot summarize chat. Please do it manually.', - }); - }), - )), - TE.chainW(({ summarize }) => this.repo.updateGeneratedSummarizeByChatId( + return TE.of({ + title: 'Untitled chat', + description: 'Cannot summarize chat. Please do it manually.', + }); + }), + TE.chainW(summarize => this.repo.updateGeneratedSummarizeByChatId( { chatId: id, name: summarize.title, diff --git a/apps/backend/src/modules/messages/messages.service.ts b/apps/backend/src/modules/messages/messages.service.ts index df7701eb..c39f5f39 100644 --- a/apps/backend/src/modules/messages/messages.service.ts +++ b/apps/backend/src/modules/messages/messages.service.ts @@ -2,7 +2,7 @@ import type { ChatCompletionChunk } from 'openai/resources/index.mjs'; import { taskEither as TE } from 'fp-ts'; import { pipe } from 'fp-ts/lib/function'; -import { inject, injectable } from 'tsyringe'; +import { delay, inject, injectable } from 'tsyringe'; import { findItemIndexById, mapAsyncIterator, tryOrThrowTE } from '@llm/commons'; import { @@ -38,7 +38,7 @@ export type AttachAppInputT = { export class MessagesService implements WithAuthFirewall { constructor( @inject(MessagesRepo) private readonly repo: MessagesRepo, - @inject(AppsService) private readonly appsService: AppsService, + @inject(delay(() => AppsService)) private readonly appsService: Readonly, @inject(MessagesEsSearchRepo) private readonly esSearchRepo: MessagesEsSearchRepo, @inject(MessagesEsIndexRepo) private readonly esIndexRepo: MessagesEsIndexRepo, @inject(AIConnectorService) private readonly aiConnectorService: AIConnectorService, diff --git a/apps/chat/src/modules/apps-creator/creator/app-create-form-modal.tsx b/apps/chat/src/modules/apps-creator/creator/app-create-form-modal.tsx index b4e0784c..3608299f 100644 --- a/apps/chat/src/modules/apps-creator/creator/app-create-form-modal.tsx +++ b/apps/chat/src/modules/apps-creator/creator/app-create-form-modal.tsx @@ -24,7 +24,7 @@ export function AppCreateFormModal({ }: AppCreateFormModalProps) { const t = useI18n().pack.appsCreator; const [currentStep, setCurrentStep] = useState(1); - const { handleSubmitEvent, validator, submitState, bind, value } = useAppCreateForm({ + const { handleSubmitEvent, validator, submitState, bind, value, setValue } = useAppCreateForm({ defaultValue, onAfterSubmit, }); @@ -51,19 +51,25 @@ export function AppCreateFormModal({ >
setCurrentStep(2)} loading={submitState.loading} - value={value} + onNext={(summarizedChat) => { + setValue({ + merge: true, + value: summarizedChat, + }); + + setCurrentStep(2); + }} />
setCurrentStep(1)} + organization={value.organization} loading={submitState.loading} - value={value} - errors={validator.errors.all} - bind={bind.merged()} + errors={validator.errors.all as any} + {...bind.merged()} />
diff --git a/apps/chat/src/modules/apps-creator/creator/steps/app-create-form-step1.tsx b/apps/chat/src/modules/apps-creator/creator/steps/app-create-form-step1.tsx index db4459c5..256615fa 100644 --- a/apps/chat/src/modules/apps-creator/creator/steps/app-create-form-step1.tsx +++ b/apps/chat/src/modules/apps-creator/creator/steps/app-create-form-step1.tsx @@ -1,27 +1,59 @@ +import { pipe } from 'fp-ts/lib/function'; +import { useState } from 'react'; + +import { runTaskAsVoid, tapTaskEither, tryOrThrowTE } from '@llm/commons'; +import { useAsyncCallback } from '@llm/commons-front'; +import { + type SdkAppFromChatT, + type SdkChatT, + type SdkTableRowWithUuidT, + useSdkForLoggedIn, +} from '@llm/sdk'; +import { FormSpinnerCTA } from '@llm/ui'; import { useI18n } from '~/i18n'; import { InternalConversationPanel } from '~/modules/chats'; -import type { StepProps } from './app-create-form-types'; +type Props = { + loading: boolean; + onNext: (chat: SdkAppFromChatT) => void; +}; -export function AppCreateFormStep1({ onNext, loading }: StepProps) { +export function AppCreateFormStep1({ onNext, loading }: Props) { + const [chat, setChat] = useState(null); const t = useI18n().pack.appsCreator; + const { sdks } = useSdkForLoggedIn(); + const [onSummarizeChat, summarizeState] = useAsyncCallback( + async (chat: SdkTableRowWithUuidT) => pipe( + sdks.dashboard.apps.summarizeChatToApp(chat.id), + tapTaskEither(onNext), + tryOrThrowTE, + runTaskAsVoid, + ), + ); + return (
- +
); diff --git a/apps/chat/src/modules/apps-creator/creator/steps/app-create-form-step2.tsx b/apps/chat/src/modules/apps-creator/creator/steps/app-create-form-step2.tsx index df30697e..a5c125c2 100644 --- a/apps/chat/src/modules/apps-creator/creator/steps/app-create-form-step2.tsx +++ b/apps/chat/src/modules/apps-creator/creator/steps/app-create-form-step2.tsx @@ -1,20 +1,19 @@ import { useI18n } from '~/i18n'; -import type { StepProps } from './app-create-form-types'; +import { AppSharedFormFields, type AppSharedFormFieldsProps } from '../../shared'; -import { AppSharedFormFields } from '../../shared'; +type StepProps = AppSharedFormFieldsProps & { + onBack?: () => void; + loading?: boolean; +}; -export function AppCreateFormStep2({ onBack, loading, value, errors, bind }: StepProps & { errors: any; bind: any; }) { +export function AppCreateFormStep2({ onBack, loading, ...props }: StepProps) { const { pack } = useI18n(); const t = pack.appsCreator; return ( <> - +