Skip to content

Commit

Permalink
feat(chat): add working editor
Browse files Browse the repository at this point in the history
  • Loading branch information
Mati365 committed Dec 8, 2024
1 parent 66e2bd7 commit be61487
Show file tree
Hide file tree
Showing 18 changed files with 187 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const AppCategorySharedFormFields = controlled<Value, Props>(({
>
<AppsCategoriesSearchSelect
{...bind.path('parentCategory')}
required
key={organization.id}
filters={{
archived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const AppSharedFormFields = controlled<Value, Props>(({ errors, organizat
>
<AppsCategoriesSearchSelect
{...bind.path('category')}
required
key={organization.id}
filters={{
archived: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ export class AppsController extends AuthorizedController {
super(configService);

this.router
.get('/summarize-chat-to-app/:chatId', async context => pipe(
{
id: context.req.param().chatId,
},
appsService.asUser(context.var.jwt).summarizeChatToApp,
rejectUnsafeSdkErrors,
serializeSdkResponseTE<ReturnType<AppsSdk['summarizeChatToApp']>>(context),
))
.get(
'/search',
sdkSchemaValidator('query', SdKSearchAppsInputV),
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 @@ -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,
);
}
30 changes: 22 additions & 8 deletions apps/backend/src/modules/apps/apps.service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
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,
runTaskAsVoid,
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';
Expand All @@ -29,12 +31,24 @@ export class AppsService implements WithAuthFirewall<AppsFirewall> {
@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<ChatsSummariesService>,
) {}

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({
Expand Down
36 changes: 20 additions & 16 deletions apps/backend/src/modules/chats-summaries/chats-summaries.repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export class ChatsSummariesRepo extends createDatabaseRepo('chat_summaries') {
transaction(qb =>
this
.createSummarizeChatsQuery(qb)
.select(({ fn }) => fn.count<number>('id').as('count'))
.select(({ fn }) => fn.count<number>('summary.id').as('count'))
.executeTakeFirstOrThrow()
.then(result => +result.count as number),
),
Expand All @@ -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,
}),
Expand Down Expand Up @@ -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)`,
'<',
Expand All @@ -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<Date>`GREATEST(name_generated_at, content_generated_at)`),
.select('messages.id')
.where('messages.chat_id', '=', eb.ref('summary.chat_id'))
.where('messages.created_at', '>', sql<Date>`GREATEST(name_generated_at, content_generated_at)`),
),

]),
Expand All @@ -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),
),
]));

Expand Down
55 changes: 36 additions & 19 deletions apps/backend/src/modules/chats-summaries/chats-summaries.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);

Expand All @@ -50,7 +50,7 @@ export class ChatsSummariesService {
await pipe(
chats,
A.map(summary => pipe(
this.summarizeChat({
this.summarizeChatAndUpdate({
id: summary.chatId,
}),
tapTaskEitherError((error) => {
Expand All @@ -65,7 +65,16 @@ export class ChatsSummariesService {
))),
);

private summarizeChat = ({ id }: TableRowWithUuid) => pipe(
summarizeChatUsingSchema = <S extends z.AnyZodObject>(
{
id,
schema,
prompt,
}: TableRowWithUuid & {
prompt: string;
schema: S;
},
) => pipe(
TE.Do,
TE.bind('chat', () => this.messagesService.searchByChatId(id)),
TE.bindW('aiModel', ({ chat }) => {
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/modules/messages/messages.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -38,7 +38,7 @@ export type AttachAppInputT = {
export class MessagesService implements WithAuthFirewall<MessagesFirewall> {
constructor(
@inject(MessagesRepo) private readonly repo: MessagesRepo,
@inject(AppsService) private readonly appsService: AppsService,
@inject(delay(() => AppsService)) private readonly appsService: Readonly<AppsService>,
@inject(MessagesEsSearchRepo) private readonly esSearchRepo: MessagesEsSearchRepo,
@inject(MessagesEsIndexRepo) private readonly esIndexRepo: MessagesEsIndexRepo,
@inject(AIConnectorService) private readonly aiConnectorService: AIConnectorService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand All @@ -51,19 +51,25 @@ export function AppCreateFormModal({
>
<div className={clsx({ block: currentStep === 1, hidden: currentStep !== 1 })}>
<AppCreateFormStep1
onNext={() => setCurrentStep(2)}
loading={submitState.loading}
value={value}
onNext={(summarizedChat) => {
setValue({
merge: true,
value: summarizedChat,
});

setCurrentStep(2);
}}
/>
</div>

<div className={clsx({ block: currentStep === 2, hidden: currentStep !== 2 })}>
<AppCreateFormStep2
onBack={() => 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()}
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<SdkChatT | null>(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 (
<div className="flex flex-col h-[75vh]">
<InternalConversationPanel
className="flex-1 overflow-y-auto"
initialMessage={t.prompts.createApp}
onChatCreated={setChat}
/>

<div>
<button
<FormSpinnerCTA
type="button"
className="uk-float-right uk-button uk-button-primary"
onClick={onNext}
disabled={loading}
loading={summarizeState.isLoading}
className="uk-float-right"
onClick={() => {
if (chat) {
void onSummarizeChat(chat);
}
}}
disabled={loading || !chat}
>
{t.create.nextStep}
</button>
</FormSpinnerCTA>
</div>
</div>
);
Expand Down
Loading

0 comments on commit be61487

Please sign in to comment.