Merge pull request #4607 from ethereum/remote_completion

Remote completion
pull/4677/head
STetsing 8 months ago committed by GitHub
commit d578358005
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      apps/remix-ide/src/app.js
  2. 103
      apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts
  3. 107
      apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts
  4. 89
      apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js
  5. 36
      apps/remix-ide/src/app/plugins/solcoderAI.tsx
  6. 4
      apps/remix-ide/src/app/tabs/settings-tab.tsx
  7. 15
      apps/remix-ide/src/assets/list.json
  8. 1
      apps/remix-ide/src/remixAppManager.js
  9. 31
      libs/remix-ui/editor/src/lib/providers/completionTimer.ts
  10. 42
      libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts
  11. 4
      libs/remix-ui/editor/src/lib/remix-ui-editor.tsx
  12. 20
      libs/remix-ui/settings/src/lib/remix-ui-settings.tsx

@ -68,7 +68,6 @@ const remixLib = require('@remix-project/remix-lib')
import { QueryParams } from '@remix-project/remix-lib'
import { SearchPlugin } from './app/tabs/search'
import { ElectronProvider } from './app/files/electronProvider'
import { CopilotSuggestion } from './app/plugins/copilot/suggestion-service/copilot-suggestion'
const Storage = remixLib.Storage
const RemixDProvider = require('./app/files/remixDProvider')
@ -235,7 +234,6 @@ class AppComponent {
// ----------------- AI --------------------------------------
const openaigpt = new OpenAIGpt()
const solcoder = new SolCoder()
const copilotSuggestion = new CopilotSuggestion()
// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
@ -363,7 +361,6 @@ class AppComponent {
templates,
openaigpt,
solcoder,
copilotSuggestion
])
//---- fs plugin

@ -1,103 +0,0 @@
import {Plugin} from '@remixproject/engine'
import {SuggestionService, SuggestOptions} from './suggestion-service'
import axios, {AxiosResponse} from 'axios'
//@ts-ignore
const _paq = (window._paq = window._paq || []) //eslint-disable-line
const profile = {
name: 'copilot-suggestion',
displayName: 'copilot-suggestion',
description: 'Get Solidity suggestions in editor',
methods: ['suggest', 'init', 'uninstall', 'status', 'isActivate', 'useRemoteService', 'discardRemoteService'],
version: '0.1.0-alpha',
maintainedBy: "Remix"
}
export class CopilotSuggestion extends Plugin {
service: SuggestionService
remoteService: string
context: string
ready: boolean=false
constructor() {
super(profile)
this.context = ''
}
onActivation(): void {
this.service = new SuggestionService()
this.service.events.on('progress', (data) => {
this.emit('loading', data)
})
this.service.events.on('done', (data) => {
})
this.service.events.on('ready', (data) => {
this.emit('ready', data)
this.ready = true
})
}
useRemoteService(service: string) {
this.remoteService = service
}
discardRemoteService() {
this.remoteService = null
}
status () {
return this.ready
}
async isActivate () {
try {
return await this.call('settings', 'get', 'settings/copilot/suggest/activate')
} catch (e) {
console.error(e)
return false
}
}
async suggest(content: string) {
if (!await this.call('settings', 'get', 'settings/copilot/suggest/activate')) return { output: [{ generated_text: ''}]}
const max_new_tokens = await this.call('settings', 'get', 'settings/copilot/suggest/max_new_tokens')
const temperature = await this.call('settings', 'get', 'settings/copilot/suggest/temperature')
const options: SuggestOptions = {
do_sample: true,
top_k: 50,
top_p: 0.92,
stream_result: false,
temperature: temperature || 0,
max_new_tokens: max_new_tokens || 0
}
if (this.remoteService) {
const {data} = await axios.post(this.remoteService, {context: content, max_new_words: options.max_new_tokens, temperature: options.temperature})
const parsedData = JSON.parse(data).trimStart()
return {output: [{generated_text: parsedData}]}
} else {
return this.service.suggest(this.context ? this.context + '\n\n' + content : content, options)
}
}
async loadModeContent() {
let importsContent = ''
const imports = await this.call('codeParser', 'getImports')
for (const imp of imports.modules) {
try {
importsContent += '\n\n' + (await this.call('contentImport', 'resolve', imp)).content
} catch (e) {
console.log(e)
}
}
return importsContent
}
async init() {
return this.service.init()
}
async uninstall() {
this.service.terminate()
}
}

@ -1,107 +0,0 @@
import EventEmitter from 'events'
export type SuggestOptions = {
max_new_tokens: number,
temperature: number,
do_sample:boolean
top_k: number,
top_p:number,
stream_result:boolean
}
export class SuggestionService {
worker: Worker
// eslint-disable-next-line @typescript-eslint/ban-types
responses: { [key: number]: Function }
events: EventEmitter
current: number
constructor() {
this.worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
this.events = new EventEmitter()
this.responses = {}
this.current
}
//todo ask Yann if we should keep the model
terminate(): void {
this.worker.terminate()
this.worker = new Worker(new URL('./worker.js', import.meta.url), {
type: 'module'
});
}
async init() {
const onMessageReceived = (e) => {
switch (e.data.status) {
case 'initiate':
this.events.emit(e.data.status, e.data)
// Model file start load: add a new progress item to the list.
break;
case 'progress':
this.events.emit(e.data.status, e.data)
// Model file progress: update one of the progress items.
break;
case 'done':
this.events.emit(e.data.status, e.data)
// Model file loaded: remove the progress item from the list.
break;
case 'ready':
this.events.emit(e.data.status, e.data)
// Pipeline ready: the worker is ready to accept messages.
break;
case 'update':
this.events.emit(e.data.status, e.data)
// Generation update: update the output text.
break;
case 'complete':
if (this.responses[e.data.id]) {
if (this.current === e.data.id) {
this.responses[e.data.id](null, e.data)
} else {
this.responses[e.data.id]('aborted')
}
delete this.responses[e.data.id]
this.current = null
}
// Generation complete: re-enable the "Generate" button
break;
}
};
// Attach the callback function as an event listener.
this.worker.addEventListener('message', onMessageReceived)
this.worker.postMessage({
cmd: 'init',
model: 'Pipper/finetuned_sol'
})
}
suggest (content: string, options: SuggestOptions) {
return new Promise((resolve, reject) => {
if (this.current) return reject(new Error('already running'))
const timespan = Date.now()
this.current = timespan
this.worker.postMessage({
id: timespan,
cmd: 'suggest',
text: content,
max_new_tokens: options.max_new_tokens,
temperature: options.temperature,
top_k: options.top_k,
})
this.responses[timespan] = (error, result) => {
if (error) return reject(error)
resolve(result)
}
})
}
}

@ -1,89 +0,0 @@
import { pipeline, env } from '@xenova/transformers';
env.allowLocalModels = true;
/**
* This class uses the Singleton pattern to ensure that only one instance of the pipeline is loaded.
*/
class CodeCompletionPipeline {
static task = 'text-generation';
static model = null
static instance = null;
static async getInstance(progress_callback = null) {
if (this.instance === null) {
this.instance = pipeline(this.task, this.model, { progress_callback });
}
return this.instance;
}
}
// Listen for messages from the main thread
self.addEventListener('message', async (event) => {
const {
id, model, text, max_new_tokens, cmd,
// Generation parameters
temperature,
top_k,
do_sample,
} = event.data;
if (cmd === 'init') {
// Retrieve the code-completion pipeline. When called for the first time,
// this will load the pipeline and save it for future use.
CodeCompletionPipeline.model = model
await CodeCompletionPipeline.getInstance(x => {
// We also add a progress callback to the pipeline so that we can
// track model loading.
self.postMessage(x);
});
return
}
if (!CodeCompletionPipeline.instance) {
// Send the output back to the main thread
self.postMessage({
id,
status: 'error',
message: 'model not yet loaded'
});
}
if (cmd === 'suggest') {
// Retrieve the code-completion pipeline. When called for the first time,
// this will load the pipeline and save it for future use.
let generator = await CodeCompletionPipeline.getInstance(x => {
// We also add a progress callback to the pipeline so that we can
// track model loading.
self.postMessage(x);
});
// Actually perform the code-completion
let output = await generator(text, {
max_new_tokens,
temperature,
top_k,
do_sample,
// Allows for partial output
callback_function: x => {
/*self.postMessage({
id,
status: 'update',
output: generator.tokenizer.decode(x[0].output_token_ids, { skip_special_tokens: true })
});
*/
}
});
// Send the output back to the main thread
self.postMessage({
id,
status: 'complete',
output: output,
});
}
});

@ -1,5 +1,13 @@
import { Plugin } from '@remixproject/engine'
import {SuggestOptions} from './copilot/suggestion-service/suggestion-service'
export type SuggestOptions = {
max_new_tokens: number,
temperature: number,
do_sample:boolean
top_k: number,
top_p:number,
stream_result:boolean
}
const _paq = (window._paq = window._paq || [])
@ -14,15 +22,17 @@ const profile = {
export class SolCoder extends Plugin {
api_url: string
completion_url: string
constructor() {
super(profile)
this.api_url = "https://solcoder.remixproject.org"
this.completion_url = "https://completion.remixproject.org"
}
async code_generation(prompt): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', { type: 'typewriterwarning', value: 'Waiting for Solcoder answer...'})
this.call('terminal', 'log', { type: 'typewriterwarning', value: 'Code Generation: Waiting for Solcoder answer...'})
let result
try {
result = await(
@ -32,15 +42,21 @@ export class SolCoder extends Plugin {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({"data":[prompt, "code_generation", false,1000,0.2,0.8,50]}),
body: JSON.stringify({"data":[prompt, "code_completion", "", false,1000,0.9,0.92,50]}),
})
).json()
return "error" in result? result.error : result.data[0]
console.log(result)
if ("error" in result){
this.call('terminal', 'log', { type: 'typewriterwarning', value: result.error })
return result
}
return result.data
} catch (e) {
this.call('terminal', 'log', { type: 'typewritererror', value: `Unable to get a response ${e.message}` })
return
}finally {
this.emit("aiInferingDone")
this.call('terminal', 'log', { type: 'typewriterwarning', value: 'Code Generation: Done'})
}
}
@ -65,6 +81,7 @@ export class SolCoder extends Plugin {
return
}finally {
this.emit("aiInferingDone")
this.call('terminal', 'log', { type: 'typewriterwarning', value: 'Solcoder: Done'})
}
if (result) {
this.call('terminal', 'log', { type: 'typewriterwarning', value: result.data[0]})
@ -77,7 +94,7 @@ export class SolCoder extends Plugin {
async code_explaining(prompt): Promise<any> {
this.emit("aiInfering")
this.call('layout', 'maximizeTerminal')
this.call('terminal', 'log', { type: 'typewriterwarning', value: 'Waiting for Solcoder answer...'})
this.call('terminal', 'log', { type: 'typewriterwarning', value: 'Explain Code: Waiting for Solcoder answer...'})
let result
try {
result = await(
@ -99,6 +116,7 @@ export class SolCoder extends Plugin {
return
}finally {
this.emit("aiInferingDone")
this.call('terminal', 'log', { type: 'typewriterwarning', value: 'Explaining: Done'})
}
}
@ -107,7 +125,7 @@ export class SolCoder extends Plugin {
let result
try {
result = await(
await fetch(this.api_url, {
await fetch(this.completion_url, {
method: 'POST',
headers: {
Accept: 'application/json',
@ -118,7 +136,7 @@ export class SolCoder extends Plugin {
"code_completion",
"", // string in 'comment' Textbox component
false, // boolean in 'stream_result' Checkbox component
200, // number (numeric value between 0 and 2000) in 'max_new_tokens' Slider component
30, // number (numeric value between 0 and 2000) in 'max_new_tokens' Slider component
0.9, // number (numeric value between 0.01 and 1) in 'temperature' Slider component
0.90, // number (numeric value between 0 and 1) in 'top_p' Slider component
50, // number (numeric value between 1 and 200) in 'top_k' Slider component
@ -139,13 +157,15 @@ export class SolCoder extends Plugin {
this.call('terminal', 'log', { type: 'typewriterwarning', value: result.error })
return result
}
return result.data
} catch (e) {
this.call('terminal', 'log', { type: 'typewritererror', value: `Unable to get a response ${e.message}` })
this.call('terminal', 'log', { type: 'typewriterwarning', value: `Unable to get a response ${e.message}` })
return
} finally {
this.emit("aiInferingDone")
}
}
}

@ -61,10 +61,8 @@ module.exports = class SettingsTab extends ViewPlugin {
}
onActivation(): void {
this.once('copilot-suggestion', 'loading', (data) => {
this.call('terminal', 'log', {type: 'typewriterlog', value: `loading Solidity copilot ...` })
})
}
render() {
return (
<div id="settingsTab">

@ -1091,9 +1091,22 @@
"bzzr://c604bdd6384bf73594cd0e5cfbe979048191549ebc88e70996346f3b744c0680",
"dweb:/ipfs/QmW2SQbEhiz3n2qV5iL8WBgzapv6cXjkLStvTMpCZhvr2x"
]
},
{
"path": "soljson-v0.8.25+commit.b61c2a91.js",
"version": "0.8.25",
"build": "commit.b61c2a91",
"longVersion": "0.8.25+commit.b61c2a91",
"keccak256": "0x4639103a26b2f669bd3ecc22b1a1665819f2a2956f917ab91380bd9565dbcd01",
"sha256": "0xf8c9554471ff2db3843167dffb7a503293b5dc728c8305b044ef9fd37d626ca7",
"urls": [
"bzzr://d201e60bd46193b11382988a854132b9e7fb0e1574cc766cb7f9efe8e44a680c",
"dweb:/ipfs/QmdduJxmPXungjJk2FBDw1bdDQ6ucHxYGLXRMBJqMFW7h9"
]
}
],
"releases": {
"0.8.25": "soljson-v0.8.25+commit.b61c2a91.js",
"0.8.24": "soljson-v0.8.24+commit.e11b9ed9.js",
"0.8.23": "soljson-v0.8.23+commit.f704f362.js",
"0.8.22": "soljson-v0.8.22+commit.4fc1097e.js",
@ -1186,5 +1199,5 @@
"0.4.0": "soljson-v0.4.0+commit.acd334c9.js",
"0.3.6": "soljson-v0.3.6+commit.3fc68da5.js"
},
"latestRelease": "0.8.24"
"latestRelease": "0.8.25"
}

@ -81,7 +81,6 @@ let requiredModules = [ // services + layout views + system views
'home',
'doc-viewer',
'doc-gen',
'copilot-suggestion',
'remix-templates'
]

@ -0,0 +1,31 @@
export class CompletionTimer {
private duration: number;
private timerId: NodeJS.Timeout | null = null;
private callback: () => void;
constructor(duration: number, callback: () => void) {
this.duration = duration;
this.callback = callback;
}
start() {
if (this.timerId) {
console.error("Timer is already running.");
return;
}
this.timerId = setTimeout(() => {
this.callback();
this.timerId = null;
}, this.duration);
}
stop() {
if (this.timerId) {
clearTimeout(this.timerId);
this.timerId = null;
} else {
console.error("Timer is not running.");
}
}
}

@ -1,5 +1,7 @@
/* eslint-disable no-control-regex */
import { EditorUIProps, monacoTypes } from '@remix-ui/editor';
import { CompletionTimer } from './completionTimer';
import axios, {AxiosResponse} from 'axios'
import { slice } from 'lodash';
const _paq = (window._paq = window._paq || [])
@ -11,11 +13,14 @@ const result: string = ''
export class RemixInLineCompletionProvider implements monacoTypes.languages.InlineCompletionsProvider {
props: EditorUIProps
monaco: any
completionEnabled: boolean
constructor(props: any, monaco: any) {
this.props = props
this.monaco = monaco
this.completionEnabled = true
}
async provideInlineCompletions(model: monacoTypes.editor.ITextModel, position: monacoTypes.Position, context: monacoTypes.languages.InlineCompletionContext, token: monacoTypes.CancellationToken): Promise<monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>> {
if (context.selectedSuggestionInfo) {
return;
@ -28,16 +33,14 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
endColumn: position.column,
});
if (!word.endsWith(' ') &&
!word.endsWith(';') &&
!word.endsWith('.') &&
!word.endsWith('(')) {
return;
}
try {
const isActivate = await this.props.plugin.call('copilot-suggestion', 'isActivate')
const isActivate = await await this.props.plugin.call('settings', 'get', 'settings/copilot/suggest/activate')
if (!isActivate) return
} catch (err) {
return;
@ -51,8 +54,7 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
// use the code generation model, only take max 1000 word as context
this.props.plugin.call('terminal', 'log', {type: 'typewriterwarning', value: 'Solcoder - generating code for following comment: ' + ask.replace('///', '')})
const data = await this.props.plugin.call('solcoder', 'code_completion', word)
if ("error" in data) return
const data = await this.props.plugin.call('solcoder', 'code_generation', word)
const parsedData = data[0].trimStart() //JSON.parse(data).trimStart()
const item: monacoTypes.languages.InlineCompletion = {
@ -64,6 +66,7 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
}
}
} catch (e) {
console.error(e)
return
}
@ -82,21 +85,32 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
return
}
// abort if the completion is not enabled
if (!this.completionEnabled) {
return
}
let result
try {
result = await this.props.plugin.call('copilot-suggestion', 'suggest', word)
const generatedText = (result as any).output[0].generated_text as string
const output = await this.props.plugin.call('solcoder', 'code_completion', word)
const generatedText = output[0]
let clean = generatedText
if (generatedText.indexOf('@custom:dev-run-script./') !== -1) {
clean = generatedText.replace('@custom:dev-run-script', '@custom:dev-run-script ')
}
clean = clean.replace(word, '').trimStart()
clean = clean.split('\n')[0].startsWith('\n') ? [clean.split('\n')[0], clean.split('\n')[1]].join('\n'): clean.split('\n')[0]
clean = this.process_completion(clean)
const item: monacoTypes.languages.InlineCompletion = {
insertText: clean
};
// handle the completion timer by locking suggestions request for 2 seconds
this.completionEnabled = false
const handleCompletionTimer = new CompletionTimer(2000, () => { this.completionEnabled = true });
handleCompletionTimer.start()
return {
items: [item],
enableForwardStability: true
@ -106,6 +120,18 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli
}
}
process_completion(data: any) {
let clean = data.split('\n')[0].startsWith('\n') ? [data.split('\n')[0], data.split('\n')[1]].join('\n'): data.split('\n')[0]
// if clean starts with a comment, remove it
if (clean.startsWith('//') || clean.startsWith('/*') || clean.startsWith('*') || clean.startsWith('*/')){
return ""
}
// remove comment inline
clean = clean.split('//')[0].trimEnd()
return clean
}
handleItemDidShow?(completions: monacoTypes.languages.InlineCompletions<monacoTypes.languages.InlineCompletion>, item: monacoTypes.languages.InlineCompletion, updatedInsertText: string): void {
}

@ -743,7 +743,7 @@ export const EditorUI = (props: EditorUIProps) => {
let solgptExplainFunctionAction
const executeSolgptExplainFunctionAction = {
id: 'explainFunction',
id: 'solExplainFunction',
label: intl.formatMessage({id: 'editor.explainFunctionSol'}),
contextMenuOrder: 1, // choose the order
contextMenuGroupId: 'sol-gtp', // create a new grouping
@ -836,6 +836,8 @@ export const EditorUI = (props: EditorUIProps) => {
gptGenerateDocumentationAction = editor.addAction(executeGptGenerateDocumentationAction)
executegptExplainFunctionAction.label = intl.formatMessage({id: 'editor.explainFunction'}, {name: functionImpl.name})
gptExplainFunctionAction = editor.addAction(executegptExplainFunctionAction)
executeSolgptExplainFunctionAction.label = intl.formatMessage({id: 'editor.explainFunctionSol'})
solgptExplainFunctionAction = editor.addAction(executeSolgptExplainFunctionAction)
}else{
executeSolgptExplainFunctionAction.label = intl.formatMessage({id: 'editor.explainFunctionSol'})
solgptExplainFunctionAction = editor.addAction(executeSolgptExplainFunctionAction)

@ -131,31 +131,18 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
}
const onchangeCopilotActivate = () => {
console.log("onchangeCopilotActivate ", props.useCopilot)
if (!props.useCopilot) {
copilotActivate(props.config, props.useCopilot, dispatch)
props.plugin.call('copilot-suggestion', 'uninstall')
props.plugin.call('terminal', 'log', {type: 'typewriterlog', value: `Solidity copilot deactivated` })
props.plugin.call('terminal', 'log', {type: 'typewriterlog', value: `Solidity copilot deactivated!` })
return
}
props.plugin.on('copilot-suggestion', 'ready', (data) => {
props.plugin.call('terminal', 'log', {type: 'typewriterlog', value: `loading Solidity copilot: 100% done.` })
})
const startCopilot = async () => {
await props.plugin.call('copilot-suggestion', 'init')
if (await props.plugin.call('copilot-suggestion', 'status')) {
copilotActivate(props.config, true, dispatch)
}
copilotActivate(props.config, true, dispatch)
props.plugin.call('terminal', 'log', {type: 'typewriterlog', value: `Solidity copilot activated!` })
}
startCopilot()
if (props.plugin.call('copilot-suggestion', 'status')) {
copilotActivate(props.config, true, dispatch)
}else {
startCopilot()
}
}
useEffect(() => {
@ -438,7 +425,6 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
copilotTemperatureValue = 0.9
}
//if (isCopilotActivated) props.plugin.call('copilot-suggestion', 'init')
const copilotSettings = () => (
<div className="border-top">
<div className="card-body pt-3 pb-2">

Loading…
Cancel
Save