Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate agent #5870

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
36 changes: 33 additions & 3 deletions apps/remix-ide/src/app/plugins/remixAIPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, { useCallback } from 'react';
import { ICompletions, IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, CodeExplainAgent, SecurityAgent } from '@remix/remix-ai-core';
import { CustomRemixApi } from '@remix-api'
import { PluginViewWrapper } from '@remix-ui/helper'
import { CodeCompletionAgent } from '@remix/remix-ai-core';
import { CodeCompletionAgent, ContractAgent } from '@remix/remix-ai-core';
const _paq = (window._paq = window._paq || [])

type chatRequestBufferT<T> = {
Expand All @@ -18,7 +18,7 @@ const profile = {
displayName: 'RemixAI',
methods: ['code_generation', 'code_completion',
"solidity_answer", "code_explaining",
"code_insertion", "error_explaining", "vulnerability_check",
"code_insertion", "error_explaining", "vulnerability_check", 'generate',
"initialize", 'chatPipe', 'ProcessChatRequestBuffer', 'isChatRequestPending'],
events: [],
icon: 'assets/img/remix-logo-blue.png',
Expand All @@ -40,14 +40,14 @@ export class RemixAIPlugin extends ViewPlugin {
chatRequestBuffer: chatRequestBufferT<any> = null
codeExpAgent: CodeExplainAgent
securityAgent: SecurityAgent
contractor: ContractAgent
useRemoteInferencer:boolean = false
dispatch: any
completionAgent: CodeCompletionAgent

constructor(inDesktop:boolean) {
super(profile)
this.isOnDesktop = inDesktop
this.codeExpAgent = new CodeExplainAgent(this)
// user machine dont use ressource for remote inferencing
}

Expand All @@ -66,6 +66,8 @@ export class RemixAIPlugin extends ViewPlugin {
}
this.completionAgent = new CodeCompletionAgent(this)
this.securityAgent = new SecurityAgent(this)
this.codeExpAgent = new CodeExplainAgent(this)
this.contractor = new ContractAgent(this)
}

async initialize(model1?:IModel, model2?:IModel, remoteModel?:IRemoteModel, useRemote?:boolean){
Expand Down Expand Up @@ -174,6 +176,34 @@ export class RemixAIPlugin extends ViewPlugin {
return this.securityAgent.getReport(file)
}

async generate(userPrompt: string, params: IParams=GenerationParams, newThreadID:string=""): Promise<any> {
params.stream_result = false // enforce no stream result
params.threadId = newThreadID
console.log('Generating code for prompt:', userPrompt)

let result = {
"projectName": "SimpleStorage",
"files": [
{
"fileName": "contracts/SimpleStorage.sol",
"content": "pragma solidity ^0.8.0; contract SimpleStorage { uint256 private storedData; function set(uint256 x) public { storedData = x; } function get() public view returns (uint256) { return storedData; } }"
},
{
"fileName": "tests/SimpleStorageTest.sol",
"content": "pragma solidity ^0.8.0; import \"truffle/Assert.sol\"; import \"truffle/DeployedAddresses.sol\"; import \"./SimpleStorage.sol\"; contract SimpleStorageTest { function testInitialValue() public { SimpleStorage simpleStorage = SimpleStorage(DeployedAddresses.SimpleStorage()); uint256 expected = 0; Assert.equal(simpleStorage.get(), expected, \"Initially, the stored data should be zero.\"); } function testSetValue() public { SimpleStorage simpleStorage = new SimpleStorage(); simpleStorage.set(42); Assert.equal(simpleStorage.get(), 42, \"Stored data should be 42.\"); } }"
}
],
"threadID": "thread_5T3AwgoicnkkabuIy2fuGS6t"
}
// if (this.isOnDesktop && !this.useRemoteInferencer) {
// result = await this.call(this.remixDesktopPluginName, 'generate', userPrompt, params)
// } else {
// result = await this.remoteInferencer.generate(userPrompt, params)
// }

return this.contractor.writeContracts(result, userPrompt)
}

async code_insertion(msg_pfx: string, msg_sfx: string): Promise<any> {
if (this.completionAgent.indexer == null || this.completionAgent.indexer == undefined) await this.completionAgent.indexWorkspace()

Expand Down
14 changes: 13 additions & 1 deletion apps/remix-ide/src/assets/list.json
Original file line number Diff line number Diff line change
Expand Up @@ -1044,9 +1044,21 @@
"urls": [
"dweb:/ipfs/QmVtdNYdUC4aX6Uk5LrxDT55B7NgGLnLcA2wTecF5xUbSS"
]
},
{
"path": "soljson-v0.8.29+commit.ab55807c.js",
"version": "0.8.29",
"build": "commit.ab55807c",
"longVersion": "0.8.29+commit.ab55807c",
"keccak256": "0x9545790fce7fb78eba3b4af7f72d179cafd4b05ea9de6a3276e871f040736417",
"sha256": "0x87616a5fc7ab3551f4133bbd2c3e1be123eae219facc2a56f8f3a4366520c67b",
"urls": [
"dweb:/ipfs/QmRoJqB44QhLFfuLEK8axiuLw7V23tSJQkitz6qMPX9wT4"
]
}
],
"releases": {
"0.8.29": "soljson-v0.8.29+commit.ab55807c.js",
"0.8.28": "soljson-v0.8.28+commit.7893614a.js",
"0.8.27": "soljson-v0.8.27+commit.40a35a09.js",
"0.8.26": "soljson-v0.8.26+commit.8a97fa7a.js",
Expand Down Expand Up @@ -1143,5 +1155,5 @@
"0.4.0": "soljson-v0.4.0+commit.acd334c9.js",
"0.3.6": "soljson-v0.3.6+commit.3fc68da5.js"
},
"latestRelease": "0.8.28"
"latestRelease": "0.8.29"
}
6 changes: 6 additions & 0 deletions apps/remixdesktop/src/lib/InferenceServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,4 +526,10 @@ export class InferenceManager implements ICompletions {
}
}

async generate(userPrompt, options:IParams=GenerationParams): Promise<any> {
const payload = { prompt: userPrompt, "endpoint":"generate", ...options }
if (options.stream_result) return this._streamInferenceRequest(payload, AIRequestType.GENERAL)
else return this._makeRequest(payload, AIRequestType.GENERAL)
}

}
6 changes: 5 additions & 1 deletion apps/remixdesktop/src/plugins/remixAIDektop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const clientProfile: Profile = {
description: 'RemixAI provides AI services to Remix IDE Desktop.',
kind: '',
documentation: 'https://remix-ide.readthedocs.io/en/latest/ai.html',
methods: ['initializeModelBackend', 'code_completion', 'code_insertion', 'code_generation', 'code_explaining', 'error_explaining', 'solidity_answer']
methods: ['initializeModelBackend', 'code_completion', 'code_insertion', 'code_generation', 'code_explaining', 'error_explaining', 'solidity_answer', 'generate']
}

class RemixAIDesktopPluginClient extends ElectronBasePluginClient {
Expand Down Expand Up @@ -107,6 +107,10 @@ class RemixAIDesktopPluginClient extends ElectronBasePluginClient {
return this.desktopInferencer.solidity_answer(prompt)
}

async generate(userPrompt): Promise<any> {
return this.desktopInferencer.generate(userPrompt)
}

changemodel(newModel: any){
/// dereference the current static inference object
/// set new one
Expand Down
116 changes: 116 additions & 0 deletions libs/remix-ai-core/src/agents/contractAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { parse } from "path";

const compilationParams = {
optimize: false,
evmVersion: null,
language: 'Solidity',
version: '0.8.28+commit.7893614a'
}

interface CompilationResult {
compilationSucceeded: boolean
errors: string
}

export class ContractAgent {
plugin: any;
readonly generationAttempts: number = 3
nAttempts: number = 0
generationThreadID: string= ''
workspaceName: string = ''

constructor(props) {
this.plugin = props;
}

async writeContracts(payload, userPrompt) {
try {
console.log('Writing contracts', payload)
const parsedFiles = payload
this.generationThreadID = parsedFiles['threadID']
this.workspaceName = parsedFiles['projectName']

this.nAttempts += 1
if (this.nAttempts > this.generationAttempts) {
console.error('Failed to generate the code')
return "Failed to generate secure code on this prompt ````" + userPrompt + "````"
}

const contracts = {}
for (const file of parsedFiles.files) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't handle yet solidity import?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I handle imports by adding all files for compilation see line 33, which make sense, the generation must be complete ( from a compilation point of view)

if (file.fileName.endsWith('.sol')) {
const result:CompilationResult = await this.compilecontracts(file.fileName, file.content)
console.log('compilation result', result)
if (!result.compilationSucceeded) {
// nasty recursion
console.log('compilation failed')
// const newPrompt = `I couldn't compile the contract ${file.fileName}. ${result.errors}. Please try again.`
// await this.plugin.call('remixAI', 'generate', newPrompt, this.generationThreadID); // reuse the same thread
}
}
}

console.log('All source files are compiling')
return "New workspace created: " + this.workspaceName + "\nUse the Hamburger menu to select it!"
} catch (error) {
console.log('Error writing contracts', error)
this.deleteWorkspace(this.workspaceName )
return "Failed to generate secure code on this prompt ```" + userPrompt + "```"
}

}

createWorkspace(workspaceName) {
// first get the workspace names
const ws = this.plugin.call('filePanel', 'getWorkspaces')

if (ws.includes(workspaceName)) {
const newWorkspaceName = workspaceName + '_1'
ws.includes(newWorkspaceName) ?
this.plugin.call('filePanel', 'createWorkspace', newWorkspaceName+'_1', true)
: this.plugin.call('filePanel', 'createWorkspace', workspaceName, true)
}
this.plugin.call('filePanel', 'createWorkspace', workspaceName, true)
}

deleteWorkspace(workspaceName) {
this.plugin.call('filePanel', 'deleteWorkspace', workspaceName)
}

async compilecontracts(fileName, fileContent): Promise<CompilationResult> {

const contract = {}
contract[fileName] = { content : fileContent }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have to put all the files in there:

contract[fileName1] = { content : fileContent1 }
contract[fileName2] = { content : fileContent2 }

etc...

so you should not loop over the files and compile them one by one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already tried this, the issue might me the import name not including the folder

console.log('compiling contract', contract)
const result = await this.plugin.call('solidity' as any, 'compileWithParameters', contract, compilationParams)
console.log('compilation result', result)
const data = result.data
const error = data.errors.find((error) => error.type !== 'Warning')
if (data.errors && data.errors.length && error) {
const msg = `
- Compilation errors: ${data.errors.map((e) => e.formattedMessage)}.
`
return { compilationSucceeded: false, errors: msg }

}

return { compilationSucceeded: true, errors: null }
}

extractImportPaths(text) {

// Define the regex pattern to match import paths
const regex = /import\s*\"([^\"]+)\"\s*;/g;
const paths = [];
let match;

// Use the regex to find all matches in the text
while ((match = regex.exec(text)) !== null) {
// Push the captured path to the paths array
paths.push(match[1]);
}

return paths;
}

}
5 changes: 5 additions & 0 deletions libs/remix-ai-core/src/helpers/streamHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,8 @@ export const HandleStreamResponse = async (streamResponse,
export const UpdateChatHistory = (userPrompt: string, AIAnswer: string) => {
ChatHistory.pushHistory(userPrompt, AIAnswer)
}

export const parseUserInput = (input: string) => {
if (input.trimStart().startsWith('/generate')) return [true, input.replace('/generate', '').trimStart()]
else return [false, input]
}
1 change: 1 addition & 0 deletions libs/remix-ai-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './helpers/streamHandler'
export * from './agents/codeExplainAgent'
export * from './agents/completionAgent'
export * from './agents/securityAgent'
export * from './agents/contractAgent'
8 changes: 7 additions & 1 deletion libs/remix-ai-core/src/inferencers/remote/remoteInference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class RemoteInferencer implements ICompletions {
max_history = 7
model_op = RemoteBackendOPModel.CODELLAMA // default model operation change this to llama if necessary
event: EventEmitter
test_env=false
test_env=true
test_url="http://solcodertest.org"

constructor(apiUrl?:string, completionUrl?:string) {
Expand Down Expand Up @@ -152,4 +152,10 @@ export class RemoteInferencer implements ICompletions {
if (options.stream_result) return this._streamInferenceRequest(payload, AIRequestType.GENERAL)
else return this._makeRequest(payload, AIRequestType.GENERAL)
}

async generate(userPrompt, options:IParams=GenerationParams): Promise<any> {
const payload = { prompt: userPrompt, "endpoint":"generate", ...options }
if (options.stream_result) return this._streamInferenceRequest(payload, AIRequestType.GENERAL)
else return this._makeRequest(payload, AIRequestType.GENERAL)
}
}
1 change: 1 addition & 0 deletions libs/remix-ai-core/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface IParams {
temp?: number;
return_stream_response?: boolean;
terminal_output?: boolean;
threadId?: string;
}

export enum AIRequestType {
Expand Down
10 changes: 0 additions & 10 deletions libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { solidityTokensProvider, solidityLanguageConfig } from './syntaxes/solid
import { cairoTokensProvider, cairoLanguageConfig } from './syntaxes/cairo'
import { zokratesTokensProvider, zokratesLanguageConfig } from './syntaxes/zokrates'
import { moveTokenProvider, moveLanguageConfig } from './syntaxes/move'
import { vyperTokenProvider, vyperLanguageConfig } from './syntaxes/vyper'
import { tomlLanguageConfig, tomlTokenProvider } from './syntaxes/toml'
import { monacoTypes } from '@remix-ui/editor'
import { loadTypes } from './web-types'
Expand Down Expand Up @@ -370,8 +369,6 @@ export const EditorUI = (props: EditorUIProps) => {
monacoRef.current.editor.setModelLanguage(file.model, 'remix-toml')
} else if (file.language === 'noir') {
monacoRef.current.editor.setModelLanguage(file.model, 'remix-noir')
} else if (file.language === 'python') {
monacoRef.current.editor.setModelLanguage(file.model, 'remix-vyper')
}
}, [props.currentFile, props.isDiff])

Expand Down Expand Up @@ -994,7 +991,6 @@ export const EditorUI = (props: EditorUIProps) => {
monacoRef.current.languages.register({ id: 'remix-zokrates' })
monacoRef.current.languages.register({ id: 'remix-move' })
monacoRef.current.languages.register({ id: 'remix-circom' })
monacoRef.current.languages.register({ id: 'remix-vyper' })
monacoRef.current.languages.register({ id: 'remix-toml' })
monacoRef.current.languages.register({ id: 'remix-noir' })

Expand All @@ -1019,7 +1015,6 @@ export const EditorUI = (props: EditorUIProps) => {

monacoRef.current.languages.setMonarchTokensProvider('remix-circom', circomTokensProvider as any)
monacoRef.current.languages.setLanguageConfiguration('remix-circom', circomLanguageConfig(monacoRef.current) as any)
monacoRef.current.languages.registerInlineCompletionsProvider('remix-circom', inlineCompletionProvider)

monacoRef.current.languages.setMonarchTokensProvider('remix-toml', tomlTokenProvider as any)
monacoRef.current.languages.setLanguageConfiguration('remix-toml', tomlLanguageConfig as any)
Expand All @@ -1035,11 +1030,6 @@ export const EditorUI = (props: EditorUIProps) => {
monacoRef.current.languages.registerInlineCompletionsProvider('remix-solidity', inlineCompletionProvider)
monaco.languages.registerCodeActionProvider('remix-solidity', new RemixCodeActionProvider(props, monaco))

monacoRef.current.languages.setMonarchTokensProvider('remix-vyper', vyperTokenProvider as any)
monacoRef.current.languages.setLanguageConfiguration('remix-vyper', vyperLanguageConfig as any)
monacoRef.current.languages.registerCompletionItemProvider('remix-vyper', new RemixCompletionProvider(props, monaco))
monacoRef.current.languages.registerInlineCompletionsProvider('remix-vyper', inlineCompletionProvider)

loadTypes(monacoRef.current)
}

Expand Down
10 changes: 9 additions & 1 deletion libs/remix-ui/remix-ai/src/lib/components/Default.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react'
import '../remix-ai.css'
import { DefaultModels, GenerationParams, ChatHistory, HandleStreamResponse } from '@remix/remix-ai-core';
import { DefaultModels, GenerationParams, ChatHistory, HandleStreamResponse, parseUserInput } from '@remix/remix-ai-core';
import { ConversationStarter, StreamSend, StreamingAdapterObserver, useAiChatApi } from '@nlux/react';
import { AiChat, useAsStreamAdapter, ChatItem } from '@nlux/react';
import { user, assistantAvatar } from './personas';
Expand Down Expand Up @@ -43,9 +43,17 @@ export const Default = (props) => {
setIS_streaming(true)
GenerationParams.return_stream_response = GenerationParams.stream_result

const userInput = parseUserInput(prompt)
const isGeneratePrompt = userInput[0]
const newprompt = userInput[1]

let response = null
if (await props.plugin.call('remixAI', 'isChatRequestPending')){
response = await props.plugin.call('remixAI', 'ProcessChatRequestBuffer', GenerationParams);
} else if (isGeneratePrompt) {
GenerationParams.return_stream_response = false
GenerationParams.stream_result = false
response = await props.plugin.call('remixAI', 'generate', newprompt, GenerationParams);
} else {
response = await props.plugin.call('remixAI', 'solidity_answer', prompt, GenerationParams);
}
Expand Down
2 changes: 1 addition & 1 deletion libs/remix-ui/tabs/src/lib/remix-ui-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export const TabsUI = (props: TabsUIProps) => {
data-id="remix_ai_switch"
id='remix_ai_switch'
className="btn ai-switch text-ai pl-2 pr-0 py-0"
disabled={!((tabsState.currentExt === 'sol') || (tabsState.currentExt === 'vy') || (tabsState.currentExt === 'circom') )}
disabled={!(tabsState.currentExt === 'sol')}
onClick={async () => {
await props.plugin.call('settings', 'updateCopilotChoice', !ai_switch)
setAI_switch(!ai_switch)
Expand Down