diff --git a/modules/openvino_code/README.md b/modules/openvino_code/README.md index 84a286ee0..6558cf465 100644 --- a/modules/openvino_code/README.md +++ b/modules/openvino_code/README.md @@ -1,32 +1,16 @@ # OpenVINO Code - VSCode extension for AI code completion with OpenVINO™ -VSCode extension for helping developers writing code with AI code assistant. OpenVINO Code is working with Large Language Model for Code (Code LLM) deployed on local or remote server. +VSCode extension for helping developers writing code with AI code assistant. +OpenVINO Code is working with Large Language Model for Code (Code LLM) deployed on local server +or remote server using [Remote Explorer](https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-explorer). -## Installing Extension +OpenVINO Code provides the following features: +- Inline Code Completion +- Summarization via docstring -VSCode extension can be installed from built `*.vsix` file: - -1. Open `Extensions` side bar in VSCode. -2. Click on the menu icon (three dots menu icon aka "meatballs" icon) in the top right corner of Extensions side panel. -3. Select "Instal from VSIX..." option and select extension file. - -For instructions on how to build extension `vsix` file please refer to the [Build Extension](#build-extension) section. - -## Extension Configuration - -To work with extension you should configure endpoint to server with Code LLM where requests will be sent: - -1. Open extension settings. -2. Fill `Server URL` parameter with server endpoint URL. - -For instructions on how to start server locally please refer to the [server README.md](./server/README.md). - -Also in extension settings you can configure special tokens. ## Working with Extension -TDB - 1. Create a new python file 2. Try typing `def main():` 3. Press shortcut buttons (TBD) for code completion @@ -35,41 +19,5 @@ TDB You can see input to and output from the code generation API: -1. Open VSCode `OUTPUT` panel -2. Select extension output source from the dropdown menu - -## Developing - -> **Prerequisite:** You should have `Node.js` installed (v16 and above). - -#### Install dependencies - -To install dependencies run the following command from the project root directory: - -``` -npm install -``` - -#### Run Extension from Source & Debugging - -Open `Run and Debug` side bar in VSCode and click `Launch Extension` (or press `F5`). - -#### Build Extension - -To build extension and generate `*.vsix` file for further installation in VSCode, run the following command: - -``` -npm run vsce:package -``` - -#### Linting - -To perform linting with `ESLint`, execute the following command: - -``` -npm run lint -``` - -#### Testing - -TBD +1. Open VSCode Side Panel +2. Click `Show Server Log` or `Show Extension Log` diff --git a/modules/openvino_code/openvino-code-completion-0.0.1.vsix b/modules/openvino_code/openvino-code-completion-0.0.1.vsix deleted file mode 100644 index 409074aa1..000000000 Binary files a/modules/openvino_code/openvino-code-completion-0.0.1.vsix and /dev/null differ diff --git a/modules/openvino_code/openvino-code-completion-0.0.2.vsix b/modules/openvino_code/openvino-code-completion-0.0.2.vsix new file mode 100644 index 000000000..cb7d8c923 Binary files /dev/null and b/modules/openvino_code/openvino-code-completion-0.0.2.vsix differ diff --git a/modules/openvino_code/package-lock.json b/modules/openvino_code/package-lock.json index 4c30cdd09..e07168a2c 100644 --- a/modules/openvino_code/package-lock.json +++ b/modules/openvino_code/package-lock.json @@ -1,13 +1,13 @@ { "name": "openvino-code-completion", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openvino-code-completion", - "version": "0.0.1", - "license": "License at https://github.com/openvinotoolkit/openvino_contrib/tree/master/modules/openvino_code", + "version": "0.0.2", + "license": "https://github.com/openvinotoolkit/openvino_contrib/blob/master/LICENSE", "workspaces": [ "side-panel-ui" ], diff --git a/modules/openvino_code/package.json b/modules/openvino_code/package.json index 6abccb37e..95888d54f 100644 --- a/modules/openvino_code/package.json +++ b/modules/openvino_code/package.json @@ -1,13 +1,13 @@ { "publisher": "OpenVINO", "name": "openvino-code-completion", - "version": "0.0.1", + "version": "0.0.2", "displayName": "OpenVINO Code Completion", "description": "VSCode extension for AI code completion with OpenVINO", "icon": "media/logo.png", "author": "", "contributors": [], - "license": "License at https://github.com/openvinotoolkit/openvino_contrib/tree/master/modules/openvino_code", + "license": "https://github.com/openvinotoolkit/openvino_contrib/blob/master/LICENSE", "homepage": "https://docs.openvino.ai/", "repository": { "type": "git", @@ -51,6 +51,7 @@ "lint": "eslint . --max-warnings 0", "lint:fix": "eslint . --fix", "lint:side-panel": "npm run lint -w side-panel-ui", + "lint:all": "npm run lint && npm run lint --workspaces", "test": "node ./out/test/runTest.js", "vsce:package": "vsce package", "vsce:publish": "vsce publish", @@ -164,6 +165,16 @@ { "title": "OpenVINO Code", "properties": { + "openvinoCode.model": { + "order": 0, + "type": "string", + "default": "codet5p-220m-py", + "enum": [ + "codet5p-220m-py", + "decicoder-1b-openvino-int8" + ], + "description": "Which model to use for code generation." + }, "openvinoCode.serverUrl": { "order": 1, "type": "string", @@ -238,6 +249,7 @@ "description": "(Optional) Stop token." }, "openvinoCode.quoteStyle": { + "order": 11, "type": "string", "default": "\"\"\"", "enum": [ @@ -247,6 +259,7 @@ "description": "Style of quote used with generate docstring command" }, "openvinoCode.docstringFormat": { + "order": 12, "type": "string", "default": "google_summary_only", "enum": [ diff --git a/modules/openvino_code/server/main.py b/modules/openvino_code/server/main.py index a1e9de1e3..3d86194e6 100644 --- a/modules/openvino_code/server/main.py +++ b/modules/openvino_code/server/main.py @@ -1,11 +1,13 @@ -import uvicorn +from src.utils import get_parser, setup_logger -from src.app import app, get_generator_dummy -from src.generators import get_generator_dependency -from src.utils import get_logger, get_parser +# Logger should be set up before other imports to propagate logging config to other packages +setup_logger() -logger = get_logger(__name__) +import uvicorn # noqa: E402 + +from src.app import app, get_generator_dummy # noqa: E402 +from src.generators import get_generator_dependency # noqa: E402 def main(): diff --git a/modules/openvino_code/server/pyproject.toml b/modules/openvino_code/server/pyproject.toml index c2bf3b68e..54d7a726b 100644 --- a/modules/openvino_code/server/pyproject.toml +++ b/modules/openvino_code/server/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ 'torch @ https://download.pytorch.org/whl/cpu-cxx11-abi/torch-2.0.1%2Bcpu.cxx11.abi-cp311-cp311-linux_x86_64.whl ; sys_platform=="linux" and python_version == "3.11"', 'torch ; sys_platform != "linux"', 'openvino==2023.1.0.dev20230811', - 'optimum-intel[openvino]==1.10.1', + 'optimum-intel[openvino]==1.11.0', ] [project.optional-dependencies] diff --git a/modules/openvino_code/server/src/app.py b/modules/openvino_code/server/src/app.py index e824eac7c..2fb061a1a 100644 --- a/modules/openvino_code/server/src/app.py +++ b/modules/openvino_code/server/src/app.py @@ -3,7 +3,7 @@ from fastapi import Depends, FastAPI from fastapi.responses import RedirectResponse, StreamingResponse -from pydantic import BaseModel +from pydantic import BaseModel, Field from src.generators import GeneratorFunctor from src.utils import get_logger @@ -26,6 +26,35 @@ class GenerationRequest(BaseModel): parameters: GenerationParameters +class GenerationDocStringRequest(BaseModel): + inputs: str = Field( + ..., + description="Function or Class body", + example=( + "def fibonacci(n):\n if n == 0:\n return 0\n elif n == 1:\n" + " return 1\n else:\n return fibonacci(n-1) + fibonacci(n-2)" + ), + ) + template: str = Field( + ..., + description=( + "Doc string template with tab stops in format ${tab_stop_number:value[type | int | str | description]}" + ), + example=( + ' """\n ${1:}\n\n Parameters\n ----------\n n : ${2:int}\n' + " ${3:[description]}\n\n Returns\n -------\n ${4:[type]}\n" + ' ${5:[description]}\n """' + ), + ) + format: str = Field( + ..., + description="Doc string format passed from extension settings [google | numpy | sphinx | dockblockr | ...]", + example="numpy", + ) + definition: str = Field("", description="Function signature", example="def fibonacci(n):") + parameters: GenerationParameters + + class GenerationResponse(BaseModel): generated_text: str @@ -40,7 +69,7 @@ def get_generator_dummy(): @app.on_event("startup") async def startup_event(): # This print is a anchor for vs code extension to track that server is started - SERVER_STARTED_STDOUT_ANCHOR = 'OpenVINO Code Server started' + SERVER_STARTED_STDOUT_ANCHOR = "OpenVINO Code Server started" logger.info(SERVER_STARTED_STDOUT_ANCHOR) @@ -59,7 +88,7 @@ async def generate( request: GenerationRequest, generator: GeneratorFunctor = Depends(get_generator_dummy), ) -> Dict[str, Union[int, str]]: - logger.info(request) + logger.info(f"Request:\n{request}") start = perf_counter() generated_text: str = generator(request.inputs, request.parameters.model_dump()) @@ -70,7 +99,7 @@ async def generate( else: logger.info(f"Elapsed: {elapsed:.3f}s") - logger.info(f"Response: {generated_text}") + logger.info(f"Response:\n{generated_text}") return {"generated_text": generated_text} @@ -85,16 +114,15 @@ async def generate_stream( @app.post("/api/summarize", status_code=200, response_model=GenerationResponse) async def summarize( - request: GenerationRequest, + request: GenerationDocStringRequest, generator: GeneratorFunctor = Depends(get_generator_dummy), ): logger.info(request) - generation_params = request.parameters.model_dump() - generation_params["repetition_penalty"] = 1.15 - start = perf_counter() - generated_text: str = generator.summarize(request.inputs, generation_params) + generated_text: str = generator.summarize( + request.inputs, request.template, request.definition, request.format, request.parameters.model_dump() + ) stop = perf_counter() if (elapsed := stop - start) > 1.5: @@ -108,12 +136,12 @@ async def summarize( @app.post("/api/summarize_stream", status_code=200) async def summarize_stream( - request: GenerationRequest, + request: GenerationDocStringRequest, generator: GeneratorFunctor = Depends(get_generator_dummy), ) -> StreamingResponse: logger.info(request) - - generation_params = request.parameters.model_dump() - generation_params["repetition_penalty"] = 1.15 - - return StreamingResponse(generator.summarize_stream(request.inputs, generation_params)) + return StreamingResponse( + generator.summarize_stream( + request.inputs, request.template, request.definition, request.format, request.parameters.model_dump() + ) + ) diff --git a/modules/openvino_code/server/src/generators.py b/modules/openvino_code/server/src/generators.py index b15c9066b..ee1ef6b2c 100644 --- a/modules/openvino_code/server/src/generators.py +++ b/modules/openvino_code/server/src/generators.py @@ -1,7 +1,9 @@ +import re from functools import lru_cache +from io import StringIO from pathlib import Path from threading import Thread -from typing import Any, Callable, Container, Dict, List, Optional, Type, Union +from typing import Any, Callable, Container, Dict, Generator, List, Optional, Type, Union import torch from huggingface_hub.utils import EntryNotFoundError @@ -9,7 +11,6 @@ from transformers import ( AutoConfig, AutoTokenizer, - AutoModelForCausalLM, GenerationConfig, StoppingCriteria, StoppingCriteriaList, @@ -26,13 +27,8 @@ model_dir = Path("models") model_dir.mkdir(exist_ok=True) -INSTRUCTION_WITH_INPUT = ( - "Below is an instruction that describes a task, paired with an input that provides further context. " - "Write a response that appropriately completes the request.\n\n" - "### Instruction:\n{instruction}\n\n### Input:\n{input}\n\n### Response:\n" -) -SUMMARIZE_INSTRUCTION = "The function description in numpy style" -SUMMARIZE_STOP_TOKENS = ("\n", ".\n") +SUMMARIZE_INSTRUCTION = "{function}\n\n# The function with {style} style docstring\n\n{signature}\n" +SUMMARIZE_STOP_TOKENS = ("\r\n", "\n") def get_model_class(checkpoint: Union[str, Path]) -> Type[OVModel]: @@ -53,7 +49,9 @@ def get_model(checkpoint: str, device: str = "CPU") -> OVModel: else: model_class = get_model_class(checkpoint) try: - model = model_class.from_pretrained(checkpoint, ov_config=ov_config, compile=False, device=device, trust_remote_code=True) + model = model_class.from_pretrained( + checkpoint, ov_config=ov_config, compile=False, device=device, trust_remote_code=True + ) except EntryNotFoundError: model = model_class.from_pretrained( checkpoint, ov_config=ov_config, export=True, compile=False, device=device, trust_remote_code=True @@ -70,6 +68,12 @@ def __call__(self, input_text: str, parameters: Dict[str, Any]) -> str: async def generate_stream(self, input_text: str, parameters: Dict[str, Any]): raise NotImplementedError + def summarize(self, input_text: str, template: str, signature: str, style: str, parameters: Dict[str, Any]): + raise NotImplementedError + + def summarize_stream(self, input_text: str, template: str, signature: str, style: str, parameters: Dict[str, Any]): + raise NotImplementedError + class OVGenerator(GeneratorFunctor): def __init__( @@ -82,7 +86,6 @@ def __init__( ) -> None: self.device = device self.model = get_model(checkpoint, device) - # self.model = AutoModelForCausalLM.from_pretrained(checkpoint, trust_remote_code=True) self.tokenizer: AutoTokenizer = AutoTokenizer.from_pretrained( tokenizer_checkpoint if tokenizer_checkpoint is not None else checkpoint, @@ -97,13 +100,15 @@ def __init__( self.assistant_model_config = {} if assistant_checkpoint is not None: self.assistant_model = get_model(assistant_checkpoint, device) - # self.assistant_model = AutoModelForSeq2SeqLM.from_pretrained(assistant_checkpoint) self.assistant_model_config["assistant_model"] = self.assistant_model self.summarize_stopping_criteria = None if summarize_stop_tokens: - stop_tokens_ids = self.tokenizer(summarize_stop_tokens).input_ids - self.summarize_stopping_criteria = StoppingCriteriaList([StopOnTokens(stop_tokens_ids)]) + stop_tokens = [] + for token_id in self.tokenizer.vocab.values(): + if any(stop_word in self.tokenizer.decode(token_id) for stop_word in summarize_stop_tokens): + stop_tokens.append(token_id) + self.summarize_stopping_criteria = StoppingCriteriaList([StopOnTokens(stop_tokens)]) def __call__( self, input_text: str, parameters: Dict[str, Any], stopping_criteria: Optional[StoppingCriteriaList] = None @@ -135,20 +140,88 @@ async def generate_stream( for token in streamer: yield token - def summarization_input(self, input_text: str) -> str: - return INSTRUCTION_WITH_INPUT.format( - instruction=SUMMARIZE_INSTRUCTION, - input=input_text, - ) + def generate_between( + self, + input_parts: List[str], + parameters: Dict[str, Any], + stopping_criteria: Optional[StoppingCriteriaList] = None, + ) -> str: + config = GenerationConfig.from_dict({**self.generation_config.to_dict(), **parameters}) + + prompt = torch.tensor([[]], dtype=torch.int64) + buffer = StringIO() + for text_input in input_parts[:-1]: + buffer.write(text_input) + + tokenized_input = self.tokenizer.encode(text_input, return_tensors="pt") + prompt = torch.concat((prompt, tokenized_input), dim=1) + prev_len = prompt.shape[-1] + + prompt = self.model.generate( + prompt, generation_config=config, stopping_criteria=stopping_criteria, **self.assistant_model_config + )[ + :, :-1 + ] # skip the last token - stop token + + decoded = self.tokenizer.decode(prompt[0, prev_len:], skip_special_tokens=True) + buffer.write(decoded.lstrip(" ")) # hack to delete leadding spaces if there are any - def summarize(self, input_text: str, parameters: Dict[str, Any]) -> str: - return self( - self.summarization_input(input_text), parameters, stopping_criteria=self.summarize_stopping_criteria + buffer.write(input_parts[-1]) + return buffer.getvalue() + + async def generate_between_stream( + self, + input_parts: List[str], + parameters: Dict[str, Any], + stopping_criteria: Optional[StoppingCriteriaList] = None, + ) -> Generator[str, None, None]: + config = GenerationConfig.from_dict({**self.generation_config.to_dict(), **parameters}) + + prompt = self.tokenizer.encode(input_parts[0], return_tensors="pt") + for text_input in input_parts[1:-1]: + yield text_input + + tokenized_input = self.tokenizer.encode(text_input, return_tensors="pt") + prompt = torch.concat((prompt, tokenized_input), dim=1) + prev_len = prompt.shape[-1] + + prompt = self.model.generate( + prompt, generation_config=config, stopping_criteria=stopping_criteria, **self.assistant_model_config + )[ + :, :-1 + ] # skip the last token - stop token + + decoded = self.tokenizer.decode(prompt[0, prev_len:], skip_special_tokens=True) + yield decoded.lstrip(" ") # hack to delete leadding spaces if there are any + + yield input_parts[-1] + + @staticmethod + def summarization_input(function: str, signature: str, style: str) -> str: + return SUMMARIZE_INSTRUCTION.format( + function=function, + style=style, + signature=signature, ) - async def summarize_stream(self, input_text: str, parameters: Dict[str, Any]): - async for token in self.generate_stream( - self.summarization_input(input_text), parameters, stopping_criteria=self.summarize_stopping_criteria + def summarize(self, input_text: str, template: str, signature: str, style: str, parameters: Dict[str, Any]) -> str: + prompt = self.summarization_input(input_text, signature, style) + splited_template = re.split(r"\$\{.*\}", template) + splited_template[0] = prompt + splited_template[0] + + return self.generate_between(splited_template, parameters, stopping_criteria=self.summarize_stopping_criteria)[ + len(prompt) : + ] + + async def summarize_stream( + self, input_text: str, template: str, signature: str, style: str, parameters: Dict[str, Any] + ): + prompt = self.summarization_input(input_text, signature, style) + splited_template = re.split(r"\$\{.*\}", template) + splited_template = [prompt] + splited_template + + async for token in self.generate_between_stream( + splited_template, parameters, stopping_criteria=self.summarize_stopping_criteria ): yield token @@ -169,11 +242,8 @@ def inner() -> GeneratorFunctor: class StopOnTokens(StoppingCriteria): - def __init__(self, token_ids: List[List[int]]) -> None: - self.token_ids = [torch.tensor(ids, requires_grad=False) for ids in token_ids] + def __init__(self, token_ids: List[int]) -> None: + self.token_ids = torch.tensor(token_ids, requires_grad=False) def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool: - return any( - torch.all(input_ids[0][-len(stopping_token_ids) :] == stopping_token_ids) - for stopping_token_ids in self.token_ids - ) + return torch.any(torch.eq(input_ids[0, -1], self.token_ids)).item() diff --git a/modules/openvino_code/server/src/utils.py b/modules/openvino_code/server/src/utils.py index f7e9694ac..c119b4a91 100644 --- a/modules/openvino_code/server/src/utils.py +++ b/modules/openvino_code/server/src/utils.py @@ -17,26 +17,44 @@ def get_parser() -> argparse.ArgumentParser: return parser -default_formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s") +def setup_logger(): + logging.setLoggerClass(ServerLogger) + _set_uvicorn_log_format(ServerLogger.default_formatter._fmt) def get_logger( name: str, level: int = logging.DEBUG, - formatter: logging.Formatter = default_formatter, ) -> logging.Logger: logger = logging.getLogger(name) logger.setLevel(level) + return logger - stdout_handler = logging.StreamHandler(sys.stdout) - stdout_handler.addFilter(lambda record: record.levelno <= logging.WARNING) - stdout_handler.setFormatter(formatter) - stderr_handler = logging.StreamHandler(sys.stderr) - stderr_handler.setLevel(logging.ERROR) - stderr_handler.setFormatter(formatter) +class ServerLogger(logging.Logger): + _server_log_prefix = "[OpenVINO Code Server Log]" - logger.addHandler(stdout_handler) - logger.addHandler(stderr_handler) + default_formatter = logging.Formatter(f"{_server_log_prefix} %(asctime)s %(levelname)s %(message)s") - return logger + def __init__(self, name): + super(ServerLogger, self).__init__(name) + + self.propagate = False + + stdout_handler = logging.StreamHandler(sys.stdout) + stdout_handler.addFilter(lambda record: record.levelno <= logging.WARNING) + stdout_handler.setFormatter(self.default_formatter) + + stderr_handler = logging.StreamHandler(sys.stderr) + stderr_handler.setLevel(logging.ERROR) + stderr_handler.setFormatter(self.default_formatter) + + self.addHandler(stdout_handler) + self.addHandler(stderr_handler) + + +def _set_uvicorn_log_format(format: str): + from uvicorn.config import LOGGING_CONFIG + + LOGGING_CONFIG["formatters"]["access"]["fmt"] = format + LOGGING_CONFIG["formatters"]["default"]["fmt"] = format diff --git a/modules/openvino_code/shared/extension-state.ts b/modules/openvino_code/shared/extension-state.ts index 8ff106473..545e3a0ea 100644 --- a/modules/openvino_code/shared/extension-state.ts +++ b/modules/openvino_code/shared/extension-state.ts @@ -1,4 +1,5 @@ import { ExtensionConfiguration } from '../src/configuration'; +import { Features } from './features'; import { ServerState } from './server-state'; export enum ConnectionStatus { @@ -7,10 +8,16 @@ export enum ConnectionStatus { PENDING = 'PENDING', } +interface IStateFeatures { + get supportedList(): Features[]; + get isSummarizationSupported(): boolean; +} + export interface IExtensionState { isLoading: boolean; connectionStatus: ConnectionStatus; server: ServerState; get isServerAvailable(): boolean; get config(): ExtensionConfiguration; + features: IStateFeatures; } diff --git a/modules/openvino_code/shared/features.ts b/modules/openvino_code/shared/features.ts new file mode 100644 index 000000000..d376256b2 --- /dev/null +++ b/modules/openvino_code/shared/features.ts @@ -0,0 +1,4 @@ +export enum Features { + CODE_COMPLETION = 'Code Completion', + SUMMARIZATION = 'Summarization', +} diff --git a/modules/openvino_code/shared/model.ts b/modules/openvino_code/shared/model.ts new file mode 100644 index 000000000..660cb383b --- /dev/null +++ b/modules/openvino_code/shared/model.ts @@ -0,0 +1,21 @@ +import { Features } from './features'; + +enum ModelId { + CODE_T5_220M = 'Salesforce/codet5p-220m-py', + DECICODER_1B_OPENVINO_INT8 = 'chgk13/decicoder-1b-openvino-int8', +} + +export enum ModelName { + CODE_T5_220M = 'codet5p-220m-py', + DECICODER_1B_OPENVINO_INT8 = 'decicoder-1b-openvino-int8', +} + +export const MODEL_NAME_TO_ID_MAP: Record = { + [ModelName.CODE_T5_220M]: ModelId.CODE_T5_220M, + [ModelName.DECICODER_1B_OPENVINO_INT8]: ModelId.DECICODER_1B_OPENVINO_INT8, +}; + +export const MODEL_SUPPORTED_FEATURES: Record = { + [ModelName.CODE_T5_220M]: [Features.CODE_COMPLETION], + [ModelName.DECICODER_1B_OPENVINO_INT8]: [Features.CODE_COMPLETION, Features.SUMMARIZATION], +}; diff --git a/modules/openvino_code/shared/side-panel-message.ts b/modules/openvino_code/shared/side-panel-message.ts index dccf8920e..0c0720d6d 100644 --- a/modules/openvino_code/shared/side-panel-message.ts +++ b/modules/openvino_code/shared/side-panel-message.ts @@ -9,6 +9,7 @@ export enum SidePanelMessageTypes { CHECK_CONNECTION_CLICK = `${sidePanelMessagePrefix}.checkConnectionClick`, GENERATE_COMPLETION_CLICK = `${sidePanelMessagePrefix}.generateCompletionClick`, SETTINGS_CLICK = `${sidePanelMessagePrefix}.settingsClick`, + MODEL_CHANGE = `${sidePanelMessagePrefix}.modelChange`, } export interface ISidePanelMessage

{ diff --git a/modules/openvino_code/side-panel-ui/src/App.tsx b/modules/openvino_code/side-panel-ui/src/App.tsx index b548cbd7a..52c32bddb 100644 --- a/modules/openvino_code/side-panel-ui/src/App.tsx +++ b/modules/openvino_code/side-panel-ui/src/App.tsx @@ -16,9 +16,11 @@ initApp(); function App(): JSX.Element { const [state] = useExtensionState(); + // TODO Check if state is defined to prevent nested optional chaining const isServerStopped = state?.server.status === ServerStatus.STOPPED; const isServerStarted = state?.server.status === ServerStatus.STARTED; + const isSummarizationSupported = state?.features.isSummarizationSupported; return ( <> @@ -28,7 +30,7 @@ function App(): JSX.Element { {isServerStarted && ( <> - + {isSummarizationSupported && } )} diff --git a/modules/openvino_code/side-panel-ui/src/components/sections/OverviewSection/OverviewSection.css b/modules/openvino_code/side-panel-ui/src/components/sections/OverviewSection/OverviewSection.css new file mode 100644 index 000000000..544e82c48 --- /dev/null +++ b/modules/openvino_code/side-panel-ui/src/components/sections/OverviewSection/OverviewSection.css @@ -0,0 +1,3 @@ +.checkbox { + margin-top: 0.4rem +} diff --git a/modules/openvino_code/side-panel-ui/src/components/sections/OverviewSection/OverviewSection.tsx b/modules/openvino_code/side-panel-ui/src/components/sections/OverviewSection/OverviewSection.tsx index c1200b790..cf699f713 100644 --- a/modules/openvino_code/side-panel-ui/src/components/sections/OverviewSection/OverviewSection.tsx +++ b/modules/openvino_code/side-panel-ui/src/components/sections/OverviewSection/OverviewSection.tsx @@ -1,3 +1,5 @@ +import './OverviewSection.css'; + export function OverviewSection(): JSX.Element { return (

@@ -7,7 +9,7 @@ export function OverviewSection(): JSX.Element {
  • Inline Code Completion
  • Summarization via docstring
  • - To use OpenVINO Code please start the server + To use OpenVINO Code please start the server.
    ); diff --git a/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ModelSelect/ModelSelect.tsx b/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ModelSelect/ModelSelect.tsx new file mode 100644 index 000000000..c77b74d1f --- /dev/null +++ b/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ModelSelect/ModelSelect.tsx @@ -0,0 +1,39 @@ +import { ModelName } from '@shared/model'; +import { Select, SelectOptionProps } from '../../../shared/Select/Select'; +import { ServerStatus } from '@shared/server-state'; +import { Features } from '@shared/features'; + +const options: SelectOptionProps[] = [ + { value: ModelName.CODE_T5_220M }, + { value: ModelName.DECICODER_1B_OPENVINO_INT8 }, +]; + +interface ModelSelectProps { + disabled: boolean; + selectedModelName: ModelName; + onChange: (modelName: ModelName) => void; + supportedFeatures: Features[]; + serverStatus: ServerStatus; +} + +export const ModelSelect = ({ + disabled, + selectedModelName, + onChange, + supportedFeatures, + serverStatus, +}: ModelSelectProps): JSX.Element => { + const isServerStopped = serverStatus === ServerStatus.STOPPED; + return ( + <> + + {isServerStopped && Supported Featues: {supportedFeatures.join(', ')}} + + ); +}; diff --git a/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ServerSection.css b/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ServerSection.css index 660332b00..0459349c8 100644 --- a/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ServerSection.css +++ b/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ServerSection.css @@ -4,3 +4,7 @@ gap: 1rem; margin: 1rem 0; } + +.server-section .select-container { + margin: 0.8rem 0; +} diff --git a/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ServerSection.tsx b/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ServerSection.tsx index 384b9c6cb..68ed0ea33 100644 --- a/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ServerSection.tsx +++ b/modules/openvino_code/side-panel-ui/src/components/sections/ServerSection/ServerSection.tsx @@ -5,6 +5,8 @@ import { ServerStatus as ServerStatusEnum } from '@shared/server-state'; import { StartingStages } from './StartingStages/StartingStages'; import { ServerStatus } from './ServerStatus/ServerStatus'; import './ServerSection.css'; +import { ModelSelect } from './ModelSelect/ModelSelect'; +import { ModelName } from '@shared/model'; interface ServerSectionProps { state: IExtensionState | null; @@ -35,14 +37,34 @@ export function ServerSection({ state }: ServerSectionProps): JSX.Element { }); }; - const isServerStopped = state?.server.status === ServerStatusEnum.STOPPED; - const isServerStarting = state?.server.status === ServerStatusEnum.STARTING; + const handleModelChange = (modelName: ModelName) => { + vscode.postMessage({ + type: SidePanelMessageTypes.MODEL_CHANGE, + payload: { + modelName, + }, + }); + }; + + if (!state) { + return <>Extension state is not available; + } + + const isServerStopped = state.server.status === ServerStatusEnum.STOPPED; + const isServerStarting = state.server.status === ServerStatusEnum.STARTING; return (

    OpenVINO Code Server

    - {state && } - {isServerStarting && state && } + + + {isServerStarting && }
    {isServerStopped && } {!isServerStopped && ( diff --git a/modules/openvino_code/side-panel-ui/src/components/shared/Checkbox/Checkbox.css b/modules/openvino_code/side-panel-ui/src/components/shared/Checkbox/Checkbox.css new file mode 100644 index 000000000..962b94c56 --- /dev/null +++ b/modules/openvino_code/side-panel-ui/src/components/shared/Checkbox/Checkbox.css @@ -0,0 +1,28 @@ +.checkbox { + display: flex; + padding: 0.2rem 0; +} + +.vscode-checkbox { + height: 18px; + width: 18px; + border: 1px solid transparent; + border-radius: 3px; + margin-right: 9px; + margin-left: 0; + padding: 0; + + background-color: var(--vscode-settings-checkboxBackground); + color: var(--vscode-settings-checkboxForeground); + border-color: var(--vscode-settings-checkboxBorder); + + cursor: pointer; + overflow: hidden; + box-sizing: border-box; +} + +.checkbox-label { + margin-top: -1px; + color: var(--vscode-foreground); + opacity: .9; +} diff --git a/modules/openvino_code/side-panel-ui/src/components/shared/Checkbox/Checkbox.tsx b/modules/openvino_code/side-panel-ui/src/components/shared/Checkbox/Checkbox.tsx new file mode 100644 index 000000000..8daad1be2 --- /dev/null +++ b/modules/openvino_code/side-panel-ui/src/components/shared/Checkbox/Checkbox.tsx @@ -0,0 +1,21 @@ +import { ReactNode } from 'react'; +import './Checkbox.css'; + +interface CheckboxProps { + checked?: boolean; + children: ReactNode; + onChange: (isChecked: boolean) => void; +} + +export const Checkbox = ({ checked, children, onChange }: CheckboxProps): JSX.Element => { + const classNames = ['vscode-checkbox', 'codicon']; + if (checked) { + classNames.push('codicon-check'); + } + return ( +
    +
    onChange(!checked)}>
    + {children} +
    + ); +}; diff --git a/modules/openvino_code/side-panel-ui/src/components/shared/Select/Select.css b/modules/openvino_code/side-panel-ui/src/components/shared/Select/Select.css new file mode 100644 index 000000000..ccb5383b2 --- /dev/null +++ b/modules/openvino_code/side-panel-ui/src/components/shared/Select/Select.css @@ -0,0 +1,30 @@ +.select-container { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.select-container .select { + background-color: var(--vscode-settings-dropdownBackground); + color: var(--vscode-settings-dropdownForeground); + border-color: var(--vscode-settings-dropdownBorder); + + flex-grow: 1; + font: inherit; + height: 26px; + padding: 2px; + + cursor: pointer; + border-radius: 2px; + + box-sizing: border-box; +} + +.select-container[aria-disabled="true"] { + cursor: not-allowed; + opacity: 0.6; +} + +.select-container[aria-disabled="true"] * { + cursor: not-allowed; +} diff --git a/modules/openvino_code/side-panel-ui/src/components/shared/Select/Select.tsx b/modules/openvino_code/side-panel-ui/src/components/shared/Select/Select.tsx new file mode 100644 index 000000000..695e364e2 --- /dev/null +++ b/modules/openvino_code/side-panel-ui/src/components/shared/Select/Select.tsx @@ -0,0 +1,42 @@ +import './Select.css'; + +export interface SelectOptionProps { + value: V; + label?: L; +} + +const SelectOption = ({ value, label }: SelectOptionProps): JSX.Element => ( + +); + +interface SelectProps { + label: string; + options: SelectOptionProps[]; + selectedValue: V; + disabled?: boolean; + onChange?: (value: V) => void; +} + +export const Select = ({ + label, + options, + disabled, + onChange, + selectedValue, +}: SelectProps): JSX.Element => { + return ( +
    + + +
    + ); +}; diff --git a/modules/openvino_code/src/configuration.ts b/modules/openvino_code/src/configuration.ts index 4d2967e18..ea7a77a6f 100644 --- a/modules/openvino_code/src/configuration.ts +++ b/modules/openvino_code/src/configuration.ts @@ -1,9 +1,11 @@ +import { ModelName } from '@shared/model'; import { WorkspaceConfiguration } from 'vscode'; /** * Extension configuration should match `contributes.configuration` properties in package.json */ export type CustomConfiguration = { + model: ModelName; serverUrl: string; serverRequestTimeout: number; fillInTheMiddleMode: boolean; diff --git a/modules/openvino_code/src/docstring/completion-item-provider.ts b/modules/openvino_code/src/docstring/completion-item-provider.ts index a8050bad7..c00999156 100644 --- a/modules/openvino_code/src/docstring/completion-item-provider.ts +++ b/modules/openvino_code/src/docstring/completion-item-provider.ts @@ -1,11 +1,12 @@ -import { CompletionItemProvider, CompletionItem, TextDocument, Position, CompletionItemKind, Range } from "vscode"; -import { validDocstringPrefix, docstringIsClosed } from "./parse"; -import { extensionState } from "../state"; -import { COMMANDS } from "../constants"; +import { CompletionItemProvider, CompletionItem, TextDocument, Position, CompletionItemKind, Range } from 'vscode'; +import { validDocstringPrefix, docstringIsClosed } from './parse'; +import { extensionState } from '../state'; +import { COMMANDS } from '../constants'; export const completionItemProvider: CompletionItemProvider = { provideCompletionItems: (document: TextDocument, position: Position) => { - if (validEnterActivation(document, position)) { + const { isSummarizationSupported } = extensionState.state.features; + if (isSummarizationSupported && validEnterActivation(document, position)) { return [new AutoDocstringCompletionItem(document, position)]; } return; @@ -30,16 +31,16 @@ function validEnterActivation(document: TextDocument, position: Position): boole */ class AutoDocstringCompletionItem extends CompletionItem { constructor(_: TextDocument, position: Position) { - super("Generate Docstring", CompletionItemKind.Snippet); - this.insertText = ""; + super('Generate Docstring', CompletionItemKind.Snippet); + this.insertText = ''; this.filterText = getQuoteStyle(); - this.sortText = "\0"; + this.sortText = '\0'; this.range = new Range(new Position(position.line, 0), position); this.command = { command: COMMANDS.GENERATE_DOC_STRING, - title: "Generate Docstring", + title: 'Generate Docstring', }; } } diff --git a/modules/openvino_code/src/docstring/docstring-template/template-data.ts b/modules/openvino_code/src/docstring/docstring-template/template-data.ts index 8c67fe021..ad3f33c5f 100644 --- a/modules/openvino_code/src/docstring/docstring-template/template-data.ts +++ b/modules/openvino_code/src/docstring/docstring-template/template-data.ts @@ -1,8 +1,8 @@ -import { Argument, Decorator, DocstringParts, Exception, KeywordArgument, Returns, Yields } from "./docstring-parts"; +import { Argument, Decorator, DocstringParts, Exception, KeywordArgument, Returns, Yields } from './docstring-parts'; export class TemplateData { public name: string; - public summary?: string; + public summary: string; public decorators: Decorator[]; public args: Argument[]; public kwargs: KeywordArgument[]; @@ -14,6 +14,7 @@ export class TemplateData { private includeName: boolean; private includeExtendedSummary: boolean; type: string | undefined = undefined; + private static DEFAULT_TYPE_PLACEHOLDER = '[type]'; constructor( docstringParts: DocstringParts, @@ -22,7 +23,7 @@ export class TemplateData { includeExtendedSummary: boolean ) { this.name = docstringParts.name; - this.summary = docstringParts.summary; + this.summary = docstringParts.summary || ''; this.decorators = docstringParts.decorators; this.args = docstringParts.args; this.kwargs = docstringParts.kwargs; @@ -38,37 +39,41 @@ export class TemplateData { this.removeTypes(); } - this.addDefaultTypePlaceholders("[type]"); + this.addDefaultTypePlaceholders(TemplateData.DEFAULT_TYPE_PLACEHOLDER); } public placeholder() { return (text: string, render: (text: string) => string) => { - return "${@@@:" + render(text) + "}"; + return '${@@@:' + render(text) + '}'; }; } public summaryPlaceholder(): string { if (this.includeName) { - return this.name + " ${@@@:" + this.summary + "}"; + return this.name + ' ${@@@:' + this.summary + '}'; } - return "${@@@:" + this.summary + "}"; + return '${@@@:' + this.summary + '}'; } public extendedSummaryPlaceholder(): string { if (this.includeExtendedSummary) { - return "${@@@:[extended_summary]}"; + return '${@@@:[extended_summary]}'; } - return ""; + return ''; } public typePlaceholder(): string { - return "${@@@:" + this.type + "}"; + // skip tabstop for guessed types + if (this.type === TemplateData.DEFAULT_TYPE_PLACEHOLDER) { + return '${@@@:' + this.type + '}'; + } + return `${this.type}`; } public descriptionPlaceholder(): string { - return "${@@@:[description]}"; + return '${@@@:[description]}'; } public argsExist(): boolean { diff --git a/modules/openvino_code/src/docstring/generate-command.ts b/modules/openvino_code/src/docstring/generate-command.ts index c27453fa9..b7e612b3e 100644 --- a/modules/openvino_code/src/docstring/generate-command.ts +++ b/modules/openvino_code/src/docstring/generate-command.ts @@ -1,9 +1,11 @@ -import { window } from "vscode"; -import { AutoDocstring } from "./generate-docstring"; +import { window } from 'vscode'; +import { AutoDocstring } from './generate-docstring'; +import { extensionState } from '../state'; export function generateCommandHandler() { + const { isSummarizationSupported } = extensionState.state.features; const editor = window.activeTextEditor; - if (!editor) { + if (!isSummarizationSupported || !editor) { return; } diff --git a/modules/openvino_code/src/docstring/generate-docstring.ts b/modules/openvino_code/src/docstring/generate-docstring.ts index fa7c08fd6..5771d5911 100644 --- a/modules/openvino_code/src/docstring/generate-docstring.ts +++ b/modules/openvino_code/src/docstring/generate-docstring.ts @@ -19,16 +19,15 @@ export class AutoDocstring { const position = this.editor.selection.active; const document = this.editor.document.getText(); - // fixme: docstring parts doesn't includes decorators - const docstringParts = parse(document, position.line); - const indentation = getDocstringIndentation( - document, - position.line, - getDefaultIndentation(this.editor.options.insertSpaces as boolean, this.editor.options.tabSize as number) + const defaultIndentation = getDefaultIndentation( + this.editor.options.insertSpaces as boolean, + this.editor.options.tabSize as number ); + const { docstringParts, definition } = parse(document, position.line, defaultIndentation.length); + const indentation = getDocstringIndentation(document, position.line, defaultIndentation); return this._insertGenerationPlaceholder(indentation) - .then(() => this._insertDocstring(docstringParts, indentation, position)) + .then(() => this._generateDocstring(docstringParts, definition, indentation, position)) .then( () => extensionState.set('isLoading', false), () => extensionState.set('isLoading', false) @@ -46,15 +45,26 @@ export class AutoDocstring { return this.editor.insertSnippet(generationPlaceholderSnippet, insertPosition); } - private _insertDocstring(docstringParts: DocstringParts, indentation: string, position: vs.Position) { - const removeGenerationPlaceholder = () => - this.editor.edit((builder) => { - builder.delete(this.editor.document.lineAt(position).range); - }); + private _removeGenerationPlaceholder(position: vs.Position) { + return this.editor.edit((builder) => { + builder.delete(this.editor.document.lineAt(position).range); + }); + } + + private _generateDocstring( + docstringParts: DocstringParts, + definition: string, + indentation: string, + position: vs.Position + ) { + const template = this.generateTemplate(docstringParts, indentation); return backendService .generateSummarization({ - inputs: docstringParts.code.join(' '), + inputs: docstringParts.code.join('\n'), + template: template, + definition: definition, + format: extensionState.config.docstringFormat, parameters: { temperature: extensionState.config.temperature, top_k: extensionState.config.topK, @@ -64,29 +74,19 @@ export class AutoDocstring { }, }) .then((response) => { - docstringParts.summary = response?.generated_text || ''; - const docstringSnippet = this.generateDocstringSnippet(docstringParts, indentation); + const docstringSnippet = new vs.SnippetString(response?.generated_text); - return removeGenerationPlaceholder().then(() => + return this._removeGenerationPlaceholder(position).then(() => this.editor.insertSnippet(docstringSnippet, position.with(position.line, 0)) ); }); } - private generateDocstringSnippet(docstringParts: DocstringParts, indentation: string): vs.SnippetString { - const config = extensionState.config; - - const docstringFactory = new DocstringFactory( - getTemplate(extensionState.config.docstringFormat), - config.quoteStyle, - true, - false, - false, - true - ); + private generateTemplate(docstringParts: DocstringParts, indentation: string): string { + const { quoteStyle, docstringFormat } = extensionState.config; - const docstring = docstringFactory.generateDocstring(docstringParts, indentation); + const docstringFactory = new DocstringFactory(getTemplate(docstringFormat), quoteStyle, true, false, false, true); - return new vs.SnippetString(docstring); + return docstringFactory.generateDocstring(docstringParts, indentation); } } diff --git a/modules/openvino_code/src/docstring/index.ts b/modules/openvino_code/src/docstring/index.ts index 59dc15231..44e740863 100644 --- a/modules/openvino_code/src/docstring/index.ts +++ b/modules/openvino_code/src/docstring/index.ts @@ -7,6 +7,7 @@ import { completionItemProvider } from './completion-item-provider'; class DocString implements IExtensionComponent { activate(context: ExtensionContext): void { const commandDisposable = commands.registerCommand(COMMANDS.GENERATE_DOC_STRING, generateCommandHandler); + const providerDisposable = languages.registerCompletionItemProvider( 'python', completionItemProvider, @@ -14,6 +15,7 @@ class DocString implements IExtensionComponent { "'", '#' ); + context.subscriptions.push(commandDisposable, providerDisposable); } deactivate(): void {} diff --git a/modules/openvino_code/src/docstring/parse/get-body.ts b/modules/openvino_code/src/docstring/parse/get-body.ts index f3ad89a72..ef265dcaa 100644 --- a/modules/openvino_code/src/docstring/parse/get-body.ts +++ b/modules/openvino_code/src/docstring/parse/get-body.ts @@ -1,11 +1,11 @@ -import { blankLine, indentationOf, preprocessLines } from "./utilities"; +import { blankLine, filterComments, indentationOf } from './utilities'; -export function getBody(document: string, linePosition: number): string[] { - const lines = document.split("\n"); +export function getBody(document: string, linePosition: number, defaultIndentation: number): string[] { + const lines = document.split('\n'); const body = []; let currentLineNum = linePosition; - const originalIndentation = getBodyBaseIndentation(lines, linePosition); + const bodyBaseIndentation = getBodyBaseIndentation(lines, linePosition); while (currentLineNum < lines.length) { const line = lines[currentLineNum]; @@ -15,15 +15,15 @@ export function getBody(document: string, linePosition: number): string[] { continue; } - if (indentationOf(line) < originalIndentation) { + if (indentationOf(line) < bodyBaseIndentation) { break; } - body.push(line); + body.push(line.slice(bodyBaseIndentation - defaultIndentation)); currentLineNum++; } - return preprocessLines(body); + return filterComments(body); } function getBodyBaseIndentation(lines: string[], linePosition: number): number { diff --git a/modules/openvino_code/src/docstring/parse/guess-type.ts b/modules/openvino_code/src/docstring/parse/guess-type.ts index d995474f8..29cb5922c 100644 --- a/modules/openvino_code/src/docstring/parse/guess-type.ts +++ b/modules/openvino_code/src/docstring/parse/guess-type.ts @@ -21,7 +21,7 @@ function getTypeFromTyping(parameter: string): string | undefined { return undefined; } - return typeHint[1].replace(/['"]+/g, "").trim(); + return typeHint[1].replace(/['"]+/g, '').trim(); } function guessTypeFromDefaultValue(parameter: string): string | undefined { @@ -35,63 +35,63 @@ function guessTypeFromDefaultValue(parameter: string): string | undefined { const defaultValue = defaultValueMatch[1]; if (isInteger(defaultValue)) { - return "int"; + return 'int'; } if (isFloat(defaultValue)) { - return "float"; + return 'float'; } if (isHexadecimal(defaultValue)) { - return "hexadecimal"; + return 'hexadecimal'; } if (isString(defaultValue)) { - return "str"; + return 'str'; } if (isBool(defaultValue)) { - return "bool"; + return 'bool'; } if (isList(defaultValue)) { - return "list"; + return 'list'; } if (isTuple(defaultValue)) { - return "tuple"; + return 'tuple'; } if (isDict(defaultValue)) { - return "dict"; + return 'dict'; } if (isRegexp(defaultValue)) { - return "regexp"; + return 'regexp'; } if (isUnicode(defaultValue)) { - return "unicode"; + return 'unicode'; } if (isBytes(defaultValue)) { - return "bytes"; + return 'bytes'; } if (isFunction(defaultValue)) { - return "function"; + return 'function'; } return undefined; } function guessTypeFromName(parameter: string): string | undefined { - if (parameter.startsWith("is") || parameter.startsWith("has")) { - return "bool"; + if (parameter.startsWith('is') || parameter.startsWith('has')) { + return 'bool'; } - if (inArray(parameter, ["cb", "callback", "done", "next", "fn"])) { - return "function"; + if (inArray(parameter, ['cb', 'callback', 'done', 'next', 'fn'])) { + return 'function'; } return undefined; @@ -103,7 +103,7 @@ function hasTypeHint(parameter: string): boolean { } function isKwarg(parameter: string): boolean { - return parameter.includes("="); + return parameter.includes('='); } function isInteger(value: string): boolean { @@ -166,6 +166,6 @@ function isFunction(value: string): boolean { return pattern.test(value); } -export function inArray(item: type, array: type[]) { +export function inArray(item: T, array: T[]) { return array.some((x) => item === x); } diff --git a/modules/openvino_code/src/docstring/parse/parse-parameters.ts b/modules/openvino_code/src/docstring/parse/parse-parameters.ts index de40d3c07..01d22d234 100644 --- a/modules/openvino_code/src/docstring/parse/parse-parameters.ts +++ b/modules/openvino_code/src/docstring/parse/parse-parameters.ts @@ -1,5 +1,5 @@ /* eslint-disable no-useless-escape */ -import { guessType } from "."; +import { guessType } from '.'; import { Argument, Decorator, @@ -8,7 +8,8 @@ import { KeywordArgument, Returns, Yields, -} from "../docstring-template/docstring-parts"; +} from '../docstring-template/docstring-parts'; +import { preprocessLines } from './utilities'; export function parseParameters( parameterTokens: string[], @@ -16,14 +17,15 @@ export function parseParameters( functionName: string, code: string[] = [] ): DocstringParts { + const bodyWithoutIndentation = preprocessLines(body); return { name: functionName, decorators: parseDecorators(parameterTokens), args: parseArguments(parameterTokens), kwargs: parseKeywordArguments(parameterTokens), - returns: parseReturn(parameterTokens, body), - yields: parseYields(parameterTokens, body), - exceptions: parseExceptions(body), + returns: parseReturn(parameterTokens, bodyWithoutIndentation), + yields: parseYields(parameterTokens, bodyWithoutIndentation), + exceptions: parseExceptions(bodyWithoutIndentation), code: code, }; } @@ -49,13 +51,13 @@ function parseDecorators(parameters: string[]): Decorator[] { function parseArguments(parameters: string[]): Argument[] { const args: Argument[] = []; - const excludedArgs = ["self", "cls"]; + const excludedArgs = ['self', 'cls']; const pattern = /^(\w+)/; for (const param of parameters) { const match = param.trim().match(pattern); - if (match == null || param.includes("=") || inArray(param, excludedArgs)) { + if (match == null || param.includes('=') || inArray(param, excludedArgs)) { continue; } @@ -128,7 +130,7 @@ function parseReturnFromDefinition(parameters: string[]): Returns | null { } // Skip "-> None" annotations - return match[1] === "None" ? null : { type: match[1] }; + return match[1] === 'None' ? null : { type: match[1] }; } return null; @@ -151,7 +153,7 @@ function parseExceptions(body: string[]): Exception[] { return exceptions; } -export function inArray(item: type, array: type[]) { +export function inArray(item: T, array: T[]) { return array.some((x) => item === x); } @@ -174,5 +176,5 @@ function parseFromBody(body: string[], pattern: RegExp): Returns | Yields | unde * @param type The annotated type */ function isIterator(type: string): boolean { - return type.startsWith("Generator") || type.startsWith("Iterator"); + return type.startsWith('Generator') || type.startsWith('Iterator'); } diff --git a/modules/openvino_code/src/docstring/parse/parse.ts b/modules/openvino_code/src/docstring/parse/parse.ts index d8cd681c3..b16f68d0d 100644 --- a/modules/openvino_code/src/docstring/parse/parse.ts +++ b/modules/openvino_code/src/docstring/parse/parse.ts @@ -1,13 +1,17 @@ -import { getBody, getDefinition, getFunctionName, parseParameters, tokenizeDefinition } from "."; -import { DocstringParts } from "../docstring-template/docstring-parts"; +import { getBody, getDefinition, getFunctionName, parseParameters, tokenizeDefinition } from '.'; +import { DocstringParts } from '../docstring-template/docstring-parts'; -export function parse(document: string, positionLine: number): DocstringParts { +export function parse( + document: string, + positionLine: number, + defaultIndentation: number +): { docstringParts: DocstringParts; definition: string } { const definition = getDefinition(document, positionLine); - const body = getBody(document, positionLine); + const body = getBody(document, positionLine, defaultIndentation); const parameterTokens = tokenizeDefinition(definition); const functionName = getFunctionName(definition); - const code = [definition].concat(body); + const code = [definition, ...body]; - return parseParameters(parameterTokens, body, functionName, code); + return { docstringParts: parseParameters(parameterTokens, body, functionName, code), definition }; } diff --git a/modules/openvino_code/src/docstring/parse/utilities.ts b/modules/openvino_code/src/docstring/parse/utilities.ts index f216b3383..b2eb4c93a 100644 --- a/modules/openvino_code/src/docstring/parse/utilities.ts +++ b/modules/openvino_code/src/docstring/parse/utilities.ts @@ -18,6 +18,10 @@ export function preprocessLines(lines: string[]): string[] { return lines.map((line) => line.trim()).filter((line) => !line.startsWith("#")); } +export function filterComments(lines: string[]): string[] { + return lines.filter((line) => !line.startsWith("#")); +} + export function indentationOf(line: string): number { return getIndentation(line).length; } diff --git a/modules/openvino_code/src/python-server/commands-runner.ts b/modules/openvino_code/src/python-server/commands-runner.ts index 988252a3a..4ee10fa6b 100644 --- a/modules/openvino_code/src/python-server/commands-runner.ts +++ b/modules/openvino_code/src/python-server/commands-runner.ts @@ -1,12 +1,12 @@ import { ChildProcess, spawn, exec } from 'child_process'; -import { StopSignal } from './stop-controller'; import { LogOutputChannel } from 'vscode'; +import { logServerMessage } from './server-log'; export interface RunCommandOptions { cwd?: string; env?: NodeJS.ProcessEnv; - stopSignal?: StopSignal; - logger: LogOutputChannel; + abortSignal?: AbortSignal; + logger: LogOutputChannel | null; listeners?: Listeners; } @@ -14,48 +14,52 @@ interface Listeners { stdout: (data: string) => void; } +export const isAbortError = (error: Error): boolean => error.name === 'AbortError'; + const pidMessagePrefixer = (pid?: number) => (message: string) => `[Process: ${pid}] ${message}`; export function spawnCommand(command: string, args: string[], options: RunCommandOptions) { - const { env, cwd, stopSignal, logger, listeners } = options; + const { env, cwd, abortSignal, logger, listeners } = options; - if (stopSignal?.stopped) { - logger.debug(`running command: ${command} ${args ? args?.join(' ') : ''} is skipped. Received stop signal`); + if (abortSignal?.aborted) { + logger?.debug(`running command: ${command} ${args ? args?.join(' ') : ''} is skipped. Received stop signal`); return; } - logger.debug(`running command: ${command} ${args ? args?.join(' ') : ''}`); + logger?.debug(`running command: ${command} ${args ? args?.join(' ') : ''}`); const process = spawn(command, args, { cwd, env, + signal: abortSignal, }); - return waitForChildProcess(process, logger, stopSignal, listeners); + return waitForChildProcess(process, logger, abortSignal, listeners); } export function execCommand(command: string, options: RunCommandOptions) { - const { env, cwd, stopSignal, logger, listeners } = options; + const { env, cwd, abortSignal, logger, listeners } = options; - if (stopSignal?.stopped) { - logger.debug(`running command: ${command} is skipped. Received stop signal`); + if (abortSignal?.aborted) { + logger?.debug(`running command: ${command} is skipped. Received stop signal`); return; } - logger.debug(`running command: ${command}`); + logger?.debug(`running command: ${command}`); const process = exec(command, { cwd, env, + signal: abortSignal, }); - return waitForChildProcess(process, logger, stopSignal, listeners); + return waitForChildProcess(process, logger, abortSignal, listeners); } async function waitForChildProcess( process: ChildProcess, - logger: LogOutputChannel, - stopSignal?: StopSignal, + logger: RunCommandOptions['logger'], + abortSignal?: AbortSignal, listeners?: Listeners ) { let result: string = ''; @@ -63,16 +67,20 @@ async function waitForChildProcess( const prefixMessage = pidMessagePrefixer(process.pid); const stopSignalHandler = () => { - logger.debug(prefixMessage('killing process')); + logger?.debug(prefixMessage('killing process')); + // TODO Consider removing explicit process kill and rely on AbortSignal only process.kill(); }; - stopSignal?.once(stopSignalHandler); + + abortSignal?.addEventListener('abort', stopSignalHandler, { once: true }); return new Promise((resolve, reject) => { process.stdout?.on('data', (data) => { const textData = String(data).trim(); - logger.debug(prefixMessage(textData)); + if (logger) { + logServerMessage(logger, textData, prefixMessage); + } if (listeners?.stdout) { listeners?.stdout(textData); @@ -84,20 +92,25 @@ async function waitForChildProcess( process.stderr?.on('data', (data) => { const textData = String(data).trim(); - logger.error(prefixMessage(textData)); + logger?.error(prefixMessage(textData)); if (!error) { error = new Error(textData); } }); process.on('error', (err) => { - logger.error(prefixMessage(err.message)); + if (isAbortError(err)) { + logger?.debug(prefixMessage(err.message)); + } else { + logger?.error(prefixMessage(err.message)); + } error = err; }); process.on('close', (code, signal) => { - logger.debug(prefixMessage(`exited with code: ${code} and signal: ${signal}`)); - stopSignal?.removeListener(stopSignalHandler); + logger?.debug(prefixMessage(`exited with code: ${code} and signal: ${signal}`)); + + abortSignal?.removeEventListener('abort', stopSignalHandler); if (code === 0) { resolve(result); diff --git a/modules/openvino_code/src/python-server/detect-python.ts b/modules/openvino_code/src/python-server/detect-python.ts index 5fc1beb63..584f50a91 100644 --- a/modules/openvino_code/src/python-server/detect-python.ts +++ b/modules/openvino_code/src/python-server/detect-python.ts @@ -16,7 +16,7 @@ export async function getPythonExecutable( for (const executable of executables) { try { - await verifyPythonVersion(executable, requriedVersion, logger); + await verifyPythonVersion(executable, requriedVersion); result = executable; } catch (e) { errors.push(e); @@ -31,24 +31,19 @@ export async function getPythonExecutable( const errorMessage = ['Cannot find python executable.']; if (errors.length) { errorMessage.push(' Next error(s) occured:\n'); - errorMessage.push(`${String(errors[0])}\n`); - } - - if (errors[1]) { - errorMessage.push(`${String(errors[1])}\n`); + for (const error of errors) { + errorMessage.push(`${String(error)}\n`); + } } throw new Error(errorMessage.join('')); } -async function verifyPythonVersion( - executable: PythonExecutable, - requriedVersion: Version, - logger: LogOutputChannel -): Promise { +async function verifyPythonVersion(executable: PythonExecutable, requriedVersion: Version): Promise { const command = `${executable} --version`; + const commandResult = await execCommand(command, { - logger, + logger: null, // Prevent showing stderr output for `python --version` command (e.g. "/bin/sh: 1: python: not found") }); if (!commandResult) { diff --git a/modules/openvino_code/src/python-server/pip.ts b/modules/openvino_code/src/python-server/pip.ts index 65ab96db5..2769729b4 100644 --- a/modules/openvino_code/src/python-server/pip.ts +++ b/modules/openvino_code/src/python-server/pip.ts @@ -2,11 +2,8 @@ import { execCommand } from './commands-runner'; import type { PythonServerConfiguration } from './python-server-runner'; import { getVenvActivateCommand } from './virtual-environment'; -// fixme: 'pip install --upgrade pip' doesn't fail on bad proxy settings. The command 'pip install --upgrade pip' exit with code 0 even there is a ProxyError in stderr. -// The plain 'pip install .' doesn't have such issue. -// fixme: 'pip install --upgrade pip' doesn't exit on stop signal. The plain 'pip install .' doesn't have such issue. export async function upgradePip(config: PythonServerConfiguration) { - const { python, serverDir, proxyEnv, stopSignal, logger } = config; + const { python, serverDir, proxyEnv, abortSignal, logger } = config; logger.info('Upgrading pip version...'); const activateCommand = getVenvActivateCommand(config); @@ -15,7 +12,7 @@ export async function upgradePip(config: PythonServerConfiguration) { await execCommand(command, { logger, cwd: serverDir, - stopSignal, + abortSignal, env: { ...proxyEnv }, }); @@ -23,7 +20,7 @@ export async function upgradePip(config: PythonServerConfiguration) { } export async function installRequirements(config: PythonServerConfiguration) { - const { serverDir, proxyEnv, stopSignal, logger } = config; + const { serverDir, proxyEnv, abortSignal, logger } = config; logger.info('Installing python requirements...'); const activateCommand = getVenvActivateCommand(config); @@ -32,7 +29,7 @@ export async function installRequirements(config: PythonServerConfiguration) { await execCommand(command, { logger, cwd: serverDir, - stopSignal, + abortSignal, env: { ...proxyEnv }, }); diff --git a/modules/openvino_code/src/python-server/python-server-runner.ts b/modules/openvino_code/src/python-server/python-server-runner.ts index c4a9dd9d9..82140a3cd 100644 --- a/modules/openvino_code/src/python-server/python-server-runner.ts +++ b/modules/openvino_code/src/python-server/python-server-runner.ts @@ -1,16 +1,17 @@ -import { spawnCommand } from './commands-runner'; +import { isAbortError, spawnCommand } from './commands-runner'; import { getVenvPythonPath } from './virtual-environment'; import { PythonExecutable, Version, getPythonExecutable } from './detect-python'; import { OS, detectOs } from './detect-os'; import { createVenv, checkActivatedVenv } from './virtual-environment'; import { upgradePip, installRequirements } from './pip'; -import { StopSignal, StopController } from './stop-controller'; import { ProxyEnv, getProxyEnv } from './proxy'; import { ServerStateController } from './server-state-controller'; import { ServerStatus, ServerStartingStage } from '@shared/server-state'; import { EXTENSION_SERVER_DISPLAY_NAME } from '../constants'; import { LogOutputChannel, window } from 'vscode'; import { join } from 'path'; +import { MODEL_NAME_TO_ID_MAP, ModelName } from '@shared/model'; +import { extensionState } from '../state'; const SERVER_STARTED_STDOUT_ANCHOR = 'OpenVINO Code Server started'; @@ -18,8 +19,8 @@ interface ServerHooks { onStarted: () => void; } -async function runServer(config: PythonServerConfiguration, hooks?: ServerHooks) { - const { serverDir, proxyEnv, stopSignal, logger } = config; +async function runServer(modelName: ModelName, config: PythonServerConfiguration, hooks?: ServerHooks) { + const { serverDir, proxyEnv, abortSignal, logger } = config; logger.info('Starting server...'); const venvPython = await getVenvPythonPath(config); @@ -33,13 +34,16 @@ async function runServer(config: PythonServerConfiguration, hooks?: ServerHooks) if (data.includes(SERVER_STARTED_STDOUT_ANCHOR)) { hooks?.onStarted(); started = true; + logger.info('Server started'); } } - await spawnCommand(venvPython, ['main.py', '--model', 'chgk13/decicoder-1b-openvino-int8'], { + const model = MODEL_NAME_TO_ID_MAP[modelName]; + + await spawnCommand(venvPython, ['main.py', '--model', model], { logger, cwd: serverDir, - stopSignal, + abortSignal, env: { ...proxyEnv }, listeners: { stdout: stdoutListener }, }); @@ -55,7 +59,7 @@ export interface PythonServerConfiguration { venvDirName: string; serverDir: string; proxyEnv?: ProxyEnv; - stopSignal: StopSignal; + abortSignal: AbortSignal; logger: LogOutputChannel; } @@ -63,7 +67,7 @@ export class NativePythonServerRunner { static readonly REQUIRED_PYTHON_VERSION: Version = [3, 8]; static readonly VENV_DIR_NAME = '.venv'; - private _stopController = new StopController(); + private _abortController = new AbortController(); private readonly _stateController = new ServerStateController(); get stateReporter() { @@ -79,16 +83,20 @@ export class NativePythonServerRunner { this._stateController.setStatus(ServerStatus.STARTING); try { - logger.info('Strating Server using python vitual environment...'); + logger.info('Starting Server using python virtual environment...'); await this._start(); - logger.info('Server stopped'); } catch (e) { - const error = e instanceof Error ? e : String(e); + const error = e instanceof Error ? e : new Error(String(e)); + if (isAbortError(error)) { + logger.debug('Server launch was aborted'); + return; + } logger.error(`Server stopped with error:`); logger.error(error); } finally { this._stateController.setStage(null); this._stateController.setStatus(ServerStatus.STOPPED); + logger.info('Server stopped'); } } @@ -103,10 +111,6 @@ export class NativePythonServerRunner { logger ); - this._stateController.setStage(ServerStartingStage.CREATE_VENV); - - await createVenv(python, SERVER_DIR, NativePythonServerRunner.VENV_DIR_NAME, logger); - const proxyEnv = getProxyEnv(); if (proxyEnv) { logger.info('Applying proxy settings:'); @@ -116,13 +120,17 @@ export class NativePythonServerRunner { const config: PythonServerConfiguration = { python, os, - proxyEnv: getProxyEnv(), + proxyEnv, serverDir: SERVER_DIR, venvDirName: NativePythonServerRunner.VENV_DIR_NAME, - stopSignal: this._stopController.signal, + abortSignal: this._abortController.signal, logger, }; + this._stateController.setStage(ServerStartingStage.CREATE_VENV); + + await createVenv(config); + this._stateController.setStage(ServerStartingStage.CHECK_VENV_ACTIVATION); await checkActivatedVenv(config); @@ -137,7 +145,9 @@ export class NativePythonServerRunner { this._stateController.setStage(ServerStartingStage.START_SERVER); - await runServer(config, { + const modelName = extensionState.config.model; + + await runServer(modelName, config, { onStarted: () => { this._stateController.setStatus(ServerStatus.STARTED); this._stateController.setStage(null); @@ -147,7 +157,7 @@ export class NativePythonServerRunner { stop() { logger.info('Stopping...'); - this._stopController.stop(); - this._stopController = new StopController(); + this._abortController.abort(); + this._abortController = new AbortController(); } } diff --git a/modules/openvino_code/src/python-server/server-log.ts b/modules/openvino_code/src/python-server/server-log.ts new file mode 100644 index 000000000..37d6c694d --- /dev/null +++ b/modules/openvino_code/src/python-server/server-log.ts @@ -0,0 +1,68 @@ +import { LogOutputChannel } from 'vscode'; + +enum LogLevel { + TRACE = 'TRACE', + DEBUG = 'DEBUG', + INFO = 'INFO', + WARNING = 'WARNING', + ERROR = 'ERROR', + CRITICAL = 'CRITICAL', +} + +const serverLogPrefix = '[OpenVINO Code Server Log] '; +const timestampPattern = '\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}'; +const logLevelPattern = Object.values(LogLevel).join('|'); + +const serverLogRegExp = new RegExp(`^${timestampPattern} (?${logLevelPattern}) (?(?:.|\\n|\\r)+)`); + +interface IServerLog { + level: LogLevel; + message: string; +} + +const parseServerLogs = (output: string): (IServerLog | null)[] => { + const serverLogs = output.split(serverLogPrefix).filter(Boolean); + + return serverLogs.map((log) => { + const match = serverLogRegExp.exec(log); + + const level = match?.groups?.level as LogLevel | undefined; + const message = match?.groups?.message; + + if (!level || !message) { + return null; + } + + return { + level, + message, + }; + }); +}; + +export const logServerMessage = ( + logger: LogOutputChannel, + message: string, + prefixer: (message: string) => string +): void => { + const logLevelToMethodMap: Record = { + [LogLevel.TRACE]: logger.trace.bind(logger), + [LogLevel.DEBUG]: logger.debug.bind(logger), + [LogLevel.INFO]: logger.info.bind(logger), + [LogLevel.WARNING]: logger.warn.bind(logger), + [LogLevel.ERROR]: logger.error.bind(logger), + [LogLevel.CRITICAL]: logger.error.bind(logger), + }; + + const serverLogs = parseServerLogs(message); + + for (const serverLog of serverLogs) { + if (!serverLog) { + logger.debug(message); + } else { + const loggerMethod = logLevelToMethodMap[serverLog.level]; + + loggerMethod(prefixer(serverLog.message)); + } + } +}; diff --git a/modules/openvino_code/src/python-server/stop-controller.ts b/modules/openvino_code/src/python-server/stop-controller.ts deleted file mode 100644 index fe58595e7..000000000 --- a/modules/openvino_code/src/python-server/stop-controller.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { EventEmitter } from 'node:events'; - -interface Listener { - (...args: unknown[]): unknown; -} - -export class StopSignal { - stopped = false; - once: (fn: Listener) => void; - removeListener: (fn: Listener) => void; - - constructor(once: (fn: Listener) => void, removeListener: (fn: Listener) => void) { - this.once = once; - this.removeListener = removeListener; - } -} - -export class StopController { - readonly signal: StopSignal; - - private readonly _emmiter = new EventEmitter(); - private static _stopEventName = 'stop'; - - constructor() { - this.signal = new StopSignal(this.once.bind(this), this.removeListener.bind(this)); - } - - private once(fn: Listener) { - this._emmiter.addListener(StopController._stopEventName, fn); - } - - private removeListener(fn: Listener) { - this._emmiter.removeListener(StopController._stopEventName, fn); - } - - stop() { - this._emmiter.emit(StopController._stopEventName); - this.signal.stopped = true; - } -} diff --git a/modules/openvino_code/src/python-server/virtual-environment.ts b/modules/openvino_code/src/python-server/virtual-environment.ts index b8cf4c291..466f83e25 100644 --- a/modules/openvino_code/src/python-server/virtual-environment.ts +++ b/modules/openvino_code/src/python-server/virtual-environment.ts @@ -1,23 +1,18 @@ import { join } from 'path'; import { execCommand } from './commands-runner'; import { OS } from './detect-os'; -import { PythonExecutable } from './detect-python'; import type { PythonServerConfiguration } from './python-server-runner'; -import { LogOutputChannel } from 'vscode'; import { stat } from 'fs/promises'; -export async function createVenv( - python: PythonExecutable, - directory: string, - venvName: string, - logger: LogOutputChannel -) { +export async function createVenv(config: PythonServerConfiguration) { + const { logger, python, serverDir, venvDirName, abortSignal } = config; logger.info('Creating virtual environment...'); - const command = `${python} -m venv ${venvName}`; + const command = `${python} -m venv ${venvDirName}`; await execCommand(command, { - cwd: directory, + cwd: serverDir, logger, + abortSignal, }); logger.info('Virtual environment created'); @@ -28,6 +23,7 @@ export async function checkActivatedVenv(config: PythonServerConfiguration) { await execCommand(activateCommand, { cwd: config.serverDir, logger: config.logger, + abortSignal: config.abortSignal, }); } diff --git a/modules/openvino_code/src/services/backend.service.ts b/modules/openvino_code/src/services/backend.service.ts index 78faa0d17..5c251a845 100644 --- a/modules/openvino_code/src/services/backend.service.ts +++ b/modules/openvino_code/src/services/backend.service.ts @@ -15,6 +15,20 @@ interface IGenerateRequest { }; } +interface IGenerateDocStringRequest { + inputs: string; + template: string; + definition: string; + format?: string; + parameters: { + temperature: number; + top_k: number; + top_p: number; + min_new_tokens: number; + max_new_tokens: number; + }; +} + interface IGenerateResponse { generated_text: string; } @@ -25,7 +39,7 @@ interface RequestOptions { timeout: number; } -class ServerError extends Error {} +class ServerError extends Error { } const skipEmptyGeneratedText = (response: IGenerateResponse | null) => !response?.generated_text.trim(); @@ -62,7 +76,7 @@ class BackendService { return this._sendRequest(this._endpoints.generate, 'POST', data); } - async generateSummarization(data: IGenerateRequest): Promise { + async generateSummarization(data: IGenerateDocStringRequest): Promise { return this._sendRequest(this._endpoints.summarize, 'POST', data); } diff --git a/modules/openvino_code/src/settings/settings.service.ts b/modules/openvino_code/src/settings/settings.service.ts index f9a3a4e3a..0583119c5 100644 --- a/modules/openvino_code/src/settings/settings.service.ts +++ b/modules/openvino_code/src/settings/settings.service.ts @@ -1,7 +1,8 @@ -import { ExtensionContext, commands } from 'vscode'; +import { ConfigurationTarget, ExtensionContext, commands } from 'vscode'; import { COMMANDS, CONFIG_KEY, EXTENSION_PACKAGE, EXTENSION_DISPLAY_NAME } from '../constants'; import { CustomConfiguration } from '../configuration'; import { IExtensionComponent } from '../extension-component.interface'; +import { extensionState } from '../state'; class SettingsService implements IExtensionComponent { activate(context: ExtensionContext): void { @@ -27,6 +28,10 @@ class SettingsService implements IExtensionComponent { const configKey = key ? [CONFIG_KEY, key].join('.') : CONFIG_KEY; void commands.executeCommand('workbench.action.openSettings', configKey); } + + updateSetting(key: K, value: CustomConfiguration[K]): void { + void extensionState.config.update(key, value, ConfigurationTarget.Global); + } } export const settingsService = new SettingsService(); diff --git a/modules/openvino_code/src/side-panel/side-panel-message-handler.ts b/modules/openvino_code/src/side-panel/side-panel-message-handler.ts new file mode 100644 index 000000000..35f42032b --- /dev/null +++ b/modules/openvino_code/src/side-panel/side-panel-message-handler.ts @@ -0,0 +1,28 @@ +import { ISidePanelMessage, SidePanelMessageTypes } from '@shared/side-panel-message'; +import { extensionState } from '../state'; +import { Webview, commands } from 'vscode'; +import { settingsService } from '../settings/settings.service'; +import { COMMANDS } from '../constants'; +import { ModelName } from '@shared/model'; + +type SidePanelMessageHandlerType = (webview: Webview, payload?: ISidePanelMessage['payload']) => void; + +const sidePanelMessageHandlers: Record = { + [SidePanelMessageTypes.GET_EXTENSION_STATE]: (webview) => void webview.postMessage(extensionState.state), + [SidePanelMessageTypes.SETTINGS_CLICK]: () => settingsService.openSettings(), + [SidePanelMessageTypes.MODEL_CHANGE]: (_, payload) => + settingsService.updateSetting('model', (payload as { modelName: ModelName }).modelName), + [SidePanelMessageTypes.START_SERVER_CLICK]: () => void commands.executeCommand(COMMANDS.START_SERVER_NATIVE), + [SidePanelMessageTypes.STOP_SERVER_CLICK]: () => void commands.executeCommand(COMMANDS.STOP_SERVER_NATIVE), + [SidePanelMessageTypes.SHOW_SERVER_LOG_CLICK]: () => void commands.executeCommand(COMMANDS.SHOW_SERVER_LOG), + [SidePanelMessageTypes.SHOW_EXTENSION_LOG_CLICK]: () => void commands.executeCommand(COMMANDS.SHOW_EXTENSION_LOG), + [SidePanelMessageTypes.CHECK_CONNECTION_CLICK]: () => void commands.executeCommand(COMMANDS.CHECK_CONNECTION), + [SidePanelMessageTypes.GENERATE_COMPLETION_CLICK]: () => + void commands.executeCommand(COMMANDS.GENERATE_INLINE_COPMLETION), +}; + +export function handleSidePanelMessage(message: M, webview: Webview): void { + const { type, payload } = message; + const handler = sidePanelMessageHandlers[type]; + handler(webview, payload); +} diff --git a/modules/openvino_code/src/side-panel/side-panel-message.ts b/modules/openvino_code/src/side-panel/side-panel-message.ts deleted file mode 100644 index d9a551e3a..000000000 --- a/modules/openvino_code/src/side-panel/side-panel-message.ts +++ /dev/null @@ -1,15 +0,0 @@ -const sidePanelMessagePrefix = 'side-panel.message'; - -export enum SidePanelMessageTypes { - GET_EXTENSION_STATE = `${sidePanelMessagePrefix}.getExtensionState`, - START_SERVER_CLICK = `${sidePanelMessagePrefix}.startServerClick`, - STOP_SERVER_CLICK = `${sidePanelMessagePrefix}.stopServerClick`, - SHOW_SERVER_LOG_CLICK = `${sidePanelMessagePrefix}.showServerLogClick`, - GENERATE_COMPLETION_CLICK = `${sidePanelMessagePrefix}.generateCompletionClick`, - SETTINGS_CLICK = `${sidePanelMessagePrefix}.settingsClick`, -} - -export interface ISidePanelMessage

    { - type: SidePanelMessageTypes; - payload?: P; -} diff --git a/modules/openvino_code/src/side-panel/side-panel-view-provider.ts b/modules/openvino_code/src/side-panel/side-panel-view-provider.ts index 4873d424a..b27063a7e 100644 --- a/modules/openvino_code/src/side-panel/side-panel-view-provider.ts +++ b/modules/openvino_code/src/side-panel/side-panel-view-provider.ts @@ -6,13 +6,12 @@ import { WebviewView, WebviewViewProvider, WebviewViewResolveContext, - commands, } from 'vscode'; -import { COMMANDS, SIDE_PANEL_VIEW_ID } from '../constants'; -import { ISidePanelMessage, SidePanelMessageTypes } from '@shared/side-panel-message'; -import { settingsService } from '../settings/settings.service'; +import { SIDE_PANEL_VIEW_ID } from '../constants'; +import { ISidePanelMessage } from '@shared/side-panel-message'; import { extensionState } from '../state'; import { IExtensionState } from '@shared/extension-state'; +import { handleSidePanelMessage } from './side-panel-message-handler'; export class SidePanelViewProvider implements WebviewViewProvider { static viewId = SIDE_PANEL_VIEW_ID; @@ -78,41 +77,9 @@ export class SidePanelViewProvider implements WebviewViewProvider { } private _subscribeToWebviewMessages(webview: Webview): void { - // TODO Consider moving outside of provider webview.onDidReceiveMessage( - ({ type }: M) => { - if (type === SidePanelMessageTypes.GET_EXTENSION_STATE) { - void webview.postMessage(extensionState.state); - return; - } - if (type === SidePanelMessageTypes.SETTINGS_CLICK) { - settingsService.openSettings(); - return; - } - if (type === SidePanelMessageTypes.START_SERVER_CLICK) { - void commands.executeCommand(COMMANDS.START_SERVER_NATIVE); - return; - } - if (type === SidePanelMessageTypes.STOP_SERVER_CLICK) { - void commands.executeCommand(COMMANDS.STOP_SERVER_NATIVE); - return; - } - if (type === SidePanelMessageTypes.SHOW_SERVER_LOG_CLICK) { - void commands.executeCommand(COMMANDS.SHOW_SERVER_LOG); - return; - } - if (type === SidePanelMessageTypes.SHOW_EXTENSION_LOG_CLICK) { - void commands.executeCommand(COMMANDS.SHOW_EXTENSION_LOG); - return; - } - if (type === SidePanelMessageTypes.CHECK_CONNECTION_CLICK) { - void commands.executeCommand(COMMANDS.CHECK_CONNECTION); - return; - } - if (type === SidePanelMessageTypes.GENERATE_COMPLETION_CLICK) { - void commands.executeCommand(COMMANDS.GENERATE_INLINE_COPMLETION); - return; - } + (message: M) => { + handleSidePanelMessage(message, webview); }, null, this._disposables diff --git a/modules/openvino_code/src/state.ts b/modules/openvino_code/src/state.ts index 1655db84b..2e8315485 100644 --- a/modules/openvino_code/src/state.ts +++ b/modules/openvino_code/src/state.ts @@ -5,6 +5,10 @@ import { CONFIG_KEY } from './constants'; import { IExtensionComponent } from './extension-component.interface'; import { IExtensionState, ConnectionStatus } from '@shared/extension-state'; import { INITIAL_SERVER_STATE, ServerStatus } from '@shared/server-state'; +import { MODEL_SUPPORTED_FEATURES } from '@shared/model'; +import { Features } from '@shared/features'; + +const getConfig = () => workspace.getConfiguration(CONFIG_KEY) as ExtensionConfiguration; class ExtensionState implements IExtensionComponent { private readonly _state: IExtensionState = { @@ -15,7 +19,16 @@ class ExtensionState implements IExtensionComponent { return this.server.status === ServerStatus.STARTED && this.connectionStatus === ConnectionStatus.AVAILABLE; }, get config(): ExtensionConfiguration { - return workspace.getConfiguration(CONFIG_KEY) as ExtensionConfiguration; + return getConfig(); + }, + features: { + get supportedList(): Features[] { + const config = getConfig(); + return MODEL_SUPPORTED_FEATURES[config.model]; + }, + get isSummarizationSupported(): boolean { + return this.supportedList.includes(Features.SUMMARIZATION); + }, }, }; @@ -36,11 +49,25 @@ class ExtensionState implements IExtensionComponent { activate(extensionContext: ExtensionContext): void { // Might be used to store configuration in `extensionContext.globalState` this._extensionContext = extensionContext; + + workspace.onDidChangeConfiguration( + (event) => { + if (event.affectsConfiguration(CONFIG_KEY)) { + this._emitCurrentState(); + } + }, + null, + extensionContext.subscriptions + ); + } + + private _emitCurrentState(): void { + this._emitter.emit(ExtensionState._stateChangedEvent, this._state); } set(key: K, value: IExtensionState[K]): void { this._state[key] = value; - this._emitter.emit(ExtensionState._stateChangedEvent, this._state); + this._emitCurrentState(); } get(key: K): IExtensionState[K] { diff --git a/modules/openvino_code/tsconfig.json b/modules/openvino_code/tsconfig.json index 589e7b168..60f58bd71 100644 --- a/modules/openvino_code/tsconfig.json +++ b/modules/openvino_code/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "module": "commonjs", "target": "es2018", - "lib": ["es2018"], + "lib": ["es2018", "WebWorker"], "outDir": "out", "sourceMap": true, "strict": true,