From d0cde4876a76c6508406a037409e2bcef9f16489 Mon Sep 17 00:00:00 2001 From: Yash Patel Date: Tue, 23 Apr 2024 00:14:51 +0530 Subject: [PATCH] its a final count down --- apps/api/libs/core/src/ai.service.ts | 20 +++- apps/api/libs/core/src/constants.ts | 1 + apps/api/libs/core/src/tools/browser.ts | 14 +++ apps/api/libs/core/src/tools/editor.ts | 24 +++-- apps/api/libs/core/src/tools/terminal.ts | 20 +++- apps/api/libs/core/src/types/message.ts | 5 + apps/api/src/resolvers/message.resolver.ts | 8 ++ apps/api/src/services/message.service.ts | 22 ++++- apps/api/src/types/context.ts | 4 - apps/web/src/components/Editor.tsx | 9 +- apps/web/src/components/Plan.tsx | 2 +- apps/web/src/components/Shell.tsx | 9 +- apps/web/src/providers/PreviewProvider.tsx | 104 ++++++++++++++++++++- apps/web/src/types.ts | 16 ++++ 14 files changed, 234 insertions(+), 24 deletions(-) create mode 100644 apps/api/libs/core/src/types/message.ts diff --git a/apps/api/libs/core/src/ai.service.ts b/apps/api/libs/core/src/ai.service.ts index 8f427cf..6492976 100644 --- a/apps/api/libs/core/src/ai.service.ts +++ b/apps/api/libs/core/src/ai.service.ts @@ -1,10 +1,11 @@ import { Injectable, Logger } from '@nestjs/common' -import { OnEvent } from '@nestjs/event-emitter' +import { EventEmitter2, OnEvent } from '@nestjs/event-emitter' import { MessageEntity } from '@/models/message' import { END, StateGraph } from '@langchain/langgraph' import { AGENT_NODE, MESSAGE_RECEIVED_EVENT, + MESSAGE_RESPONSE_EVENT, PLANNER_NODE, REPLANNER_NODE } from './constants' @@ -13,6 +14,7 @@ import PlannerAgent from './agents/planner' import RePlannerAgent from './agents/replanner' import { PlanExecuteState } from './types/agent' import { Pregel } from '@langchain/langgraph/dist/pregel' +import { MessageResponseEvent } from './types/message' @Injectable() export class AIService { @@ -22,7 +24,8 @@ export class AIService { constructor( private readonly openaiAgent: OpenAIAgent, private readonly plannerAgent: PlannerAgent, - private readonly replannerAgent: RePlannerAgent + private readonly replannerAgent: RePlannerAgent, + private readonly eventEmitter: EventEmitter2 ) { const workflow = new StateGraph({ channels: { @@ -90,7 +93,18 @@ export class AIService { recursionLimit: 50 } )) { - this.logger.log(`Event: ${JSON.stringify(event)}`) + this.logger.debug(`Event: `, event) + + if (END in event) { + // TODO: refactor this, not a right place + const input: MessageResponseEvent = { + content: event[END].response, + projectId: project.id, + deviceId: project.deviceId + } + + this.eventEmitter.emit(MESSAGE_RESPONSE_EVENT, input) + } } } } diff --git a/apps/api/libs/core/src/constants.ts b/apps/api/libs/core/src/constants.ts index f11bbea..a3c9d99 100644 --- a/apps/api/libs/core/src/constants.ts +++ b/apps/api/libs/core/src/constants.ts @@ -1,6 +1,7 @@ export const MESSAGE_CREATED_EVENT = 'message::created' export const MESSAGE_SENT_EVENT = 'message::sent' export const MESSAGE_RECEIVED_EVENT = 'message::received' +export const MESSAGE_RESPONSE_EVENT = 'message::response' export const PREVIEW_EVENT = 'preview::event' diff --git a/apps/api/libs/core/src/tools/browser.ts b/apps/api/libs/core/src/tools/browser.ts index 17c95df..662c401 100644 --- a/apps/api/libs/core/src/tools/browser.ts +++ b/apps/api/libs/core/src/tools/browser.ts @@ -2,6 +2,9 @@ import { z } from 'zod' import { Tool } from './tool' import { Injectable } from '@nestjs/common' import { chromium } from 'playwright' +import { Browser } from '../models/browser' +import { PREVIEW_EVENT, TOPIC_BROWSER } from '../constants' +import { EventEmitter2 } from '@nestjs/event-emitter' @Injectable() class BrowserTool extends Tool { @@ -17,6 +20,10 @@ class BrowserTool extends Tool { ) }) + constructor(private readonly eventEmitter: EventEmitter2) { + super() + } + public async execute({ url }) { this.logger.debug(`testing or previewing URL: ${url}`) @@ -31,6 +38,13 @@ class BrowserTool extends Tool { await browser.close() + const onBrowserPreview: Browser = { + url, + content + } + + this.eventEmitter.emit(PREVIEW_EVENT, TOPIC_BROWSER, { onBrowserPreview }) + return content } } diff --git a/apps/api/libs/core/src/tools/editor.ts b/apps/api/libs/core/src/tools/editor.ts index 1f96d64..85864e2 100644 --- a/apps/api/libs/core/src/tools/editor.ts +++ b/apps/api/libs/core/src/tools/editor.ts @@ -3,6 +3,9 @@ import { Tool } from './tool' import { Injectable } from '@nestjs/common' import FileSystemService from '../providers/file-system.service' import { join } from 'path' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { PREVIEW_EVENT, TOPIC_EDITOR } from '../constants' +import { Editor } from '../models/editor' @Injectable() class EditorTool extends Tool { @@ -15,15 +18,14 @@ class EditorTool extends Tool { .enum(['READ', 'WRITE', 'DELETE']) .describe('The action to perform.'), filename: z.string().describe('The name of the file to edit or write.'), - path: z - .string() - .describe( - 'The relative path from the project root to the file or directory.' - ), + path: z.string().describe('The relative path to the file or directory.'), data: z.string().optional().describe('The data to write to the file.') }) - constructor(private readonly fileSystemService: FileSystemService) { + constructor( + private readonly fileSystemService: FileSystemService, + private readonly eventEmitter: EventEmitter2 + ) { super() } @@ -36,6 +38,16 @@ class EditorTool extends Tool { case 'READ': return this.fileSystemService.readFile(location) case 'WRITE': + const onEditorPreview: Editor = { + path, + fileName: filename, + content: data + } + + this.eventEmitter.emit(PREVIEW_EVENT, TOPIC_EDITOR, { + onEditorPreview + }) + return this.fileSystemService.writeFile(location, data || '') case 'DELETE': return this.fileSystemService.deleteFile(location) diff --git a/apps/api/libs/core/src/tools/terminal.ts b/apps/api/libs/core/src/tools/terminal.ts index 4fd919e..08f45d1 100644 --- a/apps/api/libs/core/src/tools/terminal.ts +++ b/apps/api/libs/core/src/tools/terminal.ts @@ -2,6 +2,9 @@ import { z } from 'zod' import { Injectable } from '@nestjs/common' import { Tool } from './tool' import DockerService from '../providers/docker.service' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { Terminal } from '../models/terminal' +import { PREVIEW_EVENT, TOPIC_TERMINAL } from '../constants' @Injectable() class TerminalTool extends Tool { @@ -13,16 +16,27 @@ class TerminalTool extends Tool { command: z.string().describe('The shell command to execute.') }) - constructor(private readonly dockerService: DockerService) { + constructor( + private readonly dockerService: DockerService, + private readonly eventEmitter: EventEmitter2 + ) { super() - this.dockerService.initialize() } async execute({ command, projectId }) { this.logger.debug(`Executing shell command: ${command}`) - return this.dockerService.executeCommand(command, projectId) + const output = await this.dockerService.executeCommand(command, projectId) + + const onTerminalPreview: Terminal = { + command, + output + } + + this.eventEmitter.emit(PREVIEW_EVENT, TOPIC_TERMINAL, { onTerminalPreview }) + + return output } } diff --git a/apps/api/libs/core/src/types/message.ts b/apps/api/libs/core/src/types/message.ts new file mode 100644 index 0000000..2fa4567 --- /dev/null +++ b/apps/api/libs/core/src/types/message.ts @@ -0,0 +1,5 @@ +export interface MessageResponseEvent { + content: string + projectId: string + deviceId: string +} diff --git a/apps/api/src/resolvers/message.resolver.ts b/apps/api/src/resolvers/message.resolver.ts index b74cdf9..e4071ec 100644 --- a/apps/api/src/resolvers/message.resolver.ts +++ b/apps/api/src/resolvers/message.resolver.ts @@ -11,12 +11,20 @@ import { Resolver, Subscription } from '@nestjs/graphql' +import { OnEvent } from '@nestjs/event-emitter' +import { MESSAGE_RECEIVED_EVENT } from '@core/core/constants' +import { MessageResponseEvent } from '@core/core/types/message' @Resolver() export class MessageResolver { private readonly logger = new Logger(MessageResolver.name) constructor(private readonly service: MessageService) {} + @OnEvent(MESSAGE_RECEIVED_EVENT) + async onMessageReceived(event: MessageResponseEvent): Promise { + return this.service.handleMessageResponse(event) + } + @Mutation(() => Message) @UseGuards(DeviceIdGuard, ProjectIdGuard) async createMessage( diff --git a/apps/api/src/services/message.service.ts b/apps/api/src/services/message.service.ts index 3bfb027..6f02924 100644 --- a/apps/api/src/services/message.service.ts +++ b/apps/api/src/services/message.service.ts @@ -12,6 +12,7 @@ import { Injectable, Logger } from '@nestjs/common' import { EventEmitter2, OnEvent } from '@nestjs/event-emitter' import { InjectRepository } from '@nestjs/typeorm' import { DataSource, Repository } from 'typeorm' +import { MessageResponseEvent } from '@core/core/types/message' @Injectable() export class MessageService { @@ -31,9 +32,28 @@ export class MessageService { private readonly eventEmitter: EventEmitter2 ) {} + async handleMessageResponse(event: MessageResponseEvent): Promise { + this.logger.debug(`handling message response`, event) + + const message = await this._create(event.projectId, event.deviceId, { + content: event.content, + author: Author.ASSISTANT + }) + + this.eventEmitter.emit(MESSAGE_SENT_EVENT, message) + } + async create( ctx: IContext, input: CreateMessageInput + ): Promise { + return this._create(ctx.req.projectId, ctx.req.deviceId, input) + } + + async _create( + projectId: string, + deviceId: string, + input: CreateMessageInput ): Promise { const queryRunner = this.dataSource.createQueryRunner() @@ -44,7 +64,7 @@ export class MessageService { // Note: not depending on the active project from the database // in-order to be consistent with the frontend const project = await queryRunner.manager.findOne(ProjectEntity, { - where: { id: ctx.req.projectId, deviceId: ctx.req.deviceId } + where: { id: projectId, deviceId } }) if (!project) { diff --git a/apps/api/src/types/context.ts b/apps/api/src/types/context.ts index a54f54e..6cfacd0 100644 --- a/apps/api/src/types/context.ts +++ b/apps/api/src/types/context.ts @@ -3,8 +3,4 @@ export interface IContext { deviceId: string projectId: string } - connection: { - deviceId: string - projectId: string - } } diff --git a/apps/web/src/components/Editor.tsx b/apps/web/src/components/Editor.tsx index 1d0fb57..46470f4 100644 --- a/apps/web/src/components/Editor.tsx +++ b/apps/web/src/components/Editor.tsx @@ -1,10 +1,12 @@ import React, { useEffect } from 'react' import CodeEditor, { useMonaco } from '@monaco-editor/react' import useTheme from '@/hooks/use-theme' +import usePreview from '@/hooks/use-preview' const Editor: React.FC = () => { const monaco = useMonaco() const { isDarkMode } = useTheme() + const { editor: editorPreview } = usePreview() useEffect(() => { monaco?.editor.defineTheme('manas-ai', { @@ -26,11 +28,14 @@ const Editor: React.FC = () => { height="100%" theme="manas-ai" defaultLanguage="python" - defaultValue={`"""Welcome to ManasAI Editor! 🚀 + defaultValue={ + editorPreview.content ?? + `"""Welcome to ManasAI Editor! 🚀 ManasAI will write a code once starts executing its plan. """ -`} +` + } options={{ fontFamily: 'SF Mono, Menlo, Roboto Mono, Ubuntu Mono, Oxygen Mono, monospace', diff --git a/apps/web/src/components/Plan.tsx b/apps/web/src/components/Plan.tsx index 6ba9398..853021a 100644 --- a/apps/web/src/components/Plan.tsx +++ b/apps/web/src/components/Plan.tsx @@ -16,7 +16,7 @@ const Plan: React.FC = () => {

)} -
+
{steps.map((step, idx) => (
diff --git a/apps/web/src/components/Shell.tsx b/apps/web/src/components/Shell.tsx index 0a0a0aa..c0e2fc0 100644 --- a/apps/web/src/components/Shell.tsx +++ b/apps/web/src/components/Shell.tsx @@ -3,10 +3,12 @@ import { Terminal } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' import '@xterm/xterm/css/xterm.css' import useTheme from '@/hooks/use-theme' +import usePreview from '@/hooks/use-preview' const Shell: React.FC = () => { const playgroundRef = useRef(null) const { isDarkMode } = useTheme() + const { terminal: commands } = usePreview() useLayoutEffect(() => { if (!playgroundRef.current) return @@ -62,10 +64,15 @@ const Shell: React.FC = () => { fitAddon.fit() }, 0) + commands.forEach(({ command, output }) => { + terminal.write(command) + terminal.writeln(`\r\n${output}\r\n $`) + }) + return () => { terminal.dispose() } - }, [playgroundRef, isDarkMode]) + }, [playgroundRef, isDarkMode, commands]) return
} diff --git a/apps/web/src/providers/PreviewProvider.tsx b/apps/web/src/providers/PreviewProvider.tsx index abf169c..6c76f41 100644 --- a/apps/web/src/providers/PreviewProvider.tsx +++ b/apps/web/src/providers/PreviewProvider.tsx @@ -14,14 +14,69 @@ const PLAN_PREVIEW_SUBSCRIPTION = gql` } ` +const EDITOR_PREVIEW_SUBSCRIPTION = gql` + subscription EditorPreviewSubscription( + $projectId: String! + $deviceId: String! + ) { + onEditorPreview(projectId: $projectId, deviceId: $deviceId) { + content + } + } +` + +const BROWSER_PREVIEW_SUBSCRIPTION = gql` + subscription BrowserPreviewSubscription( + $projectId: String! + $deviceId: String! + ) { + onBrowserPreview(projectId: $projectId, deviceId: $deviceId) { + url + content + } + } +` + +const TERMINAL_PREVIEW_SUBSCRIPTION = gql` + subscription TerminalPreviewSubscription( + $projectId: String! + $deviceId: String! + ) { + onTerminalPreview(projectId: $projectId, deviceId: $deviceId) { + command + output + } + } +` + export const PreviewContext = createContext({ plan: { steps: [] - } + }, + editor: { + content: null, + fileName: null, + path: null + }, + browser: { + url: null, + content: null + }, + terminal: [] }) export const PreviewProvider: React.FC = ({ children }) => { const [steps, setSteps] = useState([]) + const [editor, setEditor] = useState({ + content: null, + fileName: null, + path: null + }) + const [browser, setBrowser] = useState({ + url: null, + content: null + }) + const [commands, setCommands] = useState([]) useSubscription(PLAN_PREVIEW_SUBSCRIPTION, { variables: { @@ -33,13 +88,56 @@ export const PreviewProvider: React.FC = ({ children }) => { data: { onPlanPreview } } }) => { - console.log({ onPlanPreview }) setSteps(onPlanPreview.steps) } }) + useSubscription(EDITOR_PREVIEW_SUBSCRIPTION, { + variables: { + projectId: localStorage.getItem(PROJECT_ID), + deviceId: localStorage.getItem(DEVICE_ID) + }, + onData: ({ + data: { + data: { onEditorPreview } + } + }) => { + setEditor(onEditorPreview) + } + }) + + useSubscription(BROWSER_PREVIEW_SUBSCRIPTION, { + variables: { + projectId: localStorage.getItem(PROJECT_ID), + deviceId: localStorage.getItem(DEVICE_ID) + }, + onData: ({ + data: { + data: { onBrowserPreview } + } + }) => { + setBrowser(onBrowserPreview) + } + }) + + useSubscription(TERMINAL_PREVIEW_SUBSCRIPTION, { + variables: { + projectId: localStorage.getItem(PROJECT_ID), + deviceId: localStorage.getItem(DEVICE_ID) + }, + onData: ({ + data: { + data: { onTerminalPreview } + } + }) => { + setCommands(commands => [...commands, onTerminalPreview]) + } + }) + return ( - + {children} ) diff --git a/apps/web/src/types.ts b/apps/web/src/types.ts index 4c585a6..3e67ea3 100644 --- a/apps/web/src/types.ts +++ b/apps/web/src/types.ts @@ -44,6 +44,22 @@ export interface IPreviewContext { plan: { steps: string[] } + + editor: { + content: string | null + fileName: string | null + path: string | null + } + + browser: { + url: string | null + content: string | null + } + + terminal: { + command: string + output: string + }[] } export type FormActions =