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 =