From 73ba9acf8c537e7fffb55ad76888597e6c36f81a Mon Sep 17 00:00:00 2001 From: Nevo David Date: Tue, 7 Oct 2025 11:07:21 +0700 Subject: [PATCH 1/5] feat: agent --- .vscode/settings.json | 28 + .../src/api/routes/copilot.controller.ts | 77 +- apps/backend/src/app.module.ts | 7 +- .../src/services/auth/auth.middleware.ts | 2 +- .../src/app/(app)/(site)/agents/page.tsx | 11 + apps/frontend/src/app/global.scss | 21 + .../src/components/agents/agent.input.tsx | 120 + .../src/components/agents/agent.textarea.tsx | 77 + apps/frontend/src/components/agents/agent.tsx | 342 + .../src/components/layout/top.menu.tsx | 18 + .../src/components/media/media.component.tsx | 6 +- .../new-launch/providers/vk/vk.provider.tsx | 1 + .../helpers/src/utils/use.wait.for.class.tsx | 43 + .../src/chat/agent.tool.interface.ts | 4 + .../nestjs-libraries/src/chat/chat.module.ts | 13 + .../src/chat/load.tools.service.ts | 83 + .../src/chat/mastra.service.ts | 21 + .../nestjs-libraries/src/chat/mastra.store.ts | 5 + .../chat/tools/integration.schedule.post.ts | 143 + .../chat/tools/integration.trigger.tool.ts | 151 + .../chat/tools/integration.validation.tool.ts | 100 + .../src/chat/tools/tool.list.ts | 9 + .../posts/providers-settings/farcaster.dto.ts | 17 + .../src/integrations/integration.manager.ts | 18 + .../integrations/social/bluesky.provider.ts | 3 + .../integrations/social/dev.to.provider.ts | 10 +- .../integrations/social/discord.provider.ts | 8 + .../integrations/social/dribbble.provider.ts | 7 + .../integrations/social/facebook.provider.ts | 14 +- .../integrations/social/farcaster.provider.ts | 12 +- .../integrations/social/hashnode.provider.ts | 6 + .../integrations/social/instagram.provider.ts | 37 +- .../social/instagram.standalone.provider.ts | 17 +- .../src/integrations/social/lemmy.provider.ts | 15 + .../integrations/social/linkedin.provider.ts | 4 +- .../integrations/social/listmonk.provider.ts | 8 + .../integrations/social/mastodon.provider.ts | 3 + .../integrations/social/medium.provider.ts | 7 + .../src/integrations/social/nostr.provider.ts | 4 + .../integrations/social/pinterest.provider.ts | 7 + .../integrations/social/reddit.provider.ts | 26 + .../src/integrations/social/slack.provider.ts | 12 + .../social/social.integrations.interface.ts | 2 + .../integrations/social/telegram.provider.ts | 9 +- .../integrations/social/threads.provider.ts | 21 +- .../integrations/social/tiktok.provider.ts | 5 +- .../src/integrations/social/vk.provider.ts | 3 + .../integrations/social/wordpress.provider.ts | 9 + .../src/integrations/social/x.provider.ts | 6 + .../integrations/social/youtube.provider.ts | 4 + .../src/integrations/tool.decorator.ts | 17 + .../nestjs-libraries/src/mcp/mcp.types.ts | 4 +- package.json | 17 +- pnpm-lock.yaml | 6680 ++++++++++++----- 54 files changed, 6261 insertions(+), 2033 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 apps/frontend/src/app/(app)/(site)/agents/page.tsx create mode 100644 apps/frontend/src/components/agents/agent.input.tsx create mode 100644 apps/frontend/src/components/agents/agent.textarea.tsx create mode 100644 apps/frontend/src/components/agents/agent.tsx create mode 100644 libraries/helpers/src/utils/use.wait.for.class.tsx create mode 100644 libraries/nestjs-libraries/src/chat/agent.tool.interface.ts create mode 100644 libraries/nestjs-libraries/src/chat/chat.module.ts create mode 100644 libraries/nestjs-libraries/src/chat/load.tools.service.ts create mode 100644 libraries/nestjs-libraries/src/chat/mastra.service.ts create mode 100644 libraries/nestjs-libraries/src/chat/mastra.store.ts create mode 100644 libraries/nestjs-libraries/src/chat/tools/integration.schedule.post.ts create mode 100644 libraries/nestjs-libraries/src/chat/tools/integration.trigger.tool.ts create mode 100644 libraries/nestjs-libraries/src/chat/tools/integration.validation.tool.ts create mode 100644 libraries/nestjs-libraries/src/chat/tools/tool.list.ts create mode 100644 libraries/nestjs-libraries/src/dtos/posts/providers-settings/farcaster.dto.ts create mode 100644 libraries/nestjs-libraries/src/integrations/tool.decorator.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..0c522537 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,28 @@ +{ + "explorer.compactFolders": false, + "explorer.fileNesting.enabled": false, + "editor.lineHeight": 27, + "editor.folding": true, + "editor.foldingImportsByDefault": true, + "editor.fontLigatures": false, + "gitlens.codeLens.enabled": false, + "gitlens.currentLine.enabled": false, + "typescript.updateImportsOnFileMove.enabled": "always", + "typescript.suggest.autoImports": true, + "javascript.suggest.autoImports": true, + "editor.codeActionsOnSave": { + "source.addMissingImports": "explicit" + }, + "workbench.activityBar.orientation": "vertical", + "workbench.colorCustomizations": { + "activityBar.background": "#393c3e", + "editor.background": "#2b2b2b", + "editorGutter.background": "#2b2b2b", + "editorIndentGuide.background1": "#373737", + "editorIndentGuide.activeBackground1": "#587973", + "editor.lineHighlightBackground": "#323232", + "editor.selectionHighlightBackground": "#40404080", + "editor.selectionBackground": "#214283", + "editor.hoverHighlightBackground": "#40404080", + } +} diff --git a/apps/backend/src/api/routes/copilot.controller.ts b/apps/backend/src/api/routes/copilot.controller.ts index 581343fb..5e23e6fd 100644 --- a/apps/backend/src/api/routes/copilot.controller.ts +++ b/apps/backend/src/api/routes/copilot.controller.ts @@ -2,15 +2,29 @@ import { Logger, Controller, Get, Post, Req, Res, Query } from '@nestjs/common'; import { CopilotRuntime, OpenAIAdapter, - copilotRuntimeNestEndpoint, + copilotRuntimeNodeHttpEndpoint, } from '@copilotkit/runtime'; import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request'; import { Organization } from '@prisma/client'; import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; +import { MastraAgent } from '@ag-ui/mastra'; +import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service'; +import { Mastra } from '@mastra/core/dist/mastra'; +import { Request, Response } from 'express'; +import { RuntimeContext } from '@mastra/core/di'; +let mastra: Mastra; + +export type ChannelsContext = { + integrations: string; + organization: string; +}; @Controller('/copilot') export class CopilotController { - constructor(private _subscriptionService: SubscriptionService) {} + constructor( + private _subscriptionService: SubscriptionService, + private _mastraService: MastraService + ) {} @Post('/chat') chat(@Req() req: Request, @Res() res: Response) { if ( @@ -21,28 +35,67 @@ export class CopilotController { return; } - const copilotRuntimeHandler = copilotRuntimeNestEndpoint({ + const copilotRuntimeHandler = copilotRuntimeNodeHttpEndpoint({ endpoint: '/copilot/chat', runtime: new CopilotRuntime(), serviceAdapter: new OpenAIAdapter({ - model: - // @ts-ignore - req?.body?.variables?.data?.metadata?.requestType === - 'TextareaCompletion' - ? 'gpt-4o-mini' - : 'gpt-4.1', + model: 'gpt-4.1', + }), + }); + + return copilotRuntimeHandler(req, res); + } + + @Post('/agent') + async agent( + @Req() req: Request, + @Res() res: Response, + @GetOrgFromRequest() organization: Organization + ) { + if ( + process.env.OPENAI_API_KEY === undefined || + process.env.OPENAI_API_KEY === '' + ) { + Logger.warn('OpenAI API key not set, chat functionality will not work'); + return; + } + mastra = mastra || (await this._mastraService.mastra()); + const runtimeContext = new RuntimeContext(); + runtimeContext.set( + 'integrations', + req?.body?.variables?.properties?.integrations || [] + ); + + runtimeContext.set('organization', organization.id); + + const runtime = new CopilotRuntime({ + agents: MastraAgent.getLocalAgents({ + mastra, + // @ts-ignore + runtimeContext, + }), + }); + + const copilotRuntimeHandler = copilotRuntimeNodeHttpEndpoint({ + endpoint: '/copilot/agent', + runtime, + properties: req.body.variables.properties, + serviceAdapter: new OpenAIAdapter({ + model: 'gpt-4.1', }), }); - // @ts-ignore return copilotRuntimeHandler(req, res); } @Get('/credits') calculateCredits( @GetOrgFromRequest() organization: Organization, - @Query('type') type: 'ai_images' | 'ai_videos', + @Query('type') type: 'ai_images' | 'ai_videos' ) { - return this._subscriptionService.checkCredits(organization, type || 'ai_images'); + return this._subscriptionService.checkCredits( + organization, + type || 'ai_images' + ); } } diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index c5bbf8ae..b1066fce 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -11,8 +11,9 @@ import { AgentModule } from '@gitroom/nestjs-libraries/agent/agent.module'; import { McpModule } from '@gitroom/backend/mcp/mcp.module'; import { ThirdPartyModule } from '@gitroom/nestjs-libraries/3rdparties/thirdparty.module'; import { VideoModule } from '@gitroom/nestjs-libraries/videos/video.module'; -import { SentryModule } from "@sentry/nestjs/setup"; +import { SentryModule } from '@sentry/nestjs/setup'; import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception'; +import { ChatModule } from '@gitroom/nestjs-libraries/chat/chat.module'; @Global() @Module({ @@ -26,6 +27,7 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception'; McpModule, ThirdPartyModule, VideoModule, + ChatModule, ThrottlerModule.forRoot([ { ttl: 3600000, @@ -43,7 +45,7 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception'; { provide: APP_GUARD, useClass: PoliciesGuard, - } + }, ], exports: [ BullMqModule, @@ -53,6 +55,7 @@ import { FILTER } from '@gitroom/nestjs-libraries/sentry/sentry.exception'; AgentModule, McpModule, ThrottlerModule, + ChatModule, ], }) export class AppModule {} diff --git a/apps/backend/src/services/auth/auth.middleware.ts b/apps/backend/src/services/auth/auth.middleware.ts index 0ef8377a..3bb3fea5 100644 --- a/apps/backend/src/services/auth/auth.middleware.ts +++ b/apps/backend/src/services/auth/auth.middleware.ts @@ -6,6 +6,7 @@ import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/o import { UsersService } from '@gitroom/nestjs-libraries/database/prisma/users/users.service'; import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter'; +import { MastraService } from '@gitroom/nestjs-libraries/chat/mastra.service'; export const removeAuth = (res: Response) => { res.cookie('auth', '', { @@ -20,7 +21,6 @@ export const removeAuth = (res: Response) => { expires: new Date(0), maxAge: -1, }); - res.header('logout', 'true'); }; diff --git a/apps/frontend/src/app/(app)/(site)/agents/page.tsx b/apps/frontend/src/app/(app)/(site)/agents/page.tsx new file mode 100644 index 00000000..ac278b81 --- /dev/null +++ b/apps/frontend/src/app/(app)/(site)/agents/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next'; +import { Agent } from '@gitroom/frontend/components/agents/agent'; +export const metadata: Metadata = { + title: 'Agent', + description: '', +}; +export default async function Page() { + return ( + + ); +} diff --git a/apps/frontend/src/app/global.scss b/apps/frontend/src/app/global.scss index 3b446329..acde904a 100644 --- a/apps/frontend/src/app/global.scss +++ b/apps/frontend/src/app/global.scss @@ -678,4 +678,25 @@ html[dir='rtl'] [dir='ltr'] { } .blur-xs { filter: blur(4px); +} + +.agent { + .copilotKitInputContainer { + padding: 0 24px !important; + } + + .copilotKitInput { + width: 100% !important; + } +} +.rm-bg .b2 { + padding-top: 0 !important; +} +.rm-bg .b1 { + background: transparent !important; + gap: 0 !important; +} + +.copilotKitMessage img { + width: 200px; } \ No newline at end of file diff --git a/apps/frontend/src/components/agents/agent.input.tsx b/apps/frontend/src/components/agents/agent.input.tsx new file mode 100644 index 00000000..85d22b9b --- /dev/null +++ b/apps/frontend/src/components/agents/agent.input.tsx @@ -0,0 +1,120 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { useCopilotContext, useCopilotReadable } from '@copilotkit/react-core'; +import AutoResizingTextarea from '@gitroom/frontend/components/agents/agent.textarea'; +import { useChatContext } from '@copilotkit/react-ui'; +import { InputProps } from '@copilotkit/react-ui/dist/components/chat/props'; +const MAX_NEWLINES = 6; + +export const Input = ({ + inProgress, + onSend, + isVisible = false, + onStop, + onUpload, + hideStopButton = false, + onChange, +}: InputProps & { onChange: (value: string) => void }) => { + const context = useChatContext(); + const copilotContext = useCopilotContext(); + const showPoweredBy = !copilotContext.copilotApiConfig?.publicApiKey; + + const textareaRef = useRef(null); + const [isComposing, setIsComposing] = useState(false); + + const handleDivClick = (event: React.MouseEvent) => { + const target = event.target as HTMLElement; + + // If the user clicked a button or inside a button, don't focus the textarea + if (target.closest('button')) return; + + // If the user clicked the textarea, do nothing (it's already focused) + if (target.tagName === 'TEXTAREA') return; + + // Otherwise, focus the textarea + textareaRef.current?.focus(); + }; + + const [text, setText] = useState(''); + const send = () => { + if (inProgress) return; + onSend(text); + setText(''); + + textareaRef.current?.focus(); + }; + + const isInProgress = inProgress; + const buttonIcon = + isInProgress && !hideStopButton + ? context.icons.stopIcon + : context.icons.sendIcon; + + const canSend = useMemo(() => { + const interruptEvent = copilotContext.langGraphInterruptAction?.event; + const interruptInProgress = + interruptEvent?.name === 'LangGraphInterruptEvent' && + !interruptEvent?.response; + + return !isInProgress && text.trim().length > 0 && !interruptInProgress; + }, [copilotContext.langGraphInterruptAction?.event, isInProgress, text]); + + const canStop = useMemo(() => { + return isInProgress && !hideStopButton; + }, [isInProgress, hideStopButton]); + + const sendDisabled = !canSend && !canStop; + + return ( +
+
+ { + onChange(event.target.value); + setText(event.target.value); + }} + onCompositionStart={() => setIsComposing(true)} + onCompositionEnd={() => setIsComposing(false)} + onKeyDown={(event) => { + if (event.key === 'Enter' && !event.shiftKey && !isComposing) { + event.preventDefault(); + if (canSend) { + send(); + } + } + }} + /> +
+ {onUpload && ( + + )} + +
+ +
+
+
+ ); +}; diff --git a/apps/frontend/src/components/agents/agent.textarea.tsx b/apps/frontend/src/components/agents/agent.textarea.tsx new file mode 100644 index 00000000..ad1f9e1e --- /dev/null +++ b/apps/frontend/src/components/agents/agent.textarea.tsx @@ -0,0 +1,77 @@ +import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from "react"; + +interface AutoResizingTextareaProps { + maxRows?: number; + placeholder?: string; + value: string; + onChange: (event: React.ChangeEvent) => void; + onKeyDown?: (event: React.KeyboardEvent) => void; + onCompositionStart?: () => void; + onCompositionEnd?: () => void; + autoFocus?: boolean; +} + +const AutoResizingTextarea = forwardRef( + ( + { + maxRows = 1, + placeholder, + value, + onChange, + onKeyDown, + onCompositionStart, + onCompositionEnd, + autoFocus, + }, + ref, + ) => { + const internalTextareaRef = useRef(null); + const [maxHeight, setMaxHeight] = useState(0); + + useImperativeHandle(ref, () => internalTextareaRef.current as HTMLTextAreaElement); + + useEffect(() => { + const calculateMaxHeight = () => { + const textarea = internalTextareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + const singleRowHeight = textarea.scrollHeight; + setMaxHeight(singleRowHeight * maxRows); + if (autoFocus) { + textarea.focus(); + } + } + }; + + calculateMaxHeight(); + }, [maxRows]); + + useEffect(() => { + const textarea = internalTextareaRef.current; + if (textarea) { + textarea.style.height = "auto"; + textarea.style.height = `${Math.min(textarea.scrollHeight, maxHeight)}px`; + } + }, [value, maxHeight]); + + return ( +