From cf11d52e40c284b344ae15f10b73bb4c1437368a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tetsing?= Date: Thu, 7 Mar 2024 13:07:28 +0100 Subject: [PATCH] initial working tree remote code completion --- apps/remix-ide/src/app.js | 3 - .../suggestion-service/copilot-suggestion.ts | 103 ----------------- .../suggestion-service/suggestion-service.ts | 107 ------------------ .../copilot/suggestion-service/worker.js | 89 --------------- apps/remix-ide/src/app/plugins/solcoderAI.tsx | 19 +++- apps/remix-ide/src/app/tabs/settings-tab.tsx | 4 +- apps/remix-ide/src/remixAppManager.js | 1 - .../lib/providers/inlineCompletionProvider.ts | 23 +++- .../settings/src/lib/remix-ui-settings.tsx | 18 +-- 9 files changed, 36 insertions(+), 331 deletions(-) delete mode 100644 apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts delete mode 100644 apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts delete mode 100644 apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js diff --git a/apps/remix-ide/src/app.js b/apps/remix-ide/src/app.js index 33e0e4d4cb..e526b6dcbd 100644 --- a/apps/remix-ide/src/app.js +++ b/apps/remix-ide/src/app.js @@ -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 diff --git a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts b/apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts deleted file mode 100644 index 61616ea4ce..0000000000 --- a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/copilot-suggestion.ts +++ /dev/null @@ -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() - } -} diff --git a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts b/apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts deleted file mode 100644 index a234b032ba..0000000000 --- a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/suggestion-service.ts +++ /dev/null @@ -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) - } - }) - } -} diff --git a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js b/apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js deleted file mode 100644 index 0623b3712f..0000000000 --- a/apps/remix-ide/src/app/plugins/copilot/suggestion-service/worker.js +++ /dev/null @@ -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, - }); - } -}); \ No newline at end of file diff --git a/apps/remix-ide/src/app/plugins/solcoderAI.tsx b/apps/remix-ide/src/app/plugins/solcoderAI.tsx index e443fa6c05..e1a9f3b0ec 100644 --- a/apps/remix-ide/src/app/plugins/solcoderAI.tsx +++ b/apps/remix-ide/src/app/plugins/solcoderAI.tsx @@ -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,9 +22,11 @@ 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 { @@ -107,7 +117,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 +128,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,9 +149,10 @@ 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") diff --git a/apps/remix-ide/src/app/tabs/settings-tab.tsx b/apps/remix-ide/src/app/tabs/settings-tab.tsx index a09c661e50..1f69e113b6 100644 --- a/apps/remix-ide/src/app/tabs/settings-tab.tsx +++ b/apps/remix-ide/src/app/tabs/settings-tab.tsx @@ -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 (
diff --git a/apps/remix-ide/src/remixAppManager.js b/apps/remix-ide/src/remixAppManager.js index 62692ec90d..25e25b38a5 100644 --- a/apps/remix-ide/src/remixAppManager.js +++ b/apps/remix-ide/src/remixAppManager.js @@ -81,7 +81,6 @@ let requiredModules = [ // services + layout views + system views 'home', 'doc-viewer', 'doc-gen', - 'copilot-suggestion', 'remix-templates' ] diff --git a/libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts b/libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts index 0a1ab7fd81..1a82b5cffc 100644 --- a/libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts +++ b/libs/remix-ui/editor/src/lib/providers/inlineCompletionProvider.ts @@ -1,5 +1,6 @@ /* eslint-disable no-control-regex */ import { EditorUIProps, monacoTypes } from '@remix-ui/editor'; + import axios, {AxiosResponse} from 'axios' import { slice } from 'lodash'; const _paq = (window._paq = window._paq || []) @@ -28,16 +29,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; @@ -64,6 +63,7 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli } } } catch (e) { + console.error(e) return } @@ -84,15 +84,16 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli 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] + console.log('clean', clean) + clean = this.process_completion(clean) const item: monacoTypes.languages.InlineCompletion = { insertText: clean @@ -106,6 +107,16 @@ export class RemixInLineCompletionProvider implements monacoTypes.languages.Inli } } + process_completion(data: any) { + const 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 "" + } + return clean + } + handleItemDidShow?(completions: monacoTypes.languages.InlineCompletions, item: monacoTypes.languages.InlineCompletion, updatedInsertText: string): void { } diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx index f9b903669d..5f3fb74378 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx @@ -134,28 +134,16 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => { 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(() => {