Feat: agent
This commit is contained in:
parent
73ba9acf8c
commit
41e7d0fdeb
|
|
@ -47,7 +47,7 @@ export const HashnodePublications: FC<{
|
|||
value={currentMedia}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{publications.map((publication: any) => (
|
||||
{(publications || []).map((publication: any) => (
|
||||
<option key={publication.id} value={publication.id}>
|
||||
{publication.name}
|
||||
</option>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export const HashnodeTags: FC<{
|
|||
);
|
||||
useEffect(() => {
|
||||
customFunc.get('tags').then((data) => setTags(data));
|
||||
const settings = getValues()[props.name];
|
||||
const settings = getValues()[props.name] || [];
|
||||
if (settings) {
|
||||
setTagValue(settings);
|
||||
}
|
||||
|
|
@ -63,12 +63,13 @@ export const HashnodeTags: FC<{
|
|||
if (!tags.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`text-[14px] mb-[6px]`}>{label}</div>
|
||||
<ReactTags
|
||||
suggestions={tags}
|
||||
selected={tagValue}
|
||||
suggestions={tags || []}
|
||||
selected={tagValue || []}
|
||||
onAdd={onAddition}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const SettingsComponent = () => {
|
|||
<Input label="Subject" {...form.register('subject')} />
|
||||
<Input label="Preview" {...form.register('preview')} />
|
||||
<SelectList {...form.register('list')} />
|
||||
<SelectTemplates {...form.register('templates')} />
|
||||
<SelectTemplates {...form.register('template')} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -29,19 +29,6 @@ export default withProvider({
|
|||
SettingsComponent: SettingsComponent,
|
||||
CustomPreviewComponent: undefined,
|
||||
dto: ListmonkDto,
|
||||
checkValidity: async (posts) => {
|
||||
if (
|
||||
posts.some(
|
||||
(p) => p.some((a) => a.path.indexOf('mp4') > -1) && p.length > 1
|
||||
)
|
||||
) {
|
||||
return 'You can only upload one video per post.';
|
||||
}
|
||||
|
||||
if (posts.some((p) => p.length > 4)) {
|
||||
return 'There can be maximum 4 pictures in a post.';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
checkValidity: undefined,
|
||||
maximumCharacters: 300000,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export class LoadToolsService {
|
|||
- For X, if you don't have Premium, don't suggest a long post because it won't work.
|
||||
- Platform format will also be passed can be "normal", "markdown", "html", make sure you use the correct format for each platform.
|
||||
|
||||
- Sometimes 'integrationSchema' will return rules, make sure you follow them (these rules are set in stone, even if the user asks to ignore them)
|
||||
- Each socials media platform has different settings and rules, you can get them by using the integrationSchema tool.
|
||||
- Always make sure you use this tool before you schedule any post.
|
||||
- In every message I will send you the list of needed social medias (id and platform), if you already have the information use it, if not, use the integrationSchema tool to get it.
|
||||
|
|
@ -65,6 +66,7 @@ export class LoadToolsService {
|
|||
- Before scheduling a post, always make sure you ask the user confirmation by providing all the details of the post (text, images, videos, date, time, social media platform, account).
|
||||
- If the user confirm, ask if they would like to get a modal with populated content without scheduling the post yet or if they want to schedule it right away.
|
||||
- Between tools, we will reference things like: [output:name] and [input:name] to set the information right.
|
||||
- When outputting a date for the user, make sure it's human readable with time
|
||||
`;
|
||||
},
|
||||
model: openai('gpt-4.1'),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import 'reflect-metadata';
|
||||
|
||||
export function Rules(description: string) {
|
||||
return function (target: any) {
|
||||
// Define metadata on the class prototype (so it can be retrieved from the class)
|
||||
Reflect.defineMetadata(
|
||||
'custom:rules:description',
|
||||
description,
|
||||
target
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
@ -2,17 +2,13 @@ import { AgentToolInterface } from '@gitroom/nestjs-libraries/chat/agent.tool.in
|
|||
import { createTool } from '@mastra/core/tools';
|
||||
import { z } from 'zod';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
IntegrationManager,
|
||||
socialIntegrationList,
|
||||
} from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { validationMetadatasToSchemas } from 'class-validator-jsonschema';
|
||||
import { socialIntegrationList } from '@gitroom/nestjs-libraries/integrations/integration.manager';
|
||||
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
|
||||
import { RefreshToken } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
|
||||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { AllProvidersSettings } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/all.providers.settings';
|
||||
import { validate } from 'class-validator';
|
||||
import { Integration } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class IntegrationSchedulePostTool implements AgentToolInterface {
|
||||
|
|
@ -36,69 +32,115 @@ If the user want to post a post to LinkedIn with one comment
|
|||
If the user want to post 20 posts for facebook each in individual days without comments
|
||||
- socialPost array length will be 20
|
||||
- postsAndComments array length will be one
|
||||
|
||||
If the tools return errors, you would need to rerun it with the right parameters, don't ask again, just run it
|
||||
`,
|
||||
requireApproval: true,
|
||||
inputSchema: z.object({
|
||||
socialPost: z.array(
|
||||
z.object({
|
||||
integrationId: z
|
||||
.string()
|
||||
.describe('The id of the integration (not internal id)'),
|
||||
date: z.string().describe('The date of the post in UTC time'),
|
||||
shortLink: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'If the post has a link inside, we can ask the user if they want to add a short link'
|
||||
),
|
||||
type: z
|
||||
.enum(['draft', 'schedule', 'now'])
|
||||
.describe(
|
||||
'The type of the post, if we pass now, we should pass the current date also'
|
||||
),
|
||||
postsAndComments: z
|
||||
.array(
|
||||
z.object({
|
||||
content: z.string().describe('The content of the post'),
|
||||
image: z
|
||||
.array(z.string())
|
||||
.describe('The image of the post (URLS)'),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'first item is the post, every other item is the comments'
|
||||
),
|
||||
settings: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string().describe('Name of the settings key to pass'),
|
||||
value: z.string().describe('Value of the key'),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'This relies on the integrationSchema tool to get the settings [input:settings]'
|
||||
),
|
||||
})
|
||||
).describe('Individual post')
|
||||
socialPost: z
|
||||
.array(
|
||||
z.object({
|
||||
integrationId: z
|
||||
.string()
|
||||
.describe('The id of the integration (not internal id)'),
|
||||
date: z.string().describe('The date of the post in UTC time'),
|
||||
shortLink: z
|
||||
.boolean()
|
||||
.describe(
|
||||
'If the post has a link inside, we can ask the user if they want to add a short link'
|
||||
),
|
||||
type: z
|
||||
.enum(['draft', 'schedule', 'now'])
|
||||
.describe(
|
||||
'The type of the post, if we pass now, we should pass the current date also'
|
||||
),
|
||||
postsAndComments: z
|
||||
.array(
|
||||
z.object({
|
||||
content: z.string().describe('The content of the post'),
|
||||
attachments: z
|
||||
.array(z.string())
|
||||
.describe('The image of the post (URLS)'),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'first item is the post, every other item is the comments'
|
||||
),
|
||||
settings: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z
|
||||
.string()
|
||||
.describe('Name of the settings key to pass'),
|
||||
value: z
|
||||
.any()
|
||||
.describe(
|
||||
'Value of the key, always prefer the id then label if possible'
|
||||
),
|
||||
})
|
||||
)
|
||||
.describe(
|
||||
'This relies on the integrationSchema tool to get the settings [input:settings]'
|
||||
),
|
||||
})
|
||||
)
|
||||
.describe('Individual post'),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
output: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
postId: z.string(),
|
||||
releaseURL: z.string(),
|
||||
status: z.string(),
|
||||
})
|
||||
),
|
||||
output: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
postId: z.string(),
|
||||
releaseURL: z.string(),
|
||||
status: z.string(),
|
||||
})
|
||||
)
|
||||
.or(z.object({ errors: z.string() })),
|
||||
}),
|
||||
execute: async ({ runtimeContext, context }) => {
|
||||
console.log(JSON.stringify(context, null, 2));
|
||||
// @ts-ignore
|
||||
const organizationId = runtimeContext.get('organization') as string;
|
||||
const finalOutput = [];
|
||||
|
||||
const integrations = {} as Record<string, Integration>;
|
||||
for (const platform of context.socialPost) {
|
||||
integrations[platform.integrationId] =
|
||||
await this._integrationService.getIntegrationById(
|
||||
organizationId,
|
||||
platform.integrationId
|
||||
);
|
||||
|
||||
const { dto } = socialIntegrationList.find(
|
||||
(p) =>
|
||||
p.identifier ===
|
||||
integrations[platform.integrationId].providerIdentifier
|
||||
)!;
|
||||
|
||||
if (dto) {
|
||||
const newDTO = new dto();
|
||||
const obj = Object.assign(
|
||||
newDTO,
|
||||
platform.settings.reduce(
|
||||
(acc, s) => ({
|
||||
...acc,
|
||||
[s.key]: s.value,
|
||||
}),
|
||||
{} as AllProvidersSettings
|
||||
)
|
||||
);
|
||||
const errors = await validate(obj);
|
||||
if (errors.length) {
|
||||
console.log(errors);
|
||||
return {
|
||||
errors: JSON.stringify(errors),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const post of context.socialPost) {
|
||||
const integration = await this._integrationService.getIntegrationById(
|
||||
organizationId,
|
||||
post.integrationId
|
||||
);
|
||||
const integration = integrations[post.integrationId];
|
||||
|
||||
if (!integration) {
|
||||
throw new Error('Integration not found');
|
||||
|
|
@ -123,7 +165,7 @@ If the user want to post 20 posts for facebook each in individual days without c
|
|||
value: post.postsAndComments.map((p) => ({
|
||||
content: p.content,
|
||||
id: makeId(10),
|
||||
image: p.image.map((p) => ({
|
||||
image: p.attachments.map((p) => ({
|
||||
id: makeId(10),
|
||||
path: p,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class IntegrationTriggerTool implements AgentToolInterface {
|
|||
),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
output: z.record(z.string(), z.any()),
|
||||
output: z.array(z.object()),
|
||||
}),
|
||||
execute: async ({ runtimeContext, context }) => {
|
||||
console.log('triggerTool', context);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export class IntegrationValidationTool implements AgentToolInterface {
|
|||
}),
|
||||
outputSchema: z.object({
|
||||
output: z.object({
|
||||
rules: z.string(),
|
||||
maxLength: z
|
||||
.number()
|
||||
.describe('The maximum length of a post / comment'),
|
||||
|
|
@ -77,7 +78,7 @@ export class IntegrationValidationTool implements AgentToolInterface {
|
|||
|
||||
if (!integration) {
|
||||
return {
|
||||
output: { maxLength: 0, settings: {}, tools: [] },
|
||||
output: { rules: '', maxLength: 0, settings: {}, tools: [] },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -86,9 +87,11 @@ export class IntegrationValidationTool implements AgentToolInterface {
|
|||
? false
|
||||
: validationMetadatasToSchemas()[integration.dto.name];
|
||||
const tools = this._integrationManager.getAllTools();
|
||||
const rules = this._integrationManager.getAllRulesDescription();
|
||||
|
||||
return {
|
||||
output: {
|
||||
rules: rules[integration.identifier],
|
||||
maxLength,
|
||||
settings: !schemas ? 'No additional settings required' : schemas,
|
||||
tools: tools[integration.identifier],
|
||||
|
|
|
|||
|
|
@ -96,6 +96,22 @@ export class IntegrationManager {
|
|||
);
|
||||
}
|
||||
|
||||
getAllRulesDescription(): {
|
||||
[key: string]: string;
|
||||
} {
|
||||
return socialIntegrationList.reduce(
|
||||
(all, current) => ({
|
||||
...all,
|
||||
[current.identifier]:
|
||||
Reflect.getMetadata(
|
||||
'custom:rules:description',
|
||||
current.constructor
|
||||
) || '',
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
getAllPlugs() {
|
||||
return socialIntegrationList
|
||||
.map((p) => {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
|||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import axios from 'axios';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
async function reduceImageBySize(url: string, maxSizeKB = 976) {
|
||||
try {
|
||||
|
|
@ -141,6 +142,9 @@ async function uploadVideo(
|
|||
} satisfies AppBskyEmbedVideo.Main;
|
||||
}
|
||||
|
||||
@Rules(
|
||||
'Bluesky can have maximum 1 video or 4 pictures in one post, it can also be without attachments'
|
||||
)
|
||||
export class BlueskyProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 2; // Bluesky has moderate rate limits
|
||||
identifier = 'bluesky';
|
||||
|
|
|
|||
|
|
@ -13,11 +13,15 @@ import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
|||
import { groupBy } from 'lodash';
|
||||
import { FarcasterDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/farcaster.dto';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
const client = new NeynarAPIClient({
|
||||
apiKey: process.env.NEYNAR_SECRET_KEY || '00000000-000-0000-000-000000000000',
|
||||
});
|
||||
|
||||
@Rules(
|
||||
'Farcaster/Warpcast can only accept pictures'
|
||||
)
|
||||
export class FarcasterProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
|
|
|
|||
|
|
@ -108,6 +108,11 @@ export class HashnodeProvider extends SocialAbstract implements SocialProvider {
|
|||
return tags.map((tag) => ({ value: tag.objectID, label: tag.name }));
|
||||
}
|
||||
|
||||
@Tool({ description: 'Tags', dataSchema: [] })
|
||||
tagsList() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
@Tool({ description: 'Publications', dataSchema: [] })
|
||||
async publications(accessToken: string) {
|
||||
const {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import dayjs from 'dayjs';
|
|||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
"Instagram should have at least one attachment, if it's a story, it can have only one picture"
|
||||
)
|
||||
export class InstagramProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.ab
|
|||
import { InstagramDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/instagram.dto';
|
||||
import { InstagramProvider } from '@gitroom/nestjs-libraries/integrations/social/instagram.provider';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
const instagramProvider = new InstagramProvider();
|
||||
|
||||
@Rules(
|
||||
"Instagram should have at least one attachment, if it's a story, it can have only one picture"
|
||||
)
|
||||
export class InstagramStandaloneProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import dayjs from 'dayjs';
|
|||
import { Integration } from '@prisma/client';
|
||||
import { Plug } from '@gitroom/helpers/decorators/plug.decorator';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
'LinkedIn can have maximum one attachment when selecting video, when choosing a carousel on LinkedIn minimum amount of attachment must be two, and only pictures, if uploading a video, LinkedIn can have only one attachment'
|
||||
)
|
||||
export class LinkedinPageProvider
|
||||
extends LinkedinProvider
|
||||
implements SocialProvider
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ import { PostPlug } from '@gitroom/helpers/decorators/post.plug';
|
|||
import { LinkedinDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/linkedin.dto';
|
||||
import imageToPDF from 'image-to-pdf';
|
||||
import { Readable } from 'stream';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
'LinkedIn can have maximum one attachment when selecting video, when choosing a carousel on LinkedIn minimum amount of attachment must be two, and only pictures, if uploading a video, LinkedIn can have only one attachment'
|
||||
)
|
||||
export class LinkedinProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'linkedin';
|
||||
name = 'LinkedIn';
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ import { timer } from '@gitroom/helpers/utils/timer';
|
|||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import dayjs from 'dayjs';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
'Pinterest requires at least one media, if posting a video, you must have two attachment, one for video, one for the cover picture, When posting a video, there can be only one'
|
||||
)
|
||||
export class PinterestProvider
|
||||
extends SocialAbstract
|
||||
implements SocialProvider
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ import {
|
|||
import { TikTokDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/tiktok.dto';
|
||||
import { timer } from '@gitroom/helpers/utils/timer';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
'TikTok can have one video or one picture or multiple pictures, it cannot be without an attachment'
|
||||
)
|
||||
export class TiktokProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'tiktok';
|
||||
name = 'Tiktok';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ import dayjs from 'dayjs';
|
|||
import { uniqBy } from 'lodash';
|
||||
import { stripHtmlValidation } from '@gitroom/helpers/utils/strip.html.validation';
|
||||
import { XDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/x.dto';
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
@Rules(
|
||||
'X can have maximum 4 pictures, or maximum one video, it can also be without attachments'
|
||||
)
|
||||
export class XProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'x';
|
||||
name = 'X';
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import * as process from 'node:process';
|
|||
import dayjs from 'dayjs';
|
||||
import { GaxiosResponse } from 'gaxios/build/src/common';
|
||||
import Schema$Video = youtube_v3.Schema$Video;
|
||||
import { Rules } from '@gitroom/nestjs-libraries/chat/rules.description.decorator';
|
||||
|
||||
const clientAndYoutube = () => {
|
||||
const client = new google.auth.OAuth2({
|
||||
|
|
@ -47,6 +48,9 @@ const clientAndYoutube = () => {
|
|||
return { client, youtube, oauth2, youtubeAnalytics };
|
||||
};
|
||||
|
||||
@Rules(
|
||||
'YouTube must have on video attachment, it cannot be empty'
|
||||
)
|
||||
export class YoutubeProvider extends SocialAbstract implements SocialProvider {
|
||||
override maxConcurrentJob = 1; // YouTube has strict upload quotas
|
||||
identifier = 'youtube';
|
||||
|
|
|
|||
Loading…
Reference in New Issue