Feat: agent

This commit is contained in:
Nevo David 2025-10-07 17:19:18 +07:00
parent 73ba9acf8c
commit 41e7d0fdeb
20 changed files with 190 additions and 82 deletions

View File

@ -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>

View File

@ -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}
/>

View File

@ -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,
});

View File

@ -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'),

View File

@ -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
);
};
}

View File

@ -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,
})),

View File

@ -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);

View File

@ -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],

View File

@ -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) => {

View File

@ -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';

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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';

View File

@ -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

View File

@ -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';

View File

@ -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';

View File

@ -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';