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
27 changes: 23 additions & 4 deletions apps/remix-ide/src/app/plugins/remixAIPlugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { ViewPlugin } from '@remixproject/engine-web'
import { Plugin } from '@remixproject/engine';
import { RemixAITab, ChatApi } from '@remix-ui/remix-ai'
import React, { useCallback } from 'react';
import { ICompletions, IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, CodeExplainAgent, SecurityAgent } from '@remix/remix-ai-core';
import { ICompletions, IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, AssistantParams, 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,15 @@ export class RemixAIPlugin extends ViewPlugin {
chatRequestBuffer: chatRequestBufferT<any> = null
codeExpAgent: CodeExplainAgent
securityAgent: SecurityAgent
contractor: ContractAgent
assistantProvider: string = 'anthropic'
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 +67,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 +177,22 @@ export class RemixAIPlugin extends ViewPlugin {
return this.securityAgent.getReport(file)
}

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

let result
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
142 changes: 142 additions & 0 deletions libs/remix-ai-core/src/agents/contractAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { parse } from "path";
import { AssistantParams } from "../types/models";

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 = ''
contracts: any = {}

constructor(props) {
this.plugin = props;
AssistantParams.provider = this.plugin.assistantProvider
}

async writeContracts(payload, userPrompt) {
const currentWorkspace = await this.plugin.call('filePanel', 'getCurrentWorkspace')
try {
console.log('Writing contracts', payload)
this.contracts = {}
const parsedFiles = payload
this.generationThreadID = parsedFiles['threadID']
this.workspaceName = parsedFiles['projectName']

this.nAttempts += 1
console.log('Attempts', this.nAttempts)
console.log('Generation attempts', this.generationAttempts)
if (this.nAttempts > this.generationAttempts) {
console.error('Failed to generate the code')
return "Failed to generate secure code on this prompt ```" + userPrompt + "````"
Copy link
Contributor

Choose a reason for hiding this comment

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

this should throw an error instead of returning a string.

}

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', file.fileName)
const newPrompt = `The contract ${file.fileName} does not compile. Here is the error message; ${result.errors}. Try again with the same formatting!`
await this.plugin.generate(newPrompt, AssistantParams, this.generationThreadID); // reuse the same thread
return "Failed to generate secure code on this prompt ```" + userPrompt + "```"
}
}
}

console.log('All source files are compiling')
await this.createWorkspace(this.workspaceName)
await this.plugin.call('filePanel', 'switchToWorkspace', { name: this.workspaceName, isLocalHost: false })
const dirCreated = []
for (const file of parsedFiles.files) {
console.log('Writing file', file.fileName)
const dir = file.fileName.split('/').slice(0, -1).join('/')
console.log('Creating directory', dir)
if (!dirCreated.includes(dir) && dir) {
console.log('Creating new directory', dir)
await this.plugin.call('fileManager', 'mkdir', dir)
dirCreated.push(dir)
}
await this.plugin.call('fileManager', 'writeFile', file.fileName, file.content)
await this.plugin.call('codeFormatter', 'format', file.fileName)
// recompile to have it in the workspace
// await this.plugin.call('solidity' as any, 'setCompilerConfig', compilationParams)
// await this.plugin.call('solidity' as any, 'compile', file.fileName)
}
this.nAttempts = 0
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 )
this.nAttempts = 0
await this.plugin.call('filePanel', 'switchToWorkspace', currentWorkspace)
return "Failed to generate secure code on this prompt ```" + userPrompt + "```"
}

}

async createWorkspace(workspaceName) {
// create random workspace surfixed with timestamp
const timestamp = new Date().getTime()
workspaceName += '-' + timestamp
await this.plugin.call('filePanel', 'createWorkspace', workspaceName, true)
this.workspaceName = workspaceName
}

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

async compilecontracts(fileName, fileContent): Promise<CompilationResult> {
// do not compile tests files
if (fileName.includes('tests/')) return { compilationSucceeded: true, errors: null }

this.contracts[fileName] = { content : fileContent }
console.log('compiling contract', this.contracts)
const result = await this.plugin.call('solidity' as any, 'compileWithParameters', this.contracts, compilationParams)
console.log('compilation result', result)
const data = result.data
let error = false

if (data.errors) {
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) {
// eslint-disable-next-line no-useless-escape
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]
}
5 changes: 3 additions & 2 deletions libs/remix-ai-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IModel, IModelResponse, IModelRequest, InferenceModel, ICompletions,
IParams, ChatEntry, AIRequestType, IRemoteModel,
RemoteBackendOPModel, IStreamResponse } from './types/types'
import { ModelType } from './types/constants'
import { DefaultModels, InsertionParams, CompletionParams, GenerationParams } from './types/models'
import { DefaultModels, InsertionParams, CompletionParams, GenerationParams, AssistantParams} from './types/models'
import { getCompletionPrompt, getInsertionPrompt } from './prompts/completionPrompts'
import { buildSolgptPrompt, PromptBuilder } from './prompts/promptBuilder'
import { RemoteInferencer } from './inferencers/remote/remoteInference'
Expand All @@ -15,7 +15,7 @@ export {
IModel, IModelResponse, IModelRequest, InferenceModel,
ModelType, DefaultModels, ICompletions, IParams, IRemoteModel,
getCompletionPrompt, getInsertionPrompt, IStreamResponse, buildSolgptPrompt,
RemoteInferencer, InsertionParams, CompletionParams, GenerationParams,
RemoteInferencer, InsertionParams, CompletionParams, GenerationParams, AssistantParams,
ChatEntry, AIRequestType, RemoteBackendOPModel, ChatHistory, downloadLatestReleaseExecutable
}

Expand All @@ -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)
}
}
5 changes: 4 additions & 1 deletion libs/remix-ai-core/src/types/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,7 @@ const GenerationParams:IParams = {
terminal_output: false,
}

export { DefaultModels, CompletionParams, InsertionParams, GenerationParams }
const AssistantParams:IParams = GenerationParams
AssistantParams.provider = 'openai'

export { DefaultModels, CompletionParams, InsertionParams, GenerationParams, AssistantParams }
2 changes: 2 additions & 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,8 @@ export interface IParams {
temp?: number;
return_stream_response?: boolean;
terminal_output?: boolean;
threadId?: string;
provider?: 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
Loading