feat: agents
This commit is contained in:
parent
e0d4661e87
commit
a0054ac2de
|
|
@ -7,6 +7,7 @@ import {
|
|||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
|
||||
|
|
@ -23,6 +24,8 @@ import { ApiTags } from '@nestjs/swagger';
|
|||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
import { Response } from 'express';
|
||||
|
||||
@ApiTags('Posts')
|
||||
@Controller('/posts')
|
||||
|
|
@ -30,7 +33,8 @@ export class PostsController {
|
|||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _starsService: StarsService,
|
||||
private _messagesService: MessagesService
|
||||
private _messagesService: MessagesService,
|
||||
private _agentGraphService: AgentGraphService
|
||||
) {}
|
||||
|
||||
@Get('/marketplace/:id?')
|
||||
|
|
@ -100,11 +104,20 @@ export class PostsController {
|
|||
|
||||
@Post('/generator')
|
||||
@CheckPolicies([AuthorizationActions.Create, Sections.POSTS_PER_MONTH])
|
||||
generatePosts(
|
||||
async generatePosts(
|
||||
@GetOrgFromRequest() org: Organization,
|
||||
@Body() body: GeneratorDto
|
||||
@Body() body: GeneratorDto,
|
||||
@Res({ passthrough: false }) res: Response
|
||||
) {
|
||||
return this._postsService.generatePosts(org.id, body);
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||
for await (const event of this._agentGraphService.start(
|
||||
org.id,
|
||||
body,
|
||||
)) {
|
||||
res.write(JSON.stringify(event) + '\n');
|
||||
}
|
||||
|
||||
res.end();
|
||||
}
|
||||
|
||||
@Delete('/:group')
|
||||
|
|
|
|||
|
|
@ -10,14 +10,29 @@ import { GetUserFromRequest } from '@gitroom/nestjs-libraries/user/user.from.req
|
|||
import { User } from '@prisma/client';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service';
|
||||
|
||||
@ApiTags('Public')
|
||||
@Controller('/public')
|
||||
export class PublicController {
|
||||
constructor(
|
||||
private _agenciesService: AgenciesService,
|
||||
private _trackService: TrackService
|
||||
private _trackService: TrackService,
|
||||
private _agentGraphInsertService: AgentGraphInsertService
|
||||
) {}
|
||||
@Post('/agent')
|
||||
async createAgent(@Body() body: { text: string; apiKey: string }) {
|
||||
if (
|
||||
!body.apiKey ||
|
||||
!process.env.AGENT_API_KEY ||
|
||||
body.apiKey !== process.env.AGENT_API_KEY
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return this._agentGraphInsertService.newPost(body.text);
|
||||
}
|
||||
|
||||
@Get('/agencies-list')
|
||||
async getAgencyByUser() {
|
||||
return this._agenciesService.getAllAgencies();
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { PluginModule } from '@gitroom/plugins/plugin.module';
|
|||
import { PublicApiModule } from '@gitroom/backend/public-api/public.api.module';
|
||||
import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
|
|
@ -18,6 +19,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
|
|||
ApiModule,
|
||||
PluginModule,
|
||||
PublicApiModule,
|
||||
AgentModule,
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: 3600000,
|
||||
|
|
|
|||
|
|
@ -25,17 +25,7 @@
|
|||
"executor": "nx:run-commands",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "commands:build",
|
||||
"inspect": false,
|
||||
"command": "cd dist/apps/commands && node main.js"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "commands:build:development"
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "commands:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,18 @@ import { DatabaseModule } from '@gitroom/nestjs-libraries/database/prisma/databa
|
|||
import { RefreshTokens } from './tasks/refresh.tokens';
|
||||
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
|
||||
import { ConfigurationTask } from './tasks/configuration';
|
||||
import { AgentRun } from './tasks/agent.run';
|
||||
import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module';
|
||||
|
||||
@Module({
|
||||
imports: [ExternalCommandModule, DatabaseModule, BullMqModule],
|
||||
imports: [
|
||||
ExternalCommandModule,
|
||||
DatabaseModule,
|
||||
BullMqModule,
|
||||
AgentModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [CheckStars, RefreshTokens, ConfigurationTask],
|
||||
providers: [CheckStars, RefreshTokens, ConfigurationTask, AgentRun],
|
||||
get exports() {
|
||||
return [...this.imports, ...this.providers];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import { Command } from 'nestjs-command';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
|
||||
@Injectable()
|
||||
export class AgentRun {
|
||||
constructor(private _agentGraphService: AgentGraphService) {}
|
||||
@Command({
|
||||
command: 'run:agent',
|
||||
describe: 'Run the agent',
|
||||
})
|
||||
async agentRun() {
|
||||
console.log(await this._agentGraphService.createGraph('hello', true));
|
||||
}
|
||||
}
|
||||
|
|
@ -394,4 +394,25 @@ div div .set-font-family {
|
|||
transform: translate(-50%, -50%);
|
||||
white-space: nowrap;
|
||||
opacity: 30%;
|
||||
}
|
||||
|
||||
.loading-shimmer {
|
||||
position: relative;
|
||||
color: rgba(255, 255, 255, .5);
|
||||
}
|
||||
|
||||
.loading-shimmer:before {
|
||||
content: attr(data-text);
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
animation: loading 4s linear 0s infinite;
|
||||
filter: blur(0.4px);
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
max-width: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -66,8 +66,13 @@ export const AddEditModal: FC<{
|
|||
integrations: Integrations[];
|
||||
reopenModal: () => void;
|
||||
mutate: () => void;
|
||||
onlyValues?: Array<{
|
||||
content: string;
|
||||
id?: string;
|
||||
image?: Array<{ id: string; path: string }>;
|
||||
}>;
|
||||
}> = (props) => {
|
||||
const { date, integrations: ints, reopenModal, mutate } = props;
|
||||
const { date, integrations: ints, reopenModal, mutate, onlyValues } = props;
|
||||
const [customer, setCustomer] = useState('');
|
||||
|
||||
// selected integrations to allow edit
|
||||
|
|
@ -104,7 +109,7 @@ export const AddEditModal: FC<{
|
|||
id?: string;
|
||||
image?: Array<{ id: string; path: string }>;
|
||||
}>
|
||||
>([{ content: '' }]);
|
||||
>(onlyValues ? onlyValues : [{ content: '' }]);
|
||||
|
||||
const fetch = useFetch();
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const Editor = forwardRef<
|
|||
const user = useUser();
|
||||
useCopilotReadable({
|
||||
description: 'Content of the post number ' + (props.order + 1),
|
||||
value: props.content,
|
||||
value: props.value,
|
||||
});
|
||||
|
||||
useCopilotAction({
|
||||
|
|
|
|||
|
|
@ -1,193 +1,30 @@
|
|||
import React, {
|
||||
FC,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { useUser } from '@gitroom/frontend/components/layout/user.context';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { deleteDialog } from '@gitroom/react/helpers/delete.dialog';
|
||||
import {
|
||||
Step,
|
||||
StepSpace,
|
||||
} from '@gitroom/frontend/components/onboarding/onboarding';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
import dayjs from 'dayjs';
|
||||
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
import { Button } from '@gitroom/react/form/button';
|
||||
import { PostSelector } from '@gitroom/frontend/components/post-url-selector/post.url.selector';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import { Textarea } from '@gitroom/react/form/textarea';
|
||||
import { Checkbox } from '@gitroom/react/form/checkbox';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
CalendarWeekProvider,
|
||||
useCalendar,
|
||||
} from '@gitroom/frontend/components/launches/calendar.context';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/launches/add.edit.model';
|
||||
import dayjs from 'dayjs';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
|
||||
const ThirdStep: FC<{ week: number; year: number }> = (props) => {
|
||||
const { week, year } = props;
|
||||
|
||||
const gotToPosts = useCallback(() => {
|
||||
window.location.href = `/launches?week=${week}&year=${year}`;
|
||||
}, [week, year]);
|
||||
return (
|
||||
<div>
|
||||
<div className="text-[20px] mb-[20px] flex flex-col items-center justify-center text-center mt-[20px] gap-[20px]">
|
||||
<img src="/success.svg" alt="success" />
|
||||
Your posts have been scheduled as drafts.
|
||||
<br />
|
||||
<Button onClick={gotToPosts}>Click here to see them</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SecondStep: FC<{
|
||||
posts: Array<Array<{ post: string }>>;
|
||||
url: string;
|
||||
postId?: string;
|
||||
nextStep: (params: { week: number; year: number }) => void;
|
||||
}> = (props) => {
|
||||
const { posts, nextStep, url, postId } = props;
|
||||
const fetch = useFetch();
|
||||
const [selected, setSelected] = useState<Array<string>>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
values: {
|
||||
date: dayjs().week() + '_' + dayjs().year(),
|
||||
},
|
||||
});
|
||||
|
||||
const addPost = useCallback(
|
||||
(index: string) => () => {
|
||||
if (selected.includes(index)) {
|
||||
setSelected(selected.filter((i) => i !== index));
|
||||
return;
|
||||
}
|
||||
setSelected([...selected, index]);
|
||||
},
|
||||
[selected]
|
||||
);
|
||||
|
||||
const list = useMemo(() => {
|
||||
const currentDate = dayjs();
|
||||
return [...new Array(52)].map((_, i) => {
|
||||
const week = currentDate.add(i, 'week');
|
||||
return {
|
||||
value: week.week() + '_' + week.year(),
|
||||
label: `Week #${week.week()} (${week
|
||||
.startOf('isoWeek')
|
||||
.format('YYYY-MM-DD')} - ${week
|
||||
.endOf('isoWeek')
|
||||
.format('YYYY-MM-DD')})`,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const createPosts: SubmitHandler<{
|
||||
date: any;
|
||||
}> = useCallback(
|
||||
async (values) => {
|
||||
setLoading(true);
|
||||
await fetch('/posts/generator/draft', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
posts: posts
|
||||
.filter((_, index) => selected.includes(String(index)))
|
||||
.map((po) => ({ list: po })),
|
||||
url,
|
||||
postId: postId ? `(post:${postId})` : undefined,
|
||||
year: values.date.year,
|
||||
week: values.date.week,
|
||||
}),
|
||||
});
|
||||
setLoading(false);
|
||||
nextStep({
|
||||
week: values.date.week,
|
||||
year: values.date.year,
|
||||
});
|
||||
},
|
||||
[selected, postId, url]
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={form.handleSubmit(createPosts)}>
|
||||
<FormProvider {...form}>
|
||||
<div className={loading ? 'opacity-75' : ''}>
|
||||
<Select
|
||||
label="Select a week"
|
||||
name="date"
|
||||
extraForm={{
|
||||
setValueAs: (value) => {
|
||||
const [week, year] = value.split('_');
|
||||
return { week: +week, year: +year };
|
||||
},
|
||||
}}
|
||||
>
|
||||
{list.map((item) => (
|
||||
<option value={item.value} key={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<div className="text-[20px] mb-[20px]">
|
||||
Click on the posts you would like to schedule.
|
||||
<br />
|
||||
They will be saved as drafts and you can edit them later.
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-[25px] select-none cursor-pointer">
|
||||
{posts.map((post, index) => (
|
||||
<div
|
||||
onClick={addPost(String(index))}
|
||||
className={clsx(
|
||||
'flex flex-col h-[200px] border rounded-[4px] group hover:border-white relative',
|
||||
selected.includes(String(index))
|
||||
? 'border-white'
|
||||
: 'border-fifth'
|
||||
)}
|
||||
key={post[0].post}
|
||||
>
|
||||
{post.length > 1 && (
|
||||
<div className="bg-forth absolute -left-[15px] -top-[15px] z-[100] p-[3px] rounded-[10px]">
|
||||
a thread
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex-1 relative h-full w-full group-hover:bg-black',
|
||||
selected.includes(String(index)) && 'bg-black'
|
||||
)}
|
||||
>
|
||||
<div className="absolute left-0 top-0 w-full h-full p-[16px]">
|
||||
<div className="w-full h-full overflow-hidden text-ellipsis group-hover:bg-black resize-none outline-none">
|
||||
{post[0].post.split('\n\n')[0]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-[20px] flex justify-end">
|
||||
<Button type="submit" disabled={!selected.length} loading={loading}>
|
||||
Create posts
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const FirstStep: FC<{
|
||||
nextStep: (
|
||||
posts: Array<Array<{ post: string }>>,
|
||||
url: string,
|
||||
postId?: string
|
||||
) => void;
|
||||
}> = (props) => {
|
||||
const { nextStep } = props;
|
||||
const FirstStep: FC = (props) => {
|
||||
const { integrations, reloadCalendarView } = useCalendar();
|
||||
const modal = useModals();
|
||||
const fetch = useFetch();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showStep, setShowStep] = useState('');
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(GeneratorDto);
|
||||
}, []);
|
||||
|
|
@ -196,52 +33,130 @@ const FirstStep: FC<{
|
|||
mode: 'all',
|
||||
resolver,
|
||||
values: {
|
||||
url: '',
|
||||
post: undefined as undefined | string,
|
||||
research: '',
|
||||
isPicture: false,
|
||||
format: 'one_short',
|
||||
tone: 'personal',
|
||||
},
|
||||
});
|
||||
|
||||
const [url, post] = form.watch(['url', 'post']);
|
||||
const [research] = form.watch(['research']);
|
||||
|
||||
const makeSelect = useCallback(
|
||||
(post?: string) => {
|
||||
form.setValue('post', post?.split?.(':')[1]?.split(')')?.[0]);
|
||||
const generateStep = useCallback(
|
||||
async (reader: ReadableStreamDefaultReader) => {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
if (!post && !url) {
|
||||
form.setError('url', {
|
||||
message: 'You need to select a post or a URL',
|
||||
});
|
||||
return;
|
||||
let lastResponse = {} as any;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) return lastResponse.data.output;
|
||||
|
||||
// Convert chunked binary data to string
|
||||
const chunkStr = decoder.decode(value, { stream: true });
|
||||
for (const chunk of chunkStr
|
||||
.split('\n')
|
||||
.filter((f) => f && f.indexOf('{') > -1)) {
|
||||
try {
|
||||
const data = JSON.parse(chunk);
|
||||
switch (data.name) {
|
||||
case 'agent':
|
||||
setShowStep('Agent starting');
|
||||
break;
|
||||
case 'research':
|
||||
setShowStep('Researching your content...');
|
||||
break;
|
||||
case 'find-category':
|
||||
setShowStep('Understanding the category...');
|
||||
break;
|
||||
case 'find-topic':
|
||||
setShowStep('Finding the topic...');
|
||||
break;
|
||||
case 'find-popular-posts':
|
||||
setShowStep('Finding popular posts to match with...');
|
||||
break;
|
||||
case 'generate-hook':
|
||||
setShowStep('Generating hook...');
|
||||
break;
|
||||
case 'generate-content':
|
||||
setShowStep('Generating content...');
|
||||
break;
|
||||
case 'generate-picture':
|
||||
setShowStep('Generating pictures...');
|
||||
break;
|
||||
case 'upload-pictures':
|
||||
setShowStep('Uploading pictures...');
|
||||
break;
|
||||
case 'post-time':
|
||||
setShowStep('Finding time to post...');
|
||||
break;
|
||||
}
|
||||
lastResponse = data;
|
||||
} catch (e) {
|
||||
/** don't do anything **/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (post && url) {
|
||||
form.setError('url', {
|
||||
message: 'You can only have a URL or a post',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
form.setError('url', {
|
||||
message: '',
|
||||
});
|
||||
},
|
||||
[post, url]
|
||||
[]
|
||||
);
|
||||
|
||||
const onSubmit: SubmitHandler<{
|
||||
url: string;
|
||||
post: string | undefined;
|
||||
}> = useCallback(async (value) => {
|
||||
setLoading(true);
|
||||
const data = await (
|
||||
await fetch('/posts/generator', {
|
||||
research: string;
|
||||
}> = useCallback(
|
||||
async (value) => {
|
||||
setLoading(true);
|
||||
const response = await fetch('/posts/generator', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(value),
|
||||
})
|
||||
).json();
|
||||
nextStep(data.list, value.url, value.post);
|
||||
setLoading(false);
|
||||
}, []);
|
||||
});
|
||||
|
||||
if (!response.body) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const load = await generateStep(reader);
|
||||
|
||||
const messages = load.content.map((p: any, index: number) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
content: load.hook + '\n' + p.content,
|
||||
...(p?.image?.path ? { image: [p.image] } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: p.content,
|
||||
...(p?.image?.path ? { image: [p.image] } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
setShowStep('');
|
||||
|
||||
modal.openModal({
|
||||
closeOnClickOutside: false,
|
||||
closeOnEscape: false,
|
||||
withCloseButton: false,
|
||||
classNames: {
|
||||
modal: 'w-[100%] max-w-[1400px] bg-transparent text-textColor',
|
||||
},
|
||||
children: (
|
||||
<AddEditModal
|
||||
integrations={integrations.slice(0).map((p) => ({ ...p }))}
|
||||
mutate={reloadCalendarView}
|
||||
date={dayjs.utc(load.date).local()}
|
||||
reopenModal={() => ({})}
|
||||
onlyValues={messages}
|
||||
/>
|
||||
),
|
||||
size: '80%',
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
},
|
||||
[integrations, reloadCalendarView]
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
|
|
@ -250,31 +165,61 @@ const FirstStep: FC<{
|
|||
>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex flex-col">
|
||||
<div className="p-[20px] border border-fifth rounded-[4px]">
|
||||
<div className="pb-[10px] rounded-[4px]">
|
||||
<div className="flex">
|
||||
<div className="flex-1">
|
||||
<Input label="URL" {...form.register('url')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col-reverse">
|
||||
<div className="p-[16px] bg-input border-fifth border rounded-[4px] min-h-[500px] empty:hidden">
|
||||
<PostSelector
|
||||
noModal={true}
|
||||
onClose={() => null}
|
||||
onSelect={makeSelect}
|
||||
date={dayjs().add(1, 'year')}
|
||||
only="article"
|
||||
{!showStep ? (
|
||||
<div className="loading-shimmer pb-[10px]"> </div>
|
||||
) : (
|
||||
<div
|
||||
className="loading-shimmer pb-[10px]"
|
||||
data-text={showStep}
|
||||
>
|
||||
{showStep}
|
||||
</div>
|
||||
)}
|
||||
<Textarea
|
||||
label="Write anything"
|
||||
disabled={loading}
|
||||
placeholder="You can write anything you want, and also add links, we will do the research for you..."
|
||||
{...form.register('research')}
|
||||
/>
|
||||
</div>
|
||||
<div className="pb-[10px] existing-empty">
|
||||
Or select from exising posts
|
||||
<Select label="Output format" {...form.register('format')}>
|
||||
<option value="one_short">Short post</option>
|
||||
<option value="one_long">Long post</option>
|
||||
<option value="thread_short">
|
||||
A thread with short posts
|
||||
</option>
|
||||
<option value="thread_long">A thread with long posts</option>
|
||||
</Select>
|
||||
<Select label="Output format" {...form.register('tone')}>
|
||||
<option value="personal">
|
||||
Personal voice ({'"'}I am happy to announce{'"'})
|
||||
</option>
|
||||
<option value="company">
|
||||
Company voice ({'"'}We are happy to announce{'"'})
|
||||
</option>
|
||||
</Select>
|
||||
<div
|
||||
className={clsx('flex items-center', loading && 'opacity-50')}
|
||||
>
|
||||
<Checkbox
|
||||
disabled={loading}
|
||||
{...form.register('isPicture')}
|
||||
label="Add pictures?"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-[20px] flex justify-end">
|
||||
<Button type="submit" disabled={!!(url && post)} loading={loading}>
|
||||
{url && post ? "You can't have both URL and a POST" : 'Next'}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={research.length < 10}
|
||||
loading={loading}
|
||||
>
|
||||
Generate
|
||||
</Button>
|
||||
</div>
|
||||
</FormProvider>
|
||||
|
|
@ -282,28 +227,14 @@ const FirstStep: FC<{
|
|||
);
|
||||
};
|
||||
export const GeneratorPopup = () => {
|
||||
const [step, setStep] = useState(1);
|
||||
const modals = useModals();
|
||||
const [posts, setPosts] = useState<
|
||||
| {
|
||||
posts: Array<Array<{ post: string }>>;
|
||||
url: string;
|
||||
postId?: string;
|
||||
}
|
||||
| undefined
|
||||
>(undefined);
|
||||
|
||||
const [yearAndWeek, setYearAndWeek] = useState<{
|
||||
year: number;
|
||||
week: number;
|
||||
} | null>(null);
|
||||
|
||||
const closeAll = useCallback(() => {
|
||||
modals.closeAll();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-sixth p-[32px] w-full max-w-[920px] mx-auto flex flex-col gap-[24px] rounded-[4px] border border-customColor6 relative">
|
||||
<div className="bg-sixth p-[32px] w-full max-w-[920px] mx-auto flex flex-col rounded-[4px] border border-customColor6 relative">
|
||||
<button
|
||||
onClick={closeAll}
|
||||
className="outline-none absolute right-[20px] top-[15px] mantine-UnstyledButton-root mantine-ActionIcon-root hover:bg-tableBorder cursor-pointer mantine-Modal-close mantine-1dcetaa"
|
||||
|
|
@ -325,37 +256,7 @@ export const GeneratorPopup = () => {
|
|||
</svg>
|
||||
</button>
|
||||
<h1 className="text-[24px]">Generate Posts</h1>
|
||||
<div className="flex">
|
||||
<Step title="Generate posts" step={1} currentStep={step} lastStep={3} />
|
||||
<StepSpace />
|
||||
<Step title="Confirm posts" step={2} currentStep={step} lastStep={3} />
|
||||
<StepSpace />
|
||||
<Step title="Done" step={3} currentStep={step} lastStep={3} />
|
||||
</div>
|
||||
{step === 1 && (
|
||||
<FirstStep
|
||||
nextStep={(posts, url: string, postId?: string) => {
|
||||
setPosts({
|
||||
posts,
|
||||
url,
|
||||
postId,
|
||||
});
|
||||
setStep(2);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<SecondStep
|
||||
{...posts!}
|
||||
nextStep={(e) => {
|
||||
setYearAndWeek(e);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<ThirdStep week={yearAndWeek?.week!} year={yearAndWeek?.year!} />
|
||||
)}
|
||||
<FirstStep />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -363,6 +264,7 @@ export const GeneratorComponent = () => {
|
|||
const user = useUser();
|
||||
const router = useRouter();
|
||||
const modal = useModals();
|
||||
const all = useCalendar();
|
||||
|
||||
const generate = useCallback(async () => {
|
||||
if (!user?.tier?.ai) {
|
||||
|
|
@ -385,9 +287,13 @@ export const GeneratorComponent = () => {
|
|||
modal: 'bg-transparent text-textColor',
|
||||
},
|
||||
size: '100%',
|
||||
children: <GeneratorPopup />,
|
||||
children: (
|
||||
<CalendarWeekProvider {...all}>
|
||||
<GeneratorPopup />
|
||||
</CalendarWeekProvider>
|
||||
),
|
||||
});
|
||||
}, [user]);
|
||||
}, [user, all]);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import { useFireEvents } from '@gitroom/helpers/utils/use.fire.events';
|
|||
import { Calendar } from './calendar';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { DNDProvider } from '@gitroom/frontend/components/launches/helpers/dnd.provider';
|
||||
import { GeneratorComponent } from './generator/generator';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
interface MenuComponentInterface {
|
||||
refreshChannel: (
|
||||
|
|
@ -217,6 +219,8 @@ export const MenuComponent: FC<
|
|||
};
|
||||
export const LaunchesComponent = () => {
|
||||
const fetch = useFetch();
|
||||
const user = useUser();
|
||||
const {billingEnabled} = useVariables();
|
||||
const router = useRouter();
|
||||
const search = useSearchParams();
|
||||
const toast = useToaster();
|
||||
|
|
@ -333,11 +337,14 @@ export const LaunchesComponent = () => {
|
|||
}
|
||||
if (search.get('msg')) {
|
||||
toast.show(search.get('msg')!, 'warning');
|
||||
window?.opener?.postMessage({msg: search.get('msg')!, success: false}, '*');
|
||||
window?.opener?.postMessage(
|
||||
{ msg: search.get('msg')!, success: false },
|
||||
'*'
|
||||
);
|
||||
}
|
||||
if (search.get('added')) {
|
||||
fireEvents('channel_added');
|
||||
window?.opener?.postMessage({msg: 'Channel added', success: true}, '*');
|
||||
window?.opener?.postMessage({ msg: 'Channel added', success: true }, '*');
|
||||
}
|
||||
if (window.opener) {
|
||||
window.close();
|
||||
|
|
@ -375,7 +382,7 @@ export const LaunchesComponent = () => {
|
|||
))}
|
||||
</div>
|
||||
<AddProviderButton update={() => update(true)} />
|
||||
{/*{sortedIntegrations?.length > 0 && user?.tier?.ai && <GeneratorComponent />}*/}
|
||||
{sortedIntegrations?.length > 0 && user?.tier?.ai && billingEnabled && <GeneratorComponent />}
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col gap-[14px]">
|
||||
<Filters />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
export const agentCategories = [
|
||||
'Educational',
|
||||
'Inspirational',
|
||||
'Promotional',
|
||||
'Entertaining',
|
||||
'Interactive',
|
||||
'Behind The Scenes',
|
||||
'Testimonial',
|
||||
'Informative',
|
||||
'Humorous',
|
||||
'Seasonal',
|
||||
'News',
|
||||
'Challenge',
|
||||
'Contest',
|
||||
'Tips',
|
||||
'Tutorial',
|
||||
'Poll',
|
||||
'Survey',
|
||||
'Quote',
|
||||
'Event',
|
||||
'FAQ',
|
||||
'Story',
|
||||
'Meme',
|
||||
'Review',
|
||||
'Announcement',
|
||||
'Highlight',
|
||||
'Celebration',
|
||||
'Reminder',
|
||||
'Debate',
|
||||
'Update',
|
||||
'Trend',
|
||||
];
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { BaseMessage, HumanMessage } from '@langchain/core/messages';
|
||||
import { END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { ChatOpenAI } from '@langchain/openai';
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import { agentCategories } from '@gitroom/nestjs-libraries/agent/agent.categories';
|
||||
import { z } from 'zod';
|
||||
import { agentTopics } from '@gitroom/nestjs-libraries/agent/agent.topics';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
interface WorkflowChannelsState {
|
||||
messages: BaseMessage[];
|
||||
topic?: string;
|
||||
category: string;
|
||||
hook?: string;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
const category = z.object({
|
||||
category: z.string().describe('The category for the post'),
|
||||
});
|
||||
|
||||
const topic = z.object({
|
||||
topic: z.string().describe('The topic of the post'),
|
||||
});
|
||||
|
||||
const hook = z.object({
|
||||
hook: z.string().describe('The hook of the post'),
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class AgentGraphInsertService {
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
) {
|
||||
}
|
||||
static state = () =>
|
||||
new StateGraph<WorkflowChannelsState>({
|
||||
channels: {
|
||||
messages: {
|
||||
reducer: (currentState, updateValue) =>
|
||||
currentState.concat(updateValue),
|
||||
default: () => [],
|
||||
},
|
||||
topic: null,
|
||||
category: null,
|
||||
hook: null,
|
||||
content: null,
|
||||
},
|
||||
});
|
||||
|
||||
async findCategory(state: WorkflowChannelsState) {
|
||||
const { messages } = state;
|
||||
const structuredOutput = model.withStructuredOutput(category);
|
||||
return ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that get a social media post and categorize it into to one from the following categories:
|
||||
{categories}
|
||||
Here is the post:
|
||||
{post}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
post: messages[0].content,
|
||||
categories: agentCategories.join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
findTopic(state: WorkflowChannelsState) {
|
||||
const { messages } = state;
|
||||
const structuredOutput = model.withStructuredOutput(topic);
|
||||
return ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that get a social media post and categorize it into one of the following topics:
|
||||
{topics}
|
||||
Here is the post:
|
||||
{post}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
post: messages[0].content,
|
||||
topics: agentTopics.join(', '),
|
||||
});
|
||||
}
|
||||
|
||||
findHook(state: WorkflowChannelsState) {
|
||||
const { messages } = state;
|
||||
const structuredOutput = model.withStructuredOutput(hook);
|
||||
return ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that get a social media post and extract the hook, the hook is usually the first or second of both sentence of the post, but can be in a different place, make sure you don't change the wording of the post use the exact text:
|
||||
{post}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
post: messages[0].content,
|
||||
});
|
||||
}
|
||||
|
||||
async savePost(state: WorkflowChannelsState) {
|
||||
await this._postsService.createPopularPosts({
|
||||
category: state.category,
|
||||
topic: state.topic!,
|
||||
hook: state.hook!,
|
||||
content: state.messages[0].content! as string
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
newPost(post: string) {
|
||||
const state = AgentGraphInsertService.state();
|
||||
const workflow = state
|
||||
.addNode('find-category', this.findCategory)
|
||||
.addNode('find-topic', this.findTopic)
|
||||
.addNode('find-hook', this.findHook)
|
||||
.addNode('save-post', this.savePost.bind(this))
|
||||
.addEdge(START, 'find-category')
|
||||
.addEdge('find-category', 'find-topic')
|
||||
.addEdge('find-topic', 'find-hook')
|
||||
.addEdge('find-hook', 'save-post')
|
||||
.addEdge('save-post', END);
|
||||
|
||||
const app = workflow.compile();
|
||||
return app.invoke({
|
||||
messages: [new HumanMessage(post)],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BaseMessage,
|
||||
HumanMessage,
|
||||
ToolMessage,
|
||||
} from '@langchain/core/messages';
|
||||
import { END, START, StateGraph } from '@langchain/langgraph';
|
||||
import { ChatOpenAI, DallEAPIWrapper } from '@langchain/openai';
|
||||
import { TavilySearchResults } from '@langchain/community/tools/tavily_search';
|
||||
import { ToolNode } from '@langchain/langgraph/prebuilt';
|
||||
import { ChatPromptTemplate } from '@langchain/core/prompts';
|
||||
import dayjs from 'dayjs';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { z } from 'zod';
|
||||
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
|
||||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
|
||||
const tools = [new TavilySearchResults({ maxResults: 3 })];
|
||||
const toolNode = new ToolNode(tools);
|
||||
|
||||
const model = new ChatOpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
temperature: 0.7,
|
||||
});
|
||||
|
||||
const dalle = new DallEAPIWrapper({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
model: 'dall-e-3',
|
||||
});
|
||||
|
||||
interface WorkflowChannelsState {
|
||||
messages: BaseMessage[];
|
||||
orgId: string;
|
||||
question: string;
|
||||
hook?: string;
|
||||
fresearch?: string;
|
||||
category?: string;
|
||||
topic?: string;
|
||||
date?: string;
|
||||
format: 'one_short' | 'one_long' | 'thread_short' | 'thread_long';
|
||||
tone: 'personal' | 'company';
|
||||
content?: {
|
||||
content: string;
|
||||
website?: string;
|
||||
prompt?: string;
|
||||
image?: string;
|
||||
}[];
|
||||
isPicture?: boolean;
|
||||
popularPosts?: { content: string; hook: string }[];
|
||||
}
|
||||
|
||||
const category = z.object({
|
||||
category: z.string().describe('The category for the post'),
|
||||
});
|
||||
|
||||
const topic = z.object({
|
||||
topic: z.string().describe('The topic for the post'),
|
||||
});
|
||||
|
||||
const hook = z.object({
|
||||
hook: z
|
||||
.string()
|
||||
.describe(
|
||||
'Hook for the new post, don\'t take it from "the request of the user"'
|
||||
),
|
||||
});
|
||||
|
||||
const contentZod = (
|
||||
isPicture: boolean,
|
||||
format: 'one_short' | 'one_long' | 'thread_short' | 'thread_long'
|
||||
) => {
|
||||
const content = z.object({
|
||||
content: z.string().describe('Content for the new post'),
|
||||
website: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Website for the new post if exists, If one of the post present a brand, website link must be to the root domain of the brand or don't include it, website url should contain the brand name"
|
||||
),
|
||||
...(isPicture
|
||||
? {
|
||||
prompt: z
|
||||
.string()
|
||||
.describe(
|
||||
"Prompt to generate a picture for this post later, make sure it doesn't contain brand names and make it very descriptive in terms of style"
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
return z.object({
|
||||
content:
|
||||
format === 'one_short' || format === 'one_long'
|
||||
? content
|
||||
: z.array(content).min(2).describe(`Content for the new post`),
|
||||
});
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AgentGraphService {
|
||||
private storage = UploadFactory.createStorage();
|
||||
constructor(
|
||||
private _postsService: PostsService,
|
||||
private _mediaService: MediaService
|
||||
) {}
|
||||
static state = () =>
|
||||
new StateGraph<WorkflowChannelsState>({
|
||||
channels: {
|
||||
messages: {
|
||||
reducer: (currentState, updateValue) =>
|
||||
currentState.concat(updateValue),
|
||||
default: () => [],
|
||||
},
|
||||
fresearch: null,
|
||||
format: null,
|
||||
tone: null,
|
||||
question: null,
|
||||
orgId: null,
|
||||
hook: null,
|
||||
content: null,
|
||||
date: null,
|
||||
category: null,
|
||||
popularPosts: null,
|
||||
topic: null,
|
||||
isPicture: null,
|
||||
},
|
||||
});
|
||||
|
||||
async startCall(state: WorkflowChannelsState) {
|
||||
const runTools = model.bindTools(tools);
|
||||
const response = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
Today is ${dayjs().format()}, You are an assistant that gets a social media post or requests for a social media post.
|
||||
You research should be on the most possible recent data.
|
||||
You concat the text of the request together with an internet research based on the text.
|
||||
{text}
|
||||
`
|
||||
)
|
||||
.pipe(runTools)
|
||||
.invoke({
|
||||
text: state.messages[state.messages.length - 1].content,
|
||||
});
|
||||
|
||||
return { messages: [response] };
|
||||
}
|
||||
|
||||
async saveResearch(state: WorkflowChannelsState) {
|
||||
const content = state.messages.filter((f) => f instanceof ToolMessage);
|
||||
return { fresearch: content };
|
||||
}
|
||||
|
||||
async findCategories(state: WorkflowChannelsState) {
|
||||
const allCategories = await this._postsService.findAllExistingCategories();
|
||||
const structuredOutput = model.withStructuredOutput(category);
|
||||
const { category: outputCategory } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets a text that will be later summarized into a social media post
|
||||
and classify it to one of the following categories: {categories}
|
||||
text: {text}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
categories: allCategories.map((p) => p.category).join(', '),
|
||||
text: state.fresearch,
|
||||
});
|
||||
|
||||
return {
|
||||
category: outputCategory,
|
||||
};
|
||||
}
|
||||
|
||||
async findTopic(state: WorkflowChannelsState) {
|
||||
const allTopics = await this._postsService.findAllExistingTopicsOfCategory(
|
||||
state?.category!
|
||||
);
|
||||
if (allTopics.length === 0) {
|
||||
return { topic: null };
|
||||
}
|
||||
|
||||
const structuredOutput = model.withStructuredOutput(topic);
|
||||
const { topic: outputTopic } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets a text that will be later summarized into a social media post
|
||||
and classify it to one of the following topics: {topics}
|
||||
text: {text}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
topics: allTopics.map((p) => p.topic).join(', '),
|
||||
text: state.fresearch,
|
||||
});
|
||||
|
||||
return {
|
||||
topic: outputTopic,
|
||||
};
|
||||
}
|
||||
|
||||
async findPopularPosts(state: WorkflowChannelsState) {
|
||||
const popularPosts = await this._postsService.findPopularPosts(
|
||||
state.category!,
|
||||
state.topic
|
||||
);
|
||||
return { popularPosts };
|
||||
}
|
||||
|
||||
async generateHook(state: WorkflowChannelsState) {
|
||||
const structuredOutput = model.withStructuredOutput(hook);
|
||||
const { hook: outputHook } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets content for a social media post, and generate only the hook.
|
||||
The hook is the 1-2 sentences of the post that will be used to grab the attention of the reader.
|
||||
You will be provided existing hooks you should use as inspiration.
|
||||
- Avoid weird hook that starts with "Discover the secret...", "The best...", "The most...", "The top..."
|
||||
- Make sure it sounds ${state.tone}
|
||||
- Use ${state.tone === 'personal' ? '1st' : '3rd'} person mode
|
||||
- Make sure it's engaging
|
||||
- Don't be cringy
|
||||
- Use simple english
|
||||
- Make sure you add "\n" between the lines
|
||||
- Don't take the hook from "request of the user"
|
||||
|
||||
<!-- BEGIN request of the user -->
|
||||
{request}
|
||||
<!-- END request of the user -->
|
||||
|
||||
<!-- BEGIN existing hooks -->
|
||||
{hooks}
|
||||
<!-- END existing hooks -->
|
||||
|
||||
<!-- BEGIN current content -->
|
||||
{text}
|
||||
<!-- END current content -->
|
||||
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
request: state.messages[0].content,
|
||||
hooks: state.popularPosts!.map((p) => p.hook).join('\n'),
|
||||
text: state.fresearch,
|
||||
});
|
||||
|
||||
return {
|
||||
hook: outputHook,
|
||||
};
|
||||
}
|
||||
|
||||
async generateContent(state: WorkflowChannelsState) {
|
||||
const structuredOutput = model.withStructuredOutput(
|
||||
contentZod(!!state.isPicture, state.format)
|
||||
);
|
||||
const { content: outputContent } = await ChatPromptTemplate.fromTemplate(
|
||||
`
|
||||
You are an assistant that gets existing hook of a social media, content and generate only the content.
|
||||
- Don't add any hashtags
|
||||
- Make sure it sounds ${state.tone}
|
||||
- Use ${state.tone === 'personal' ? '1st' : '3rd'} person mode
|
||||
- ${
|
||||
state.format === 'one_short' || state.format === 'thread_short'
|
||||
? 'Post should be maximum 200 chars to fit twitter'
|
||||
: 'Post should be long'
|
||||
}
|
||||
- ${
|
||||
state.format === 'one_short' || state.format === 'one_long'
|
||||
? 'Post should have only 1 item'
|
||||
: 'Post should have minimum 2 items'
|
||||
}
|
||||
- Use the hook as inspiration
|
||||
- Make sure it's engaging
|
||||
- Don't be cringy
|
||||
- Use simple english
|
||||
- The Content should not contain the hook
|
||||
- Try to put some call to action at the end of the post
|
||||
- Make sure you add "\n" between the lines
|
||||
- Add "\n" after every "."
|
||||
|
||||
Hook:
|
||||
{hook}
|
||||
|
||||
User request:
|
||||
{request}
|
||||
|
||||
current content information:
|
||||
{information}
|
||||
`
|
||||
)
|
||||
.pipe(structuredOutput)
|
||||
.invoke({
|
||||
hook: state.hook,
|
||||
request: state.messages[0].content,
|
||||
information: state.fresearch,
|
||||
});
|
||||
|
||||
return {
|
||||
content: outputContent,
|
||||
};
|
||||
}
|
||||
|
||||
async fixArray(state: WorkflowChannelsState) {
|
||||
if (state.format === 'one_short' || state.format === 'one_long') {
|
||||
return {
|
||||
content: [state.content],
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
async generatePictures(state: WorkflowChannelsState) {
|
||||
if (!state.isPicture) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const newContent = await Promise.all(
|
||||
(state.content || []).map(async (p) => {
|
||||
const image = await dalle.invoke(p.prompt!);
|
||||
return {
|
||||
...p,
|
||||
image,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
content: newContent,
|
||||
};
|
||||
}
|
||||
|
||||
async uploadPictures(state: WorkflowChannelsState) {
|
||||
const all = await Promise.all(
|
||||
(state.content || []).map(async (p) => {
|
||||
if (p.image) {
|
||||
const upload = await this.storage.uploadSimple(p.image);
|
||||
const name = upload.split('/').pop()!;
|
||||
const uploadWithId = await this._mediaService.saveFile(
|
||||
state.orgId,
|
||||
name,
|
||||
upload
|
||||
);
|
||||
|
||||
return {
|
||||
...p,
|
||||
image: uploadWithId,
|
||||
};
|
||||
}
|
||||
|
||||
return p;
|
||||
})
|
||||
);
|
||||
|
||||
return { content: all };
|
||||
}
|
||||
|
||||
async isGeneratePicture(state: WorkflowChannelsState) {
|
||||
if (state.isPicture) {
|
||||
return 'generate-picture';
|
||||
}
|
||||
|
||||
return 'post-time';
|
||||
}
|
||||
|
||||
async postDateTime(state: WorkflowChannelsState) {
|
||||
return { date: await this._postsService.findFreeDateTime(state.orgId) };
|
||||
}
|
||||
|
||||
start(orgId: string, body: GeneratorDto) {
|
||||
const state = AgentGraphService.state();
|
||||
const workflow = state
|
||||
.addNode('agent', this.startCall.bind(this))
|
||||
.addNode('research', toolNode)
|
||||
.addNode('save-research', this.saveResearch.bind(this))
|
||||
.addNode('find-category', this.findCategories.bind(this))
|
||||
.addNode('find-topic', this.findTopic.bind(this))
|
||||
.addNode('find-popular-posts', this.findPopularPosts.bind(this))
|
||||
.addNode('generate-hook', this.generateHook.bind(this))
|
||||
.addNode('generate-content', this.generateContent.bind(this))
|
||||
.addNode('generate-content-fix', this.fixArray.bind(this))
|
||||
.addNode('generate-picture', this.generatePictures.bind(this))
|
||||
.addNode('upload-pictures', this.uploadPictures.bind(this))
|
||||
.addNode('post-time', this.postDateTime.bind(this))
|
||||
.addEdge(START, 'agent')
|
||||
.addEdge('agent', 'research')
|
||||
.addEdge('research', 'save-research')
|
||||
.addEdge('save-research', 'find-category')
|
||||
.addEdge('find-category', 'find-topic')
|
||||
.addEdge('find-topic', 'find-popular-posts')
|
||||
.addEdge('find-popular-posts', 'generate-hook')
|
||||
.addEdge('generate-hook', 'generate-content')
|
||||
.addEdge('generate-content', 'generate-content-fix')
|
||||
.addConditionalEdges(
|
||||
'generate-content-fix',
|
||||
this.isGeneratePicture.bind(this)
|
||||
)
|
||||
.addEdge('generate-picture', 'upload-pictures')
|
||||
.addEdge('upload-pictures', 'post-time')
|
||||
.addEdge('post-time', END);
|
||||
|
||||
const app = workflow.compile();
|
||||
|
||||
return app.streamEvents(
|
||||
{
|
||||
messages: [new HumanMessage(body.research)],
|
||||
isPicture: body.isPicture,
|
||||
format: body.format,
|
||||
tone: body.tone,
|
||||
orgId,
|
||||
},
|
||||
{
|
||||
streamMode: 'values',
|
||||
version: 'v2',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Global, Module } from '@nestjs/common';
|
||||
import { AgentGraphService } from '@gitroom/nestjs-libraries/agent/agent.graph.service';
|
||||
import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [AgentGraphService, AgentGraphInsertService],
|
||||
get exports() {
|
||||
return this.providers;
|
||||
},
|
||||
})
|
||||
export class AgentModule {}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
export const agentTopics = [
|
||||
'Business',
|
||||
'Marketing',
|
||||
'Finance',
|
||||
'Startups',
|
||||
'Networking',
|
||||
'Leadership',
|
||||
'Strategy',
|
||||
'Branding',
|
||||
'Analytics',
|
||||
'Growth',
|
||||
'Drawing',
|
||||
'Painting',
|
||||
'Design',
|
||||
'Photography',
|
||||
'Writing',
|
||||
'Sculpting',
|
||||
'Animation',
|
||||
'Sketching',
|
||||
'Crafting',
|
||||
'Calligraphy',
|
||||
'Mindset',
|
||||
'Productivity',
|
||||
'Motivation',
|
||||
'Education',
|
||||
'Learning',
|
||||
'Skills',
|
||||
'Success',
|
||||
'Wellness',
|
||||
'Goals',
|
||||
'Inspiration',
|
||||
'Fashion',
|
||||
'Travel',
|
||||
'Food',
|
||||
'Fitness',
|
||||
'Health',
|
||||
'Beauty',
|
||||
'Home',
|
||||
'Decor',
|
||||
'Hobbies',
|
||||
'Parenting',
|
||||
'Tech',
|
||||
'Gadgets',
|
||||
'AI',
|
||||
'Coding',
|
||||
'Software',
|
||||
'Innovation',
|
||||
'Apps',
|
||||
'Gaming',
|
||||
'Robotics',
|
||||
'Security',
|
||||
'Music',
|
||||
'Movies',
|
||||
'Sports',
|
||||
'Books',
|
||||
'Theater',
|
||||
'Comedy',
|
||||
'Dance',
|
||||
'Celebrities',
|
||||
'Culture',
|
||||
'Gaming',
|
||||
'Environment',
|
||||
'Equality',
|
||||
'Activism',
|
||||
'Justice',
|
||||
'Diversity',
|
||||
'Sustainability',
|
||||
'Inclusion',
|
||||
'Awareness',
|
||||
'Charity',
|
||||
'Peace',
|
||||
'Holidays',
|
||||
'Festivities',
|
||||
'Seasons',
|
||||
'Trends',
|
||||
'Celebrations',
|
||||
'Anniversaries',
|
||||
'Milestones',
|
||||
'Memories',
|
||||
'Promotions',
|
||||
'Updates',
|
||||
];
|
||||
|
|
@ -39,8 +39,8 @@ export class IntegrationRepository {
|
|||
id: plugId,
|
||||
},
|
||||
include: {
|
||||
integration: true
|
||||
}
|
||||
integration: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -486,4 +486,17 @@ export class IntegrationRepository {
|
|||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async getPostingTimes(orgId: string) {
|
||||
return this._integration.model.integration.findMany({
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
disabled: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
postingTimes: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import { IntegrationTimeDto } from '@gitroom/nestjs-libraries/dtos/integrations/
|
|||
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
|
||||
import { PlugDto } from '@gitroom/nestjs-libraries/dtos/plugs/plug.dto';
|
||||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { difference } from 'lodash';
|
||||
import { difference, uniq } from 'lodash';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
dayjs.extend(utc);
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationService {
|
||||
|
|
@ -515,4 +517,16 @@ export class IntegrationService {
|
|||
const loadOnlyIds = exisingData.map((p) => p.value);
|
||||
return difference(id, loadOnlyIds);
|
||||
}
|
||||
|
||||
async findFreeDateTime(orgId: string): Promise<number[]> {
|
||||
const findTimes = await this._integrationRepository.getPostingTimes(orgId);
|
||||
return uniq(
|
||||
findTimes.reduce((all: any, current: any) => {
|
||||
return [
|
||||
...all,
|
||||
...JSON.parse(current.postingTimes).map((p: { time: number }) => p.time),
|
||||
];
|
||||
}, [] as number[])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ dayjs.extend(weekOfYear);
|
|||
|
||||
@Injectable()
|
||||
export class PostsRepository {
|
||||
constructor(private _post: PrismaRepository<'post'>) {}
|
||||
constructor(
|
||||
private _post: PrismaRepository<'post'>,
|
||||
private _popularPosts: PrismaRepository<'popularPosts'>
|
||||
) {}
|
||||
|
||||
getOldPosts(orgId: string, date: string) {
|
||||
return this._post.model.post.findMany({
|
||||
|
|
@ -387,4 +390,84 @@ export class PostsRepository {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
findAllExistingCategories() {
|
||||
return this._popularPosts.model.popularPosts.findMany({
|
||||
select: {
|
||||
category: true,
|
||||
},
|
||||
distinct: ['category'],
|
||||
});
|
||||
}
|
||||
|
||||
findAllExistingTopicsOfCategory(category: string) {
|
||||
return this._popularPosts.model.popularPosts.findMany({
|
||||
where: {
|
||||
category,
|
||||
},
|
||||
select: {
|
||||
topic: true,
|
||||
},
|
||||
distinct: ['topic'],
|
||||
});
|
||||
}
|
||||
|
||||
findPopularPosts(category: string, topic?: string) {
|
||||
return this._popularPosts.model.popularPosts.findMany({
|
||||
where: {
|
||||
category,
|
||||
...(topic ? { topic } : {}),
|
||||
},
|
||||
select: {
|
||||
content: true,
|
||||
hook: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
createPopularPosts(post: {
|
||||
category: string;
|
||||
topic: string;
|
||||
content: string;
|
||||
hook: string;
|
||||
}) {
|
||||
return this._popularPosts.model.popularPosts.create({
|
||||
data: {
|
||||
category: 'category',
|
||||
topic: 'topic',
|
||||
content: 'content',
|
||||
hook: 'hook',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getPostsCountsByDates(
|
||||
orgId: string,
|
||||
times: number[],
|
||||
date: dayjs.Dayjs
|
||||
) {
|
||||
const dates = await this._post.model.post.findMany({
|
||||
where: {
|
||||
deletedAt: null,
|
||||
organizationId: orgId,
|
||||
publishDate: {
|
||||
in: times.map((time) => {
|
||||
return date.clone().add(time, 'minutes').toDate();
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return times.filter(
|
||||
(time) =>
|
||||
date.clone().add(time, 'minutes').isAfter(dayjs.utc()) &&
|
||||
!dates.find((dateFind) => {
|
||||
return (
|
||||
dayjs
|
||||
.utc(dateFind.publishDate)
|
||||
.diff(date.clone().startOf('day'), 'minutes') == time
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,9 @@ import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integ
|
|||
import { Integration, Post, Media, From } from '@prisma/client';
|
||||
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';
|
||||
import { NotificationService } from '@gitroom/nestjs-libraries/database/prisma/notifications/notification.service';
|
||||
import { capitalize, shuffle } from 'lodash';
|
||||
import { capitalize, shuffle, uniq } from 'lodash';
|
||||
import { MessagesService } from '@gitroom/nestjs-libraries/database/prisma/marketplace/messages.service';
|
||||
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
|
||||
import { GeneratorDto } from '@gitroom/nestjs-libraries/dtos/generator/generator.dto';
|
||||
import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service';
|
||||
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
|
||||
import { CreateGeneratedPostsDto } from '@gitroom/nestjs-libraries/dtos/generator/create.generated.posts.dto';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
|
|
@ -22,6 +19,8 @@ import {
|
|||
import { BullMqClient } from '@gitroom/nestjs-libraries/bull-mq-transport-new/client';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { AuthTokenDetails } from '@gitroom/nestjs-libraries/integrations/social/social.integrations.interface';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
dayjs.extend(utc);
|
||||
|
||||
type PostWithConditionals = Post & {
|
||||
integration?: Integration;
|
||||
|
|
@ -37,8 +36,6 @@ export class PostsService {
|
|||
private _notificationService: NotificationService,
|
||||
private _messagesService: MessagesService,
|
||||
private _stripeService: StripeService,
|
||||
private _extractContentService: ExtractContentService,
|
||||
private _openAiService: OpenaiService,
|
||||
private _integrationService: IntegrationService
|
||||
) {}
|
||||
|
||||
|
|
@ -606,26 +603,6 @@ export class PostsService {
|
|||
}
|
||||
}
|
||||
|
||||
async loadPostContent(postId: string) {
|
||||
const post = await this._postRepository.getPostById(postId);
|
||||
if (!post) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return post.content;
|
||||
}
|
||||
|
||||
async generatePosts(orgId: string, body: GeneratorDto) {
|
||||
const content = body.url
|
||||
? await this._extractContentService.extractContent(body.url)
|
||||
: await this.loadPostContent(body.post);
|
||||
|
||||
const value = body.url
|
||||
? await this._openAiService.extractWebsiteText(content!)
|
||||
: await this._openAiService.generatePosts(content!);
|
||||
return { list: value };
|
||||
}
|
||||
|
||||
async generatePostsDraft(orgId: string, body: CreateGeneratedPostsDto) {
|
||||
const getAllIntegrations = (
|
||||
await this._integrationService.getIntegrationsList(orgId)
|
||||
|
|
@ -702,4 +679,59 @@ export class PostsService {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
findAllExistingCategories() {
|
||||
return this._postRepository.findAllExistingCategories();
|
||||
}
|
||||
|
||||
findAllExistingTopicsOfCategory(category: string) {
|
||||
return this._postRepository.findAllExistingTopicsOfCategory(category);
|
||||
}
|
||||
|
||||
findPopularPosts(category: string, topic?: string) {
|
||||
return this._postRepository.findPopularPosts(category, topic);
|
||||
}
|
||||
|
||||
async findFreeDateTime(orgId: string) {
|
||||
const findTimes = await this._integrationService.findFreeDateTime(orgId);
|
||||
return this.findFreeDateTimeRecursive(
|
||||
orgId,
|
||||
findTimes,
|
||||
dayjs.utc().startOf('day')
|
||||
);
|
||||
}
|
||||
|
||||
async createPopularPosts(post: {
|
||||
category: string;
|
||||
topic: string;
|
||||
content: string;
|
||||
hook: string;
|
||||
}) {
|
||||
return this._postRepository.createPopularPosts(post);
|
||||
}
|
||||
|
||||
private async findFreeDateTimeRecursive(
|
||||
orgId: string,
|
||||
times: number[],
|
||||
date: dayjs.Dayjs
|
||||
): Promise<string> {
|
||||
const list = await this._postRepository.getPostsCountsByDates(
|
||||
orgId,
|
||||
times,
|
||||
date
|
||||
);
|
||||
|
||||
if (!list.length) {
|
||||
return this.findFreeDateTimeRecursive(orgId, times, date.add(1, 'day'));
|
||||
}
|
||||
|
||||
const num = list.reduce<null | number>((prev, curr) => {
|
||||
if (prev === null || prev > curr) {
|
||||
return curr;
|
||||
}
|
||||
return prev;
|
||||
}, null) as number;
|
||||
|
||||
return date.clone().add(num, 'minutes').format('YYYY-MM-DDTHH:mm:00');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -486,6 +486,16 @@ model ExisingPlugData {
|
|||
@@unique([integrationId, methodName, value])
|
||||
}
|
||||
|
||||
model PopularPosts {
|
||||
id String @id @default(uuid())
|
||||
category String
|
||||
topic String
|
||||
content String
|
||||
hook String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum OrderStatus {
|
||||
PENDING
|
||||
ACCEPTED
|
||||
|
|
|
|||
|
|
@ -1,24 +1,20 @@
|
|||
import {
|
||||
IsDefined,
|
||||
IsInt,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateIf,
|
||||
ValidateNested,
|
||||
IsBoolean, IsIn, IsString, MinLength
|
||||
} from 'class-validator';
|
||||
|
||||
export class GeneratorDto {
|
||||
@IsString()
|
||||
@ValidateIf((o) => !o.post)
|
||||
@IsUrl(
|
||||
{},
|
||||
{
|
||||
message: 'Invalid URL',
|
||||
}
|
||||
)
|
||||
url: string;
|
||||
@MinLength(10)
|
||||
research: string;
|
||||
|
||||
@IsBoolean()
|
||||
isPicture: boolean;
|
||||
|
||||
@ValidateIf((o) => !o.url)
|
||||
@IsString()
|
||||
post: string;
|
||||
@IsIn(['one_short', 'one_long', 'thread_short', 'thread_long'])
|
||||
format: 'one_short' | 'one_long' | 'thread_short' | 'thread_long';
|
||||
|
||||
@IsString()
|
||||
@IsIn(['personal', 'company'])
|
||||
tone: 'personal' | 'company';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const Textarea: FC<DetailedHTMLProps<InputHTMLAttributes<HTMLTextAreaElem
|
|||
}, [form?.formState?.errors?.[props?.name!]?.message, error]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-[6px]">
|
||||
<div className={clsx("flex flex-col gap-[6px]", props.disabled && 'opacity-50')}>
|
||||
<div className={`${interClass} text-[14px]`}>{label}</div>
|
||||
<textarea {...disableForm ? {} : form.register(props.name)} className={clsx("bg-input min-h-[150px] p-[16px] outline-none border-fifth border rounded-[4px] text-inputText placeholder-inputText", className)} {...rest} />
|
||||
<div className="text-red-400 text-[12px]">{err || <> </>}</div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
|
@ -17,6 +17,7 @@
|
|||
"dev:cron": "npx nx run cron:serve:development",
|
||||
"dev:docker": "docker compose -f ./docker-compose.dev.yaml up -d",
|
||||
"start:prod": "node dist/apps/backend/main.js",
|
||||
"commands:build:development": "npx nx run commands:build:development",
|
||||
"start:prod:frontend": "nx run frontend:serve:production",
|
||||
"start:prod:workers": "node dist/apps/workers/main.js",
|
||||
"start:prod:cron": "node dist/apps/cron/main.js",
|
||||
|
|
@ -41,6 +42,10 @@
|
|||
"@copilotkit/react-ui": "^1.4.7",
|
||||
"@copilotkit/runtime": "^1.4.7",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@langchain/community": "^0.3.19",
|
||||
"@langchain/core": "^0.3.26",
|
||||
"@langchain/langgraph": "^0.2.34",
|
||||
"@langchain/openai": "^0.3.16",
|
||||
"@mantine/core": "^5.10.5",
|
||||
"@mantine/dates": "^5.10.5",
|
||||
"@mantine/hooks": "^5.10.5",
|
||||
|
|
@ -91,6 +96,8 @@
|
|||
"@uppy/react": "^4.0.2",
|
||||
"@uppy/status-bar": "^4.0.3",
|
||||
"@uppy/xhr-upload": "^4.1.0",
|
||||
"ai": "^4.0.22",
|
||||
"algoliasearch": "^5.18.0",
|
||||
"array-move": "^4.0.0",
|
||||
"axios": "^1.7.7",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
|
@ -157,7 +164,8 @@
|
|||
"utf-8-validate": "^5.0.10",
|
||||
"uuid": "^10.0.0",
|
||||
"yargs": "^17.7.2",
|
||||
"yup": "^1.4.0"
|
||||
"yup": "^1.4.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/schematics": "^10.0.1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue