diff --git a/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx b/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx
index 130192eb..7e7a1bcf 100644
--- a/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx
+++ b/apps/frontend/src/components/new-launch/providers/listmonk/listmonk.provider.tsx
@@ -18,7 +18,7 @@ const SettingsComponent = () => {
-
+
>
);
};
@@ -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,
});
diff --git a/libraries/nestjs-libraries/src/chat/load.tools.service.ts b/libraries/nestjs-libraries/src/chat/load.tools.service.ts
index c0729567..cc324174 100644
--- a/libraries/nestjs-libraries/src/chat/load.tools.service.ts
+++ b/libraries/nestjs-libraries/src/chat/load.tools.service.ts
@@ -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'),
diff --git a/libraries/nestjs-libraries/src/chat/rules.description.decorator.ts b/libraries/nestjs-libraries/src/chat/rules.description.decorator.ts
new file mode 100644
index 00000000..a817d629
--- /dev/null
+++ b/libraries/nestjs-libraries/src/chat/rules.description.decorator.ts
@@ -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
+ );
+ };
+}
diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts
index 52af9c7f..8399d1ba 100644
--- a/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts
+++ b/libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts
@@ -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;
+ 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,
})),
diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts b/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts
index e094c3a9..6833286d 100644
--- a/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts
+++ b/libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts
@@ -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);
diff --git a/libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts b/libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts
index 46acf8c9..2c9d84a5 100644
--- a/libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts
+++ b/libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts
@@ -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],
diff --git a/libraries/nestjs-libraries/src/integrations/integration.manager.ts b/libraries/nestjs-libraries/src/integrations/integration.manager.ts
index 598da9e1..dea5122d 100644
--- a/libraries/nestjs-libraries/src/integrations/integration.manager.ts
+++ b/libraries/nestjs-libraries/src/integrations/integration.manager.ts
@@ -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) => {
diff --git a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts
index 8980a4dc..3e1aa59b 100644
--- a/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/bluesky.provider.ts
@@ -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';
diff --git a/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts b/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts
index 7d2e595f..32e68bfa 100644
--- a/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/farcaster.provider.ts
@@ -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
diff --git a/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts b/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts
index a20b0407..41c0c80f 100644
--- a/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/hashnode.provider.ts
@@ -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 {
diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
index 7fe240be..2cc499e2 100644
--- a/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/instagram.provider.ts
@@ -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
diff --git a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
index f8261893..bc058502 100644
--- a/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/instagram.standalone.provider.ts
@@ -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
diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts
index 52cf8aff..fdb66eb6 100644
--- a/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.page.provider.ts
@@ -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
diff --git a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
index 743b4c30..4aa1c9d5 100644
--- a/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/linkedin.provider.ts
@@ -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';
diff --git a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
index 6e9decad..f9779010 100644
--- a/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/pinterest.provider.ts
@@ -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
diff --git a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts
index 227d29ee..41b4eebc 100644
--- a/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/tiktok.provider.ts
@@ -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';
diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
index d28fb976..e1ad9eee 100644
--- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts
@@ -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';
diff --git a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
index e0ca0f09..bf66dc16 100644
--- a/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
+++ b/libraries/nestjs-libraries/src/integrations/social/youtube.provider.ts
@@ -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';