Skip to content

Commit

Permalink
Merge pull request #4175 from ethereum/inlinecompletion
Browse files Browse the repository at this point in the history
provide Inlinecompletion using an huggingface model
  • Loading branch information
yann300 authored Nov 13, 2023
2 parents e79bd99 + 4925ccd commit d8fa6f6
Show file tree
Hide file tree
Showing 15 changed files with 800 additions and 12 deletions.
7 changes: 5 additions & 2 deletions apps/remix-ide/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const remixLib = require('@remix-project/remix-lib')

import {QueryParams} from '@remix-project/remix-lib'
import {SearchPlugin} from './app/tabs/search'
import { CopilotSuggestion } from './app/plugins/copilot/suggestion-service/copilot-suggestion'

const Storage = remixLib.Storage
const RemixDProvider = require('./app/files/remixDProvider')
Expand Down Expand Up @@ -186,8 +187,9 @@ class AppComponent {
// ----------------- ContractFlattener ----------------------------
const contractFlattener = new ContractFlattener()

// ----------------- Open AI --------------------------------------
// ----------------- AI --------------------------------------
const openaigpt = new OpenAIGpt()
const copilotSuggestion = new CopilotSuggestion()

// ----------------- import content service ------------------------
const contentImport = new CompilerImports()
Expand Down Expand Up @@ -310,7 +312,8 @@ class AppComponent {
compilationDetails,
contractFlattener,
solidityScript,
openaigpt
openaigpt,
copilotSuggestion
])

// LAYOUT & SYSTEM VIEWS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {Plugin} from '@remixproject/engine'
import {SuggestionService, SuggestOptions} from './suggestion-service'
const _paq = (window._paq = window._paq || []) //eslint-disable-line

const profile = {
name: 'copilot-suggestion',
displayName: 'copilot-suggestion',
description: 'copilot-suggestion',
methods: ['suggest', 'init', 'uninstall', 'status']
}

export class CopilotSuggestion extends Plugin {
service: SuggestionService
context: string
ready: boolean
constructor() {
super(profile)
this.service = new SuggestionService()
this.context = ''
this.service.events.on('progress', (data) => {
this.emit('loading', data)
})
this.service.events.on('done', (data) => {
})
this.service.events.on('ready', (data) => {
this.ready = true
})
}

status () {
return this.ready
}

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')
console.log('suggest', max_new_tokens, temperature)
const options: SuggestOptions = {
do_sample: false,
top_k: 0,
temperature,
max_new_tokens
}
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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import EventEmitter from 'events'

export type SuggestOptions = { max_new_tokens: number, temperature: number, top_k: number, do_sample: 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
}

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':
console.log(e.data)
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)
console.log(e.data)
// Model file progress: update one of the progress items.
break;

case 'done':
this.events.emit(e.data.status, e.data)
console.log(e.data)
// Model file loaded: remove the progress item from the list.
break;

case 'ready':
this.events.emit(e.data.status, e.data)
console.log(e.data)
// Pipeline ready: the worker is ready to accept messages.
break;

case 'update':
this.events.emit(e.data.status, e.data)
console.log(e.data)
// Generation update: update the output text.
break;

case 'complete':
console.log(e.data)
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
} else {
console.log('no callback for', e.data)
}

// 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,
do_sample: options.do_sample
})
this.responses[timespan] = (error, result) => {
if (error) return reject(error)
resolve(result)
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@

import { pipeline, env } from '@xenova/transformers';

env.allowLocalModels = true;

const instance = null
/**
* 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,
});
}
});
6 changes: 5 additions & 1 deletion apps/remix-ide/src/app/tabs/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@
"settings.port": "PORT",
"settings.projectID": "PROJECT ID",
"settings.projectSecret": "PROJECT SECRET",
"settings.analyticsInRemix": "Analytics in Remix IDE"
"settings.analyticsInRemix": "Analytics in Remix IDE",
"settings.copilot": "Solidity copilot - Alpha",
"settings.copilot.activate": "Load & Activate copilot",
"settings.copilot.max_new_tokens": "Maximum amount of new words to generate",
"settings.copilot.temperature": "Temperature"
}
1 change: 1 addition & 0 deletions apps/remix-ide/src/app/tabs/settings-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ module.exports = class SettingsTab extends ViewPlugin {
updateComponent(state: any) {
return (
<RemixUiSettings
plugin={this}
config={state.config}
editor={state.editor}
_deps={state._deps}
Expand Down
2 changes: 1 addition & 1 deletion libs/remix-ui/app/src/lib/remix-app/interface/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface AppModal {
message: string | JSX.Element
okLabel: string | JSX.Element
okFn?: (value?:any) => void
cancelLabel: string | JSX.Element
cancelLabel?: string | JSX.Element
cancelFn?: () => void,
modalType?: ModalTypes,
defaultValue?: string
Expand Down
Loading

0 comments on commit d8fa6f6

Please sign in to comment.