Skip to content

Commit 08c3d44

Browse files
authored
Merge pull request #6164 from ethereum/ai_local_imports
Ai local imports
2 parents 8e6f159 + 6e37d93 commit 08c3d44

File tree

11 files changed

+353
-140
lines changed

11 files changed

+353
-140
lines changed

apps/remix-ide-e2e/src/tests/ai_panel.test.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const tests = {
130130
timeout: 120000
131131
})
132132
.waitForElementPresent('*[data-id="remix-ai-assistant-ready"]')
133-
.assistantGenerate('a simple ERC20 contract', 'mistralai')
133+
.assistantGenerate('a simple ERC20 contract', 'anthropic')
134134
.waitForElementVisible({
135135
locateStrategy: 'xpath',
136136
selector: '//div[contains(@class,"chat-bubble") and contains(.,"New workspace created:")]',
@@ -231,39 +231,6 @@ const tests = {
231231
selector: "//*[@data-id='remix-ai-streaming' and @data-streaming='false']"
232232
})
233233
},
234-
'Generate new workspaces code with all AI assistant providers #group1': function (browser: NightwatchBrowser) {
235-
browser
236-
.assistantClearChat()
237-
.waitForCompilerLoaded()
238-
.clickLaunchIcon('remixaiassistant')
239-
240-
.waitForElementPresent('*[data-id="remix-ai-assistant-ready"]')
241-
242-
.assistantGenerate('a simple ERC20 contract', 'openai')
243-
.waitForElementVisible({
244-
locateStrategy: 'xpath',
245-
selector: '//div[contains(@class,"chat-bubble") and contains(.,"New workspace created:")]',
246-
timeout: 60000
247-
})
248-
.waitForElementPresent({
249-
locateStrategy: 'xpath',
250-
selector: "//*[@data-id='remix-ai-streaming' and @data-streaming='false']"
251-
})
252-
.assistantClearChat()
253-
254-
.clickLaunchIcon('remixaiassistant')
255-
256-
.assistantGenerate('a simple ERC20 contract', 'anthropic')
257-
.waitForElementVisible({
258-
locateStrategy: 'xpath',
259-
selector: '//div[contains(@class,"chat-bubble") and contains(.,"New workspace created:")]',
260-
timeout: 60000
261-
})
262-
.waitForElementPresent({
263-
locateStrategy: 'xpath',
264-
selector: "//*[@data-id='remix-ai-streaming' and @data-streaming='false']"
265-
})
266-
},
267234
"Should close the AI assistant #group1": function (browser: NightwatchBrowser) {
268235
browser
269236
.click('*[data-id="movePluginToLeft"]')

apps/remix-ide/src/app/plugins/remixAIPlugin.tsx

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,16 @@ export class RemixAIPlugin extends Plugin {
154154

155155
async error_explaining(prompt: string, params: IParams=GenerationParams): Promise<any> {
156156
let result
157+
let localFilesImports = ""
158+
159+
// Get local imports from the workspace restrict to 5 most relevant files
160+
const relevantFiles = this.workspaceAgent.getRelevantLocalFiles(prompt, 5);
161+
162+
for (const file in relevantFiles) {
163+
localFilesImports += `\n\nFileName: ${file}\n\n${relevantFiles[file]}`
164+
}
165+
localFilesImports = localFilesImports + "\n End of local files imports.\n\n"
166+
prompt = localFilesImports ? `Using the following local imports: ${localFilesImports}\n\n` + prompt : prompt
157167
if (this.isOnDesktop && !this.useRemoteInferencer) {
158168
result = await this.call(this.remixDesktopPluginName, 'error_explaining', prompt)
159169
} else {
@@ -183,17 +193,20 @@ export class RemixAIPlugin extends Plugin {
183193
* Generates a new remix IDE workspace based on the provided user prompt, optionally using Retrieval-Augmented Generation (RAG) context.
184194
* - If `useRag` is `true`, the function fetches additional context from a RAG API and prepends it to the user prompt.
185195
*/
186-
async generate(userPrompt: string, params: IParams=AssistantParams, newThreadID:string="", useRag:boolean=false): Promise<any> {
196+
async generate(prompt: string, params: IParams=AssistantParams, newThreadID:string="", useRag:boolean=false, statusCallback?: (status: string) => Promise<void>): Promise<any> {
187197
params.stream_result = false // enforce no stream result
188198
params.threadId = newThreadID
189-
params.provider = this.assistantProvider
199+
params.provider = 'anthropic' // enforce all generation to be only on anthropic
200+
useRag = false
190201
_paq.push(['trackEvent', 'ai', 'remixAI', 'GenerateNewAIWorkspace'])
202+
let userPrompt = ''
191203

192204
if (useRag) {
205+
statusCallback?.('Fetching RAG context...')
193206
try {
194207
let ragContext = ""
195208
const options = { headers: { 'Content-Type': 'application/json', } }
196-
const response = await axios.post(endpointUrls.rag, { query: userPrompt, endpoint:"query" }, options)
209+
const response = await axios.post(endpointUrls.rag, { query: prompt, endpoint:"query" }, options)
197210
if (response.data) {
198211
ragContext = response.data.response
199212
userPrompt = "Using the following context: ```\n\n" + JSON.stringify(ragContext) + "```\n\n" + userPrompt
@@ -203,32 +216,37 @@ export class RemixAIPlugin extends Plugin {
203216
} catch (error) {
204217
console.log('RAG context error:', error)
205218
}
219+
} else {
220+
userPrompt = prompt
206221
}
207222
// Evaluate if this function requires any context
208223
// console.log('Generating code for prompt:', userPrompt, 'and threadID:', newThreadID)
209-
let result
210-
if (this.isOnDesktop && !this.useRemoteInferencer) {
211-
result = await this.call(this.remixDesktopPluginName, 'generate', userPrompt, params)
212-
} else {
213-
result = await this.remoteInferencer.generate(userPrompt, params)
214-
}
224+
await statusCallback?.('Generating new workspace with AI...\nThis might take some minutes. Please be patient!')
225+
const result = await this.remoteInferencer.generate(userPrompt, params)
226+
227+
await statusCallback?.('Creating contracts and files...')
228+
const genResult = await this.contractor.writeContracts(result, userPrompt, statusCallback)
215229

216-
const genResult = await this.contractor.writeContracts(result, userPrompt)
217230
if (genResult.includes('No payload')) return genResult
218231
await this.call('menuicons', 'select', 'filePanel')
219232
return genResult
220233
}
221234

222235
/**
223-
* Performs any user action on the entire curren workspace or updates the workspace based on a user prompt, optionally using Retrieval-Augmented Generation (RAG) for additional context.
236+
* Performs any user action on the entire curren workspace or updates the workspace based on a user prompt,
237+
* optionally using Retrieval-Augmented Generation (RAG) for additional context.
224238
*
225239
*/
226-
async generateWorkspace (userPrompt: string, params: IParams=AssistantParams, newThreadID:string="", useRag:boolean=false): Promise<any> {
240+
async generateWorkspace (userPrompt: string, params: IParams=AssistantParams, newThreadID:string="", useRag:boolean=false, statusCallback?: (status: string) => Promise<void>): Promise<any> {
227241
params.stream_result = false // enforce no stream result
228242
params.threadId = newThreadID
229243
params.provider = this.assistantProvider
244+
useRag = false
230245
_paq.push(['trackEvent', 'ai', 'remixAI', 'WorkspaceAgentEdit'])
246+
247+
await statusCallback?.('Performing workspace request...')
231248
if (useRag) {
249+
await statusCallback?.('Fetching RAG context...')
232250
try {
233251
let ragContext = ""
234252
const options = { headers: { 'Content-Type': 'application/json', } }
@@ -244,25 +262,20 @@ export class RemixAIPlugin extends Plugin {
244262
console.log('RAG context error:', error)
245263
}
246264
}
265+
await statusCallback?.('Loading workspace context...')
247266
const files = !this.workspaceAgent.ctxFiles ? await this.workspaceAgent.getCurrentWorkspaceFiles() : this.workspaceAgent.ctxFiles
248267
userPrompt = "Using the following workspace context: ```\n" + files + "```\n\n" + userPrompt
249268

250-
let result
251-
if (this.isOnDesktop && !this.useRemoteInferencer) {
252-
result = await this.call(this.remixDesktopPluginName, 'generateWorkspace', userPrompt, params)
253-
} else {
254-
result = await this.remoteInferencer.generateWorkspace(userPrompt, params)
255-
}
256-
return (result !== undefined) ? this.workspaceAgent.writeGenerationResults(result) : "### No Changes applied!"
269+
await statusCallback?.('Generating workspace updates with AI...')
270+
const result = await this.remoteInferencer.generateWorkspace(userPrompt, params)
271+
272+
await statusCallback?.('Applying changes to workspace...')
273+
return (result !== undefined) ? this.workspaceAgent.writeGenerationResults(result, statusCallback) : "### No Changes applied!"
257274
}
258275

259-
async fixWorspaceErrors(continueGeneration=false): Promise<any> {
276+
async fixWorspaceErrors(): Promise<any> {
260277
try {
261-
if (continueGeneration) {
262-
return this.contractor.continueCompilation()
263-
} else {
264-
return this.contractor.fixWorkspaceCompilationErrors(this.workspaceAgent)
265-
}
278+
return this.contractor.fixWorkspaceCompilationErrors(this.workspaceAgent)
266279
} catch (error) {
267280
}
268281
}

libs/remix-ai-core/src/agents/completionAgent.ts

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import lunr from 'lunr';
2+
import { extractImportsFromFile } from "../helpers/localImportsExtractor";
23

34
interface Document {
45
id: number;
@@ -80,23 +81,10 @@ export class CodeCompletionAgent {
8081

8182
async getLocalImports(fileContent: string, currentFile: string) {
8283
try {
83-
const lines = fileContent.split('\n');
84-
const imports = [];
85-
86-
for (const line of lines) {
87-
const trimmedLine = line.trim();
88-
if (trimmedLine.startsWith('import')) {
89-
const parts = trimmedLine.split(' ');
90-
if (parts.length >= 2) {
91-
const importPath = parts[1].replace(/['";]/g, '');
92-
imports.push(importPath);
93-
}
94-
}
95-
}
96-
// Only local imports are those files that are in the workspace
97-
const localImports = this.Documents.length >0 ? imports.filter((imp) => {return this.Documents.find((doc) => doc.filename === imp);}) : [];
98-
99-
return localImports;
84+
// Use extractImportsFromFile to get all imports from the current file
85+
const imports = extractImportsFromFile(fileContent);
86+
const localImports = imports.filter(imp => imp.isLocal);
87+
return localImports.map(imp => imp.importPath);
10088
} catch (error) {
10189
return [];
10290
}

libs/remix-ai-core/src/agents/contractAgent.ts

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AssistantParams } from "../types/models";
22
import { workspaceAgent } from "./workspaceAgent";
33
import { CompilationResult } from "../types/types";
4-
import { compilecontracts } from "../helpers/compile";
4+
import { compilecontracts, compilationParams } from "../helpers/compile";
55

66
const COMPILATION_WARNING_MESSAGE = '⚠️**Warning**: The compilation failed. Please check the compilation errors in the Solidity compiler plugin. Enter `/continue` or `/c` if you want Remix AI to try again until a compilable solution is generated?'
77

@@ -12,11 +12,9 @@ export class ContractAgent {
1212
generationThreadID: string= ''
1313
workspaceName: string = ''
1414
contracts: any = {}
15-
performCompile: boolean = false
16-
overrideWorkspace: boolean = false
15+
mainPrompt: string = ''
1716
static instance
1817
oldPayload: any = undefined
19-
mainPrompt: string
2018

2119
private constructor(props) {
2220
this.plugin = props;
@@ -33,23 +31,29 @@ export class ContractAgent {
3331
* Write the result of the generation to the workspace. Compiles the contracts one time and creates a new workspace.
3432
* @param payload - The payload containing the generated files
3533
* @param userPrompt - The user prompt used to generate the files
34+
* @param statusCallback - Optional callback for status updates in chat window
3635
*/
37-
async writeContracts(payload, userPrompt) {
36+
async writeContracts(payload, userPrompt, statusCallback?: (status: string) => Promise<void>) {
37+
await statusCallback?.('Getting current workspace info...')
3838
const currentWorkspace = await this.plugin.call('filePanel', 'getCurrentWorkspace')
3939

4040
const writeAIResults = async (parsedResults) => {
4141
if (this.plugin.isOnDesktop) {
42+
await statusCallback?.('Preparing files for desktop...')
4243
const files = parsedResults.files.reduce((acc, file) => {
4344
acc[file.fileName] = file.content
4445
return acc
4546
}, {})
47+
await statusCallback?.('Opening in new window...')
4648
await this.plugin.call('electronTemplates', 'loadTemplateInNewWindow', files)
4749
//return "Feature not only available in the browser version of Remix IDE. Please use the browser version to generate secure code."
4850
return "## New workspace created! \nNavigate to the new window!"
4951
}
5052

53+
await statusCallback?.('Creating new workspace...')
5154
await this.createWorkspace(this.workspaceName)
52-
if (!this.overrideWorkspace) await this.plugin.call('filePanel', 'switchToWorkspace', { name: this.workspaceName, isLocalHost: false })
55+
56+
await statusCallback?.('Writing files to workspace...')
5357
const dirCreated = []
5458
for (const file of parsedResults.files) {
5559
const dir = file.fileName.split('/').slice(0, -1).join('/')
@@ -66,44 +70,70 @@ export class ContractAgent {
6670

6771
try {
6872
if (payload === undefined) {
73+
await statusCallback?.('No payload received, retrying...')
6974
this.nAttempts += 1
7075
if (this.nAttempts > this.generationAttempts) {
71-
this.performCompile = false
7276
if (this.oldPayload) {
7377
return await writeAIResults(this.oldPayload)
7478
}
7579
return "Max attempts reached! Please try again with a different prompt."
7680
}
77-
return "No payload, try again while considering changing the assistant provider with the command `/setAssistant <openai|anthorpic|mistralai>`"
81+
return "No payload, try again while considering changing the assistant provider to one of these choices `<openai|anthropic|mistralai>`"
7882
}
83+
84+
await statusCallback?.('Processing generated files...')
7985
this.contracts = {}
8086
const parsedFiles = payload
8187
this.oldPayload = payload
8288
this.generationThreadID = parsedFiles['threadID']
8389
this.workspaceName = parsedFiles['projectName']
8490

8591
this.nAttempts += 1
86-
if (this.nAttempts === 1) this.mainPrompt = userPrompt
92+
if (this.nAttempts === 1) this.mainPrompt=userPrompt
8793

8894
if (this.nAttempts > this.generationAttempts) {
8995
return await writeAIResults(parsedFiles)
9096
}
9197

98+
await statusCallback?.('Processing Solidity contracts...')
99+
const genContrats = []
92100
for (const file of parsedFiles.files) {
93101
if (file.fileName.endsWith('.sol')) {
94102
this.contracts[file.fileName] = { content: file.content }
103+
genContrats.push({ fileName: file.fileName, content: file.content })
95104
}
96105
}
97106

107+
await statusCallback?.('Compiling contracts...')
98108
const result:CompilationResult = await compilecontracts(this.contracts, this.plugin)
99-
if (!result.compilationSucceeded && this.performCompile) {
109+
if (!result.compilationSucceeded) {
110+
await statusCallback?.('Compilation failed, fixing errors...')
100111
// console.log('Compilation failed, trying again recursively ...')
101-
const newPrompt = `Payload:\n${JSON.stringify(result.errfiles)}}\n\nWhile considering this compilation error: Here is the error message\n. Try this again:${this.mainPrompt}\n `
102-
return await this.plugin.generate(newPrompt, AssistantParams, this.generationThreadID); // reuse the same thread
112+
const generatedContracts = genContrats.map(contract =>
113+
`File: ${contract.fileName}\n${contract.content}`
114+
).join('\n\n');
115+
116+
// Format error files properly according to the schema
117+
const formattedErrorFiles = Object.entries(result.errfiles).map(([fileName, fileData]: [string, any]) => {
118+
const errors = fileData.errors.map((err: any) =>
119+
`Error at ${err.errorStart}-${err.errorEnd}: ${err.errorMessage}`
120+
).join('\n ');
121+
return `File: ${fileName}\n ${errors}`;
122+
}).join('\n\n');
123+
124+
const newPrompt = `
125+
Compilation parameters:\n${JSON.stringify(compilationParams)}\n\n
126+
Compilation errors:\n${formattedErrorFiles}\n\n
127+
Generated contracts:\n${generatedContracts}\n\nConsider other possible solutions and retry this main prompt again: \n${this.mainPrompt}\n `
128+
129+
await statusCallback?.('Regenerating workspace with error fixes...')
130+
return await this.plugin.generate(newPrompt, AssistantParams, this.generationThreadID, false, statusCallback); // reuse the same thread
103131
}
104132

133+
await statusCallback?.('Finalizing workspace creation...')
105134
return result.compilationSucceeded ? await writeAIResults(parsedFiles) : await writeAIResults(parsedFiles) + "\n\n" + COMPILATION_WARNING_MESSAGE
106135
} catch (error) {
136+
await statusCallback?.('Error occurred, cleaning up...')
107137
this.deleteWorkspace(this.workspaceName )
108138
this.nAttempts = 0
109139
await this.plugin.call('filePanel', 'switchToWorkspace', currentWorkspace)
@@ -148,21 +178,4 @@ export class ContractAgent {
148178
}
149179
}
150180

151-
async continueCompilation(){
152-
try {
153-
if (this.oldPayload === undefined) {
154-
return "No payload, try again while considering changing the assistant provider with the command `/setAssistant <openai|anthorpic|mistralai>`"
155-
}
156-
157-
this.performCompile = true
158-
this.overrideWorkspace = true
159-
return await this.writeContracts(this.oldPayload, this.mainPrompt)
160-
} catch (error) {
161-
return "Error during continue compilation. Please try again."
162-
} finally {
163-
this.performCompile = false
164-
this.overrideWorkspace = false
165-
}
166-
}
167-
168181
}

0 commit comments

Comments
 (0)