From 34f6878cd9379884e296f519ca338fbfbc54c855 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Thu, 29 Aug 2024 14:45:29 -0700 Subject: [PATCH 01/27] all --- .../user/ai-assistant/ai-assistant.service.ts | 62 +++++ .../code-editor.component.html | 18 ++ .../code-editor.component.scss | 40 ++++ .../code-editor.component.ts | 212 +++++++++++++++++- 4 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts new file mode 100644 index 00000000000..11fbec9d534 --- /dev/null +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -0,0 +1,62 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; +import { AppSettings } from "../../../../common/app-setting"; +import { HttpClient } from "@angular/common/http"; + +export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassistant`; + +@Injectable({ + providedIn: "root", +}) +export class AiAssistantService { + constructor(private http: HttpClient) {} + + public checkAiAssistantEnabled(): Promise { + const apiUrl = `${AI_ASSISTANT_API_BASE_URL}/isenabled`; + return firstValueFrom(this.http.get(apiUrl)) + .then(response => (response !== undefined ? response : false)) + .catch(() => false); + } + + public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Promise { + const prompt = ` + Your task is to analyze the given Python code and provide only the type annotation as stated in the instructions. + Instructions: + - The provided code will only be one of the 2 situations below: + - First situation: The input is not start with "def". If the provided code only contains variable, output the result in the format ":type". + - Second situation: The input is start with "def". If the provided code starts with "def" (a longer line than just a variable, indicative of a function or method), output the result in the format " -> type". + - The type should only be one word, such as "str", "int", etc. + Examples: + - First situation: + - Provided code is "name", then the output may be : str + - Provided code is "age", then the output may be : int + - Provided code is "data", then the output may be : Tuple[int, str] + - Provided code is "new_user", then the output may be : User + - A special case: provided code is "self" and the context is something like "def __init__(self, username :str , age :int)", if the user requires the type annotation for the first parameter "self", then you should generate nothing. + - Second situation: (actual output depends on the complete code content) + - Provided code is "process_data(data: List[Tuple[int, str]], config: Dict[str, Union[int, str]])", then the output may be -> Optional[str] + - Provided code is "def add(a: int, b: int)", then the output may be -> int + Counterexamples: + - Provided code is "def __init__(self, username: str, age: int)" and you generate the result: + The result is The provided code is "def __init__(self, username: str, age: int)", so it fits the second situation, which means the result should be in " -> type" format. However, the __init__ method in Python doesn't return anything or in other words, it implicitly returns None. Hence the correct type hint would be: -> None. + I don't want this result! The correct result you should generate is -> None for this counter case. + Details: + - Provided code: ${code} + - Line number of the provided code in the complete code context: ${lineNumber} + - Complete code context: ${allcode} + Important: (you must follow!!) + - For the first situation: you must return strictly according to the format ": type", without adding any extra characters. No need for an explanation, just the result : type is enough! + - For the second situation: you return strictly according to the format " -> type", without adding any extra characters. No need for an explanation, just the result -> type is enough! + `; + return firstValueFrom(this.http.post(`${AI_ASSISTANT_API_BASE_URL}/getresult`, { prompt })) + .then(response => { + console.log("Received response from backend:", response); + const result = response.choices[0].message.content.trim(); + return result; + }) + .catch(error => { + console.error("Request to backend failed:", error); + return ""; + }); + } +} diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html index e94d617541e..481ecbcec39 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html @@ -29,3 +29,21 @@
+ +
+

Do you agree with the type annotation suggestion?

+
Adding annotation for code: {{ currentCode }}
+

Given suggestion: {{ currentSuggestion }}

+ + +
diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss index d4920dddeff..53eef3440b4 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss @@ -49,3 +49,43 @@ left: -4px; top: -5px; } + +.annotation-suggestion { + position: absolute; + background: #222; + color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.annotation-suggestion button { + margin-right: 10px; +} + +.annotation-suggestion button.accept-button { + background-color: #28a745; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; +} + +.annotation-suggestion button.accept-button:hover { + background-color: #218838; +} + +.annotation-suggestion button.decline-button { + background-color: #dc3545; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; +} + +.annotation-suggestion button.decline-button:hover { + background-color: #c82333; +} diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index d0ed9343d0e..b0fa0f3683b 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -17,6 +17,7 @@ import { isUndefined } from "lodash"; import { CloseAction, ErrorAction } from "vscode-languageclient/lib/common/client.js"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { FormControl } from "@angular/forms"; +import { AiAssistantService } from "../../../dashboard/service/user/ai-assistant/ai-assistant.service"; /** * CodeEditorComponent is the content of the dialogue invoked by CodeareaCustomTemplateComponent. @@ -46,6 +47,15 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy public language: string = ""; public languageTitle: string = ""; + // Boolean to determine whether the suggestion UI should be shown + public showAnnotationSuggestion: boolean = false; + // The code selected by the user + public currentCode: string = ""; + // The result returned by the backend AI assistant + public currentSuggestion: string = ""; + // The range selected by the user + public currentRange: monaco.Range | null = null; + private generateLanguageTitle(language: string): string { return `${language.charAt(0).toUpperCase()}${language.slice(1)} UDF`; } @@ -63,7 +73,8 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy private sanitizer: DomSanitizer, private workflowActionService: WorkflowActionService, private workflowVersionService: WorkflowVersionService, - public coeditorPresenceService: CoeditorPresenceService + public coeditorPresenceService: CoeditorPresenceService, + private aiAssistantService: AiAssistantService ) { const currentOperatorId = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0]; const operatorType = this.workflowActionService.getTexeraGraph().getOperator(currentOperatorId).operatorType; @@ -152,7 +163,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy * Create a Monaco editor and connect it to MonacoBinding. * @private */ - private initMonaco() { + private async initMonaco() { const editor = monaco.editor.create(this.editorElement.nativeElement, { language: this.language, fontSize: 11, @@ -169,11 +180,208 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy ); } this.editor = editor; + + // Check if the AI provider is "openai" + if (await this.aiAssistantService.checkAiAssistantEnabled()) { + // Add all needed modules for add type annotation + this.addAnnotationModule(editor); + + // "Add Type Annotation" Button + editor.addAction({ + id: "type-annotation-action", + label: "Add Type Annotation", + contextMenuGroupId: "1_modification", + contextMenuOrder: 1.0, + run: async ed => { + // User selected code(including range and content) + const selection = ed.getSelection(); + const model = ed.getModel(); + if (!model || !selection) { + return; + } + // All the code in Python UDF + const allcode = model.getValue(); + // Content of user selected code + const code = model.getValueInRange(selection); + // Start line of the selected code + const lineNumber = selection.startLineNumber; + await this.handleTypeAnnotation( + code, + selection, + ed as monaco.editor.IStandaloneCodeEditor, + lineNumber, + allcode + ); + }, + }); + } if (this.language == "python") { this.connectLanguageServer(); } } + private async handleTypeAnnotation( + code: string, + range: monaco.Range, + editor: monaco.editor.IStandaloneCodeEditor, + lineNumber: number, + allcode: string + ): Promise { + return new Promise(resolve => { + this.aiAssistantService.getTypeAnnotations(code, lineNumber, allcode).then(typeAnnotations => { + console.log("The result from OpenAI is", typeAnnotations); + + let acceptButton: HTMLButtonElement | null = null; + let declineButton: HTMLButtonElement | null = null; + + this.currentCode = code; + this.currentSuggestion = typeAnnotations; + this.currentRange = range; + this.showAnnotationSuggestion = true; + + // Let the suggestion pop up next to the selected code + setTimeout(() => { + const position = editor.getScrolledVisiblePosition(range.getStartPosition()); + const popupElement = document.querySelector(".annotation-suggestion") as HTMLElement; + + if (popupElement && position) { + popupElement.style.top = `${position.top + 100}px`; + popupElement.style.left = `${position.left + 100}px`; + } + + // Make sure the user click the button + const cleanup = () => { + console.log("Cleaning up and resolving..."); + if (acceptButton) acceptButton.removeEventListener("click", acceptListener); + if (declineButton) declineButton.removeEventListener("click", declineListener); + this.showAnnotationSuggestion = false; + resolve(); + console.log("Resolved!"); + }; + + const acceptListener = () => { + this.acceptCurrentAnnotation(); + cleanup(); + }; + + const declineListener = () => { + cleanup(); + }; + acceptButton = document.querySelector(".accept-button") as HTMLButtonElement; + declineButton = document.querySelector(".decline-button") as HTMLButtonElement; + + if (acceptButton && declineButton) { + console.log("Buttons found, adding event listeners"); + //clean the old one for the "add all type annotation" + acceptButton.removeEventListener("click", acceptListener); + declineButton.removeEventListener("click", declineListener); + + acceptButton.addEventListener("click", acceptListener, { once: true }); + declineButton.addEventListener("click", declineListener, { once: true }); + } else { + console.error("Buttons not found!"); + } + }, 0); + }); + }); + } + + // Called when the user clicks the "accept" button + public acceptCurrentAnnotation(): void { + // Avoid accidental calls + if (!this.showAnnotationSuggestion || !this.currentRange || !this.currentSuggestion) { + return; + } + + if (this.currentRange && this.currentSuggestion) { + const selection = new monaco.Selection( + this.currentRange.startLineNumber, + this.currentRange.startColumn, + this.currentRange.endLineNumber, + this.currentRange.endColumn + ); + this.insertTypeAnnotations(this.editor, selection, this.currentSuggestion); + } + // close the UI after adding the annotation + this.showAnnotationSuggestion = false; + } + + // Called when the user clicks the "decline" button + public rejectCurrentAnnotation(): void { + // Do nothing except for closing the UI + this.showAnnotationSuggestion = false; + } + + // Add the type annotation into monaco editor + private insertTypeAnnotations( + editor: monaco.editor.IStandaloneCodeEditor, + selection: monaco.Selection, + annotations: string + ) { + const endLineNumber = selection.endLineNumber; + const endColumn = selection.endColumn; + const range = new monaco.Range( + // Insert the content to the end of the selected code + endLineNumber, + endColumn, + endLineNumber, + endColumn + ); + const text = `${annotations}`; + const op = { + range: range, + text: text, + forceMoveMarkers: true, + }; + editor.executeEdits("add annotation", [op]); + } + + // Add all necessary modules for type annotation at the first line of the Python UDF + private addAnnotationModule(editor: monaco.editor.IStandaloneCodeEditor) { + const model = editor.getModel(); + if (!model) { + return; + } + const allCode = model.getValue(); + const typingImports = [ + "Any", + "Awaitable", + "Callable", + "Coroutine", + "Dict", + "FrozenSet", + "Generator", + "Generic", + "Iterable", + "Iterator", + "List", + "Mapping", + "Optional", + "Sequence", + "Set", + "Tuple", + "Type", + "TypeVar", + "Union", + "Deque", + "NamedTuple", + "TypedDict", + "Protocol", + "Literal", + "NewType", + "NoReturn", + ]; + const importStatement = `from typing import (\n ${typingImports.join(",\n ")}\n)`; + if (!allCode.includes(importStatement)) { + const importOp = { + // Add the module at the first line + range: new monaco.Range(1, 1, 1, 1), + text: `${importStatement}\n\n`, + }; + editor.executeEdits("add module", [importOp]); + } + } + private connectLanguageServer() { if (this.languageServerSocket === undefined) { this.languageServerSocket = new WebSocket(getWebsocketUrl("/python-language-server", "3000")); From e0f51d370cbd45593fe0ecfe2380c27c0b18e743 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Sat, 31 Aug 2024 23:15:11 -0400 Subject: [PATCH 02/27] amber --- .../ics/amber/engine/common/AmberConfig.scala | 2 + .../ics/texera/web/TexeraWebApplication.scala | 1 + .../aiassistant/AiAssistantManager.scala | 43 ++++++++++++ .../aiassistant/AiAssistantResource.scala | 70 +++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala create mode 100644 core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala diff --git a/core/amber/src/main/scala/edu/uci/ics/amber/engine/common/AmberConfig.scala b/core/amber/src/main/scala/edu/uci/ics/amber/engine/common/AmberConfig.scala index 6b1c04981ee..a5e3b8b4469 100644 --- a/core/amber/src/main/scala/edu/uci/ics/amber/engine/common/AmberConfig.scala +++ b/core/amber/src/main/scala/edu/uci/ics/amber/engine/common/AmberConfig.scala @@ -105,4 +105,6 @@ object AmberConfig { // JDBC configuration val jdbcConfig: Config = getConfSource.getConfig("jdbc") + // Python language server configuration + val aiAssistantConfig: Config = getConfSource.getConfig("ai-assistant-server") } diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala index 99b8bfd19e7..e05e7e23fd3 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala @@ -258,6 +258,7 @@ class TexeraWebApplication environment.jersey.register(classOf[AdminExecutionResource]) environment.jersey.register(classOf[UserQuotaResource]) environment.jersey.register(classOf[UserDiscussionResource]) + environment.jersey.register(classOf[AiAssistantResource]) } /** diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala new file mode 100644 index 00000000000..62a1ce38739 --- /dev/null +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala @@ -0,0 +1,43 @@ +package edu.uci.ics.texera.web.resource.aiassistant +import edu.uci.ics.amber.engine.common.AmberConfig +import java.net.{HttpURLConnection, URL} +import java.util.logging.Logger + +object AiAssistantManager { + private val logger = Logger.getLogger(getClass.getName) + + private val aiAssistantConfig = AmberConfig.aiAssistantConfig + val assistantType: String = aiAssistantConfig.getString("assistant") + val accountKey: String = aiAssistantConfig.getString("ai-service-key") + val validAIAssistant: Boolean = assistantType match { + case "none" => + false + + case "openai" => + var isKeyValid: Boolean = false + var connection: HttpURLConnection = null + try { + val url = new URL("https://api.openai.com/v1/models") + connection = url.openConnection().asInstanceOf[HttpURLConnection] + connection.setRequestMethod("GET") + connection.setRequestProperty( + "Authorization", + s"Bearer ${accountKey.trim.replaceAll("^\"|\"$", "")}" + ) + val responseCode = connection.getResponseCode + isKeyValid = responseCode == 200 + } catch { + case e: Exception => + isKeyValid = false + logger.warning(s"Error validating OpenAI API key: ${e.getMessage}") + } finally { + if (connection != null) { + connection.disconnect() + } + } + isKeyValid + + case _ => + false + } +} \ No newline at end of file diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala new file mode 100644 index 00000000000..40e873bef2a --- /dev/null +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -0,0 +1,70 @@ +package edu.uci.ics.texera.web.resource + +import edu.uci.ics.texera.web.auth.SessionUser +import edu.uci.ics.texera.web.resource.aiassistant.AiAssistantManager +import io.dropwizard.auth.Auth + +import javax.annotation.security.RolesAllowed +import javax.ws.rs._ + +import javax.ws.rs.core.Response +import java.util.Base64 +import scala.sys.process._ +import java.util.logging.Logger + +@Path("/aiassistant") +class AiAssistantResource { + private val logger = Logger.getLogger(classOf[AiAssistantResource].getName) + + final private lazy val isEnabled = AiAssistantManager.validAIAssistant + + @GET + @RolesAllowed(Array("REGULAR", "ADMIN")) + @Path("/isenabled") + def isAiAssistantEnabled: Boolean = isEnabled + + /** + * To get the type annotation suggestion from OpenAI + */ + @POST + @RolesAllowed(Array("REGULAR", "ADMIN")) + @Path("/getresult") + def getAiResponse(prompt: String, @Auth user: SessionUser): Response = { + val finalPrompt = prompt.replace("\\", "\\\\").replace("\"", "\\\"") + val requestBody = + s""" + |{ + | "model": "gpt-4", + | "messages": [{"role": "user", "content": "$finalPrompt"}], + | "max_tokens": 15 + |} + """.stripMargin + + try { + val url = new java.net.URL("https://api.openai.com/v1/chat/completions") + val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] + connection.setRequestMethod("POST") + connection.setRequestProperty("Authorization", s"Bearer ${AiAssistantManager.accountKey}") + connection.setRequestProperty("Content-Type", "application/json") + connection.setDoOutput(true) + connection.getOutputStream.write(requestBody.getBytes("UTF-8")) + val responseCode = connection.getResponseCode + val responseStream = connection.getInputStream + val responseString = scala.io.Source.fromInputStream(responseStream).mkString + if (responseCode == 200) { + logger.info(s"Response from OpenAI API: $responseString") + } else { + logger.warning(s"Error response from OpenAI API: $responseString") + } + responseStream.close() + connection.disconnect() + Response.status(responseCode).entity(responseString).build() + } catch { + case e: Exception => + logger.warning(s"Exception occurred: ${e.getMessage}") + e.printStackTrace() + Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Error occurred").build() + } + } +} + From 86b366311db42f00634819f3677e1210660085bd Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Sat, 31 Aug 2024 23:50:24 -0400 Subject: [PATCH 03/27] fmt --- .../web/resource/aiassistant/AiAssistantManager.scala | 2 +- .../web/resource/aiassistant/AiAssistantResource.scala | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala index 62a1ce38739..465adf46461 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala @@ -40,4 +40,4 @@ object AiAssistantManager { case _ => false } -} \ No newline at end of file +} diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index 40e873bef2a..3203f737d0e 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -8,8 +8,6 @@ import javax.annotation.security.RolesAllowed import javax.ws.rs._ import javax.ws.rs.core.Response -import java.util.Base64 -import scala.sys.process._ import java.util.logging.Logger @Path("/aiassistant") @@ -24,8 +22,8 @@ class AiAssistantResource { def isAiAssistantEnabled: Boolean = isEnabled /** - * To get the type annotation suggestion from OpenAI - */ + * To get the type annotation suggestion from OpenAI + */ @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/getresult") @@ -67,4 +65,3 @@ class AiAssistantResource { } } } - From bbcc2a347599331232e28ec012951e5dde0ce079 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Sun, 1 Sep 2024 13:58:43 -0400 Subject: [PATCH 04/27] frontend logger --- core/amber/src/main/resources/application.conf | 6 ++++++ .../resource/aiassistant/AiAssistantManager.scala | 4 ---- .../user/ai-assistant/ai-assistant.service.ts | 13 +++++++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/core/amber/src/main/resources/application.conf b/core/amber/src/main/resources/application.conf index 69e0c00188e..ae0f0e615b0 100644 --- a/core/amber/src/main/resources/application.conf +++ b/core/amber/src/main/resources/application.conf @@ -91,3 +91,9 @@ region-plan-generator { enable-cost-based-region-plan-generator = false use-global-search = false } + +ai-assistant-server{ + assistant = "none" + # Put your OpenAI authentication key here + ai-service-key = "" +} diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala index 465adf46461..4ebdc0cd2d6 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala @@ -1,11 +1,8 @@ package edu.uci.ics.texera.web.resource.aiassistant import edu.uci.ics.amber.engine.common.AmberConfig import java.net.{HttpURLConnection, URL} -import java.util.logging.Logger object AiAssistantManager { - private val logger = Logger.getLogger(getClass.getName) - private val aiAssistantConfig = AmberConfig.aiAssistantConfig val assistantType: String = aiAssistantConfig.getString("assistant") val accountKey: String = aiAssistantConfig.getString("ai-service-key") @@ -29,7 +26,6 @@ object AiAssistantManager { } catch { case e: Exception => isKeyValid = false - logger.warning(s"Error validating OpenAI API key: ${e.getMessage}") } finally { if (connection != null) { connection.disconnect() diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index 11fbec9d534..9a9783a0b01 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -14,8 +14,17 @@ export class AiAssistantService { public checkAiAssistantEnabled(): Promise { const apiUrl = `${AI_ASSISTANT_API_BASE_URL}/isenabled`; return firstValueFrom(this.http.get(apiUrl)) - .then(response => (response !== undefined ? response : false)) - .catch(() => false); + .then(response => { + const isEnabled = response !== undefined ? response : false; + console.log( + isEnabled ? "AI Assistant successfully started" : "No AI Assistant or OpenAI authentication key error" + ); + return isEnabled; + }) + .catch(() => { + console.log("No AI Assistant or OpenAI authentication key error"); + return false; + }); } public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Promise { From 980c6b782ba74d2ff904d8966e1f69d003b66ff2 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Sun, 1 Sep 2024 14:25:44 -0400 Subject: [PATCH 05/27] logger to frontend --- .../aiassistant/AiAssistantResource.scala | 8 ------ .../user/ai-assistant/ai-assistant.service.ts | 25 +++++++++++-------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index 3203f737d0e..acbd9170da6 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -8,11 +8,9 @@ import javax.annotation.security.RolesAllowed import javax.ws.rs._ import javax.ws.rs.core.Response -import java.util.logging.Logger @Path("/aiassistant") class AiAssistantResource { - private val logger = Logger.getLogger(classOf[AiAssistantResource].getName) final private lazy val isEnabled = AiAssistantManager.validAIAssistant @@ -49,17 +47,11 @@ class AiAssistantResource { val responseCode = connection.getResponseCode val responseStream = connection.getInputStream val responseString = scala.io.Source.fromInputStream(responseStream).mkString - if (responseCode == 200) { - logger.info(s"Response from OpenAI API: $responseString") - } else { - logger.warning(s"Error response from OpenAI API: $responseString") - } responseStream.close() connection.disconnect() Response.status(responseCode).entity(responseString).build() } catch { case e: Exception => - logger.warning(s"Exception occurred: ${e.getMessage}") e.printStackTrace() Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Error occurred").build() } diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index 9a9783a0b01..ecdce7403f8 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -57,15 +57,18 @@ export class AiAssistantService { - For the first situation: you must return strictly according to the format ": type", without adding any extra characters. No need for an explanation, just the result : type is enough! - For the second situation: you return strictly according to the format " -> type", without adding any extra characters. No need for an explanation, just the result -> type is enough! `; - return firstValueFrom(this.http.post(`${AI_ASSISTANT_API_BASE_URL}/getresult`, { prompt })) - .then(response => { - console.log("Received response from backend:", response); - const result = response.choices[0].message.content.trim(); - return result; - }) - .catch(error => { - console.error("Request to backend failed:", error); - return ""; - }); - } + return firstValueFrom(this.http.post(`${AI_ASSISTANT_API_BASE_URL}/getresult`, { prompt })) + .then(response => { + if (response.choices && response.choices.length > 0) { + return response.choices[0].message.content.trim(); + } else { + console.error("Error from backend:", response.body); + return ""; + } + }) + .catch(error => { + console.error("Request to backend failed:", error); + return ""; + }); + } } From d5a3dede795bdc2059a6900c1d881a89f6b5cd20 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Sun, 1 Sep 2024 15:30:19 -0400 Subject: [PATCH 06/27] yarn fmt --- .../user/ai-assistant/ai-assistant.service.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index ecdce7403f8..0a93911a0ad 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -57,18 +57,18 @@ export class AiAssistantService { - For the first situation: you must return strictly according to the format ": type", without adding any extra characters. No need for an explanation, just the result : type is enough! - For the second situation: you return strictly according to the format " -> type", without adding any extra characters. No need for an explanation, just the result -> type is enough! `; - return firstValueFrom(this.http.post(`${AI_ASSISTANT_API_BASE_URL}/getresult`, { prompt })) - .then(response => { - if (response.choices && response.choices.length > 0) { - return response.choices[0].message.content.trim(); - } else { - console.error("Error from backend:", response.body); - return ""; - } - }) - .catch(error => { - console.error("Request to backend failed:", error); + return firstValueFrom(this.http.post(`${AI_ASSISTANT_API_BASE_URL}/getresult`, { prompt })) + .then(response => { + if (response.choices && response.choices.length > 0) { + return response.choices[0].message.content.trim(); + } else { + console.error("Error from backend:", response.body); return ""; - }); - } + } + }) + .catch(error => { + console.error("Request to backend failed:", error); + return ""; + }); + } } From 9c316b9f6bd4f01d504841985497cc0d2bcea826 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 2 Sep 2024 15:47:08 -0400 Subject: [PATCH 07/27] fmt --- core/amber/src/main/resources/application.conf | 2 +- .../texera/web/resource/aiassistant/AiAssistantManager.scala | 1 - .../texera/web/resource/aiassistant/AiAssistantResource.scala | 3 --- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/core/amber/src/main/resources/application.conf b/core/amber/src/main/resources/application.conf index f844cb6ed72..460a60a9310 100644 --- a/core/amber/src/main/resources/application.conf +++ b/core/amber/src/main/resources/application.conf @@ -98,4 +98,4 @@ ai-assistant-server{ ai-service-key = "" # Put your Ai service url here (If you are using OpenAI, then the url should be "https://api.openai.com/v1") ai-service-url = "" -} +} \ No newline at end of file diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala index bb61292bbfb..00e334a17aa 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala @@ -3,7 +3,6 @@ import edu.uci.ics.amber.engine.common.AmberConfig import java.net.{HttpURLConnection, URL} object AiAssistantManager { - private val aiAssistantConfig = AmberConfig.aiAssistantConfig.getOrElse( throw new Exception("ai-assistant-server configuration is missing in application.conf") ) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index caf78b1ab9d..f1f6812d701 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -1,12 +1,9 @@ package edu.uci.ics.texera.web.resource - import edu.uci.ics.texera.web.auth.SessionUser import edu.uci.ics.texera.web.resource.aiassistant.AiAssistantManager import io.dropwizard.auth.Auth - import javax.annotation.security.RolesAllowed import javax.ws.rs._ - import javax.ws.rs.core.Response @Path("/aiassistant") From c04d4984a7b6ba686c3533a378704573f4f7ac07 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 2 Sep 2024 15:54:02 -0400 Subject: [PATCH 08/27] add action --- .../user/ai-assistant/ai-assistant.service.ts | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index 66534152827..a4c667b70be 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -27,4 +27,49 @@ export class AiAssistantService { return "NoAiAssistant"; }); } + + public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Promise { + const prompt = ` + Your task is to analyze the given Python code and provide only the type annotation as stated in the instructions. + Instructions: + - The provided code will only be one of the 2 situations below: + - First situation: The input is not start with "def". If the provided code only contains variable, output the result in the format ":type". + - Second situation: The input is start with "def". If the provided code starts with "def" (a longer line than just a variable, indicative of a function or method), output the result in the format " -> type". + - The type should only be one word, such as "str", "int", etc. + Examples: + - First situation: + - Provided code is "name", then the output may be : str + - Provided code is "age", then the output may be : int + - Provided code is "data", then the output may be : Tuple[int, str] + - Provided code is "new_user", then the output may be : User + - A special case: provided code is "self" and the context is something like "def __init__(self, username :str , age :int)", if the user requires the type annotation for the first parameter "self", then you should generate nothing. + - Second situation: (actual output depends on the complete code content) + - Provided code is "process_data(data: List[Tuple[int, str]], config: Dict[str, Union[int, str]])", then the output may be -> Optional[str] + - Provided code is "def add(a: int, b: int)", then the output may be -> int + Counterexamples: + - Provided code is "def __init__(self, username: str, age: int)" and you generate the result: + The result is The provided code is "def __init__(self, username: str, age: int)", so it fits the second situation, which means the result should be in " -> type" format. However, the __init__ method in Python doesn't return anything or in other words, it implicitly returns None. Hence the correct type hint would be: -> None. + I don't want this result! The correct result you should generate is -> None for this counter case. + Details: + - Provided code: ${code} + - Line number of the provided code in the complete code context: ${lineNumber} + - Complete code context: ${allcode} + Important: (you must follow!!) + - For the first situation: you must return strictly according to the format ": type", without adding any extra characters. No need for an explanation, just the result : type is enough! + - For the second situation: you return strictly according to the format " -> type", without adding any extra characters. No need for an explanation, just the result -> type is enough! + `; + return firstValueFrom(this.http.post(`${AI_ASSISTANT_API_BASE_URL}/getresult`, { prompt })) + .then(response => { + if (response.choices && response.choices.length > 0) { + return response.choices[0].message.content.trim(); + } else { + console.error("Error from backend:", response.body); + return ""; + } + }) + .catch(error => { + console.error("Request to backend failed:", error); + return ""; + }); + } } From 081113f5d7ae1dc440823a07cf180218378de432 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 2 Sep 2024 16:10:13 -0400 Subject: [PATCH 09/27] debug --- .../web/resource/{aiassistant => }/AiAssistantResource.scala | 2 +- .../component/code-editor-dialog/code-editor.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename core/amber/src/main/scala/edu/uci/ics/texera/web/resource/{aiassistant => }/AiAssistantResource.scala (95%) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/AiAssistantResource.scala similarity index 95% rename from core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala rename to core/amber/src/main/scala/edu/uci/ics/texera/web/resource/AiAssistantResource.scala index f1f6812d701..1de6750c314 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/AiAssistantResource.scala @@ -32,7 +32,7 @@ class AiAssistantResource { """.stripMargin try { - val url = new java.net.URL("https://api.openai.com/v1/chat/completions") + val url = new java.net.URL(s"${AiAssistantManager.sharedUrl}/chat/completions") val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] connection.setRequestMethod("POST") connection.setRequestProperty("Authorization", s"Bearer ${AiAssistantManager.accountKey}") diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index b0fa0f3683b..7f9e2c40c26 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -182,7 +182,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.editor = editor; // Check if the AI provider is "openai" - if (await this.aiAssistantService.checkAiAssistantEnabled()) { + if (await this.aiAssistantService.checkAiAssistantEnabled() == "OpenAI") { // Add all needed modules for add type annotation this.addAnnotationModule(editor); From 37f95732abb526116cee7aaf2a697b71d6e63b2e Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 2 Sep 2024 16:13:45 -0400 Subject: [PATCH 10/27] debug --- .../web/resource/{ => aiassistant}/AiAssistantResource.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename core/amber/src/main/scala/edu/uci/ics/texera/web/resource/{ => aiassistant}/AiAssistantResource.scala (96%) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala similarity index 96% rename from core/amber/src/main/scala/edu/uci/ics/texera/web/resource/AiAssistantResource.scala rename to core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index 1de6750c314..eae923b168b 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -15,8 +15,8 @@ class AiAssistantResource { def isAiAssistantEnable: String = isEnabled /** - * To get the type annotation suggestion from OpenAI - */ + * To get the type annotation suggestion from OpenAI + */ @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/getresult") From 388325ad8caa36e449bf755dff69d7e6f08873c0 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 2 Sep 2024 16:23:43 -0400 Subject: [PATCH 11/27] fmt --- .../texera/web/resource/aiassistant/AiAssistantResource.scala | 4 ++-- .../component/code-editor-dialog/code-editor.component.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index eae923b168b..1de6750c314 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -15,8 +15,8 @@ class AiAssistantResource { def isAiAssistantEnable: String = isEnabled /** - * To get the type annotation suggestion from OpenAI - */ + * To get the type annotation suggestion from OpenAI + */ @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/getresult") diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 7f9e2c40c26..b3289cab459 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -182,7 +182,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.editor = editor; // Check if the AI provider is "openai" - if (await this.aiAssistantService.checkAiAssistantEnabled() == "OpenAI") { + if ((await this.aiAssistantService.checkAiAssistantEnabled()) == "OpenAI") { // Add all needed modules for add type annotation this.addAnnotationModule(editor); From acf6a05df5be7b31c4dbc691e5fc02a345007278 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Tue, 3 Sep 2024 13:22:29 -0400 Subject: [PATCH 12/27] change "Ai" to "AI" --- .../uci/ics/texera/web/TexeraWebApplication.scala | 2 +- ...stantManager.scala => AIAssistantManager.scala} | 10 +++++----- ...antResource.scala => AIAssistantResource.scala} | 14 +++++++------- .../user/ai-assistant/ai-assistant.service.ts | 8 ++++---- .../code-editor-dialog/code-editor.component.ts | 6 +++--- 5 files changed, 20 insertions(+), 20 deletions(-) rename core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/{AiAssistantManager.scala => AIAssistantManager.scala} (92%) rename core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/{AiAssistantResource.scala => AIAssistantResource.scala} (82%) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala index 28802934329..2d7db7dbc0f 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/TexeraWebApplication.scala @@ -260,7 +260,7 @@ class TexeraWebApplication environment.jersey.register(classOf[AdminExecutionResource]) environment.jersey.register(classOf[UserQuotaResource]) environment.jersey.register(classOf[UserDiscussionResource]) - environment.jersey.register(classOf[AiAssistantResource]) + environment.jersey.register(classOf[AIAssistantResource]) } /** diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantManager.scala similarity index 92% rename from core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala rename to core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantManager.scala index 00e334a17aa..2b1d4637beb 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantManager.scala @@ -2,7 +2,7 @@ package edu.uci.ics.texera.web.resource.aiassistant import edu.uci.ics.amber.engine.common.AmberConfig import java.net.{HttpURLConnection, URL} -object AiAssistantManager { +object AIAssistantManager { private val aiAssistantConfig = AmberConfig.aiAssistantConfig.getOrElse( throw new Exception("ai-assistant-server configuration is missing in application.conf") ) @@ -26,11 +26,11 @@ object AiAssistantManager { if (responseCode == 200) { "OpenAI" } else { - "NoAiAssistant" + "NoAIAssistant" } } catch { case e: Exception => - "NoAiAssistant" + "NoAIAssistant" } finally { if (connection != null) { connection.disconnect() @@ -40,12 +40,12 @@ object AiAssistantManager { val validAIAssistant: String = assistantType match { case "none" => - "NoAiAssistant" + "NoAIAssistant" case "openai" => initOpenAI() case _ => - "NoAiAssistant" + "NoAIAssistant" } } diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala similarity index 82% rename from core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala rename to core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala index 1de6750c314..fd9a7f552f9 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala @@ -1,18 +1,18 @@ package edu.uci.ics.texera.web.resource import edu.uci.ics.texera.web.auth.SessionUser -import edu.uci.ics.texera.web.resource.aiassistant.AiAssistantManager +import edu.uci.ics.texera.web.resource.aiassistant.AIAssistantManager import io.dropwizard.auth.Auth import javax.annotation.security.RolesAllowed import javax.ws.rs._ import javax.ws.rs.core.Response @Path("/aiassistant") -class AiAssistantResource { - final private lazy val isEnabled = AiAssistantManager.validAIAssistant +class AIAssistantResource { + final private lazy val isEnabled = AIAssistantManager.validAIAssistant @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/isenabled") - def isAiAssistantEnable: String = isEnabled + def isAIAssistantEnable: String = isEnabled /** * To get the type annotation suggestion from OpenAI @@ -20,7 +20,7 @@ class AiAssistantResource { @POST @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/getresult") - def getAiResponse(prompt: String, @Auth user: SessionUser): Response = { + def getAIResponse(prompt: String, @Auth user: SessionUser): Response = { val finalPrompt = prompt.replace("\\", "\\\\").replace("\"", "\\\"") val requestBody = s""" @@ -32,10 +32,10 @@ class AiAssistantResource { """.stripMargin try { - val url = new java.net.URL(s"${AiAssistantManager.sharedUrl}/chat/completions") + val url = new java.net.URL(s"${AIAssistantManager.sharedUrl}/chat/completions") val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] connection.setRequestMethod("POST") - connection.setRequestProperty("Authorization", s"Bearer ${AiAssistantManager.accountKey}") + connection.setRequestProperty("Authorization", s"Bearer ${AIAssistantManager.accountKey}") connection.setRequestProperty("Content-Type", "application/json") connection.setDoOutput(true) connection.getOutputStream.write(requestBody.getBytes("UTF-8")) diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index a4c667b70be..0160b530300 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -8,14 +8,14 @@ export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassi @Injectable({ providedIn: "root", }) -export class AiAssistantService { +export class AIAssistantService { constructor(private http: HttpClient) {} - public checkAiAssistantEnabled(): Promise { + public checkAIAssistantEnabled(): Promise { const apiUrl = `${AI_ASSISTANT_API_BASE_URL}/isenabled`; return firstValueFrom(this.http.get(apiUrl, { responseType: "text" })) .then(response => { - const isEnabled = response !== undefined ? response : "NoAiAssistant"; + const isEnabled = response !== undefined ? response : "NoAIAssistant"; console.log( isEnabled === "OpenAI" ? "AI Assistant successfully started" @@ -24,7 +24,7 @@ export class AiAssistantService { return isEnabled; }) .catch(() => { - return "NoAiAssistant"; + return "NoAIAssistant"; }); } diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index b3289cab459..3be461052b8 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -17,7 +17,7 @@ import { isUndefined } from "lodash"; import { CloseAction, ErrorAction } from "vscode-languageclient/lib/common/client.js"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { FormControl } from "@angular/forms"; -import { AiAssistantService } from "../../../dashboard/service/user/ai-assistant/ai-assistant.service"; +import { AIAssistantService } from "../../../dashboard/service/user/ai-assistant/ai-assistant.service"; /** * CodeEditorComponent is the content of the dialogue invoked by CodeareaCustomTemplateComponent. @@ -74,7 +74,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy private workflowActionService: WorkflowActionService, private workflowVersionService: WorkflowVersionService, public coeditorPresenceService: CoeditorPresenceService, - private aiAssistantService: AiAssistantService + private aiAssistantService: AIAssistantService ) { const currentOperatorId = this.workflowActionService.getJointGraphWrapper().getCurrentHighlightedOperatorIDs()[0]; const operatorType = this.workflowActionService.getTexeraGraph().getOperator(currentOperatorId).operatorType; @@ -182,7 +182,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.editor = editor; // Check if the AI provider is "openai" - if ((await this.aiAssistantService.checkAiAssistantEnabled()) == "OpenAI") { + if ((await this.aiAssistantService.checkAIAssistantEnabled()) == "OpenAI") { // Add all needed modules for add type annotation this.addAnnotationModule(editor); From 4e0b697d01eed21fcfc79d15509807af2d4eda1e Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 9 Sep 2024 01:20:23 -0400 Subject: [PATCH 13/27] move prompt from frontend to backend --- .../aiassistant/AIAssistantResource.scala | 74 +++++++++++++++---- .../user/ai-assistant/ai-assistant.service.ts | 37 ++-------- 2 files changed, 67 insertions(+), 44 deletions(-) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala index fd9a7f552f9..30b77d31641 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala @@ -5,6 +5,11 @@ import io.dropwizard.auth.Auth import javax.annotation.security.RolesAllowed import javax.ws.rs._ import javax.ws.rs.core.Response +import javax.ws.rs.Consumes +import javax.ws.rs.core.MediaType +import play.api.libs.json.{Json, JsValue} + +case class AIAssistantRequest(code: String, lineNumber: Int, allcode: String) @Path("/aiassistant") class AIAssistantResource { @@ -19,18 +24,25 @@ class AIAssistantResource { */ @POST @RolesAllowed(Array("REGULAR", "ADMIN")) - @Path("/getresult") - def getAIResponse(prompt: String, @Auth user: SessionUser): Response = { - val finalPrompt = prompt.replace("\\", "\\\\").replace("\"", "\\\"") - val requestBody = - s""" - |{ - | "model": "gpt-4", - | "messages": [{"role": "user", "content": "$finalPrompt"}], - | "max_tokens": 15 - |} - """.stripMargin + @Path("/annotationresult") + @Consumes(Array(MediaType.APPLICATION_JSON)) + def getAnnotation( + request: AIAssistantRequest, + @Auth user: SessionUser + ): Response = { + val finalPrompt = generatePrompt(request.code, request.lineNumber, request.allcode) + val requestBodyJson: JsValue = Json.obj( + "model" -> "gpt-4", + "messages" -> Json.arr( + Json.obj( + "role" -> "user", + "content" -> finalPrompt + ) + ), + "max_tokens" -> 15 + ) + val requestBodyString = Json.stringify(requestBodyJson) try { val url = new java.net.URL(s"${AIAssistantManager.sharedUrl}/chat/completions") val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] @@ -38,9 +50,13 @@ class AIAssistantResource { connection.setRequestProperty("Authorization", s"Bearer ${AIAssistantManager.accountKey}") connection.setRequestProperty("Content-Type", "application/json") connection.setDoOutput(true) - connection.getOutputStream.write(requestBody.getBytes("UTF-8")) + connection.getOutputStream.write(requestBodyString.getBytes("UTF-8")) val responseCode = connection.getResponseCode - val responseStream = connection.getInputStream + val responseStream = if (responseCode >= 200 && responseCode < 300) { + connection.getInputStream + } else { + connection.getErrorStream + } val responseString = scala.io.Source.fromInputStream(responseStream).mkString responseStream.close() connection.disconnect() @@ -51,4 +67,36 @@ class AIAssistantResource { Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Error occurred").build() } } + + // Helper function to get the type annotation + def generatePrompt(code: String, lineNumber: Int, allcode: String): String = { + s""" + |Your task is to analyze the given Python code and provide only the type annotation as stated in the instructions. + |Instructions: + |- The provided code will only be one of the 2 situations below: + |- First situation: The input is not start with "def". If the provided code only contains variable, output the result in the format ":type". + |- Second situation: The input is start with "def". If the provided code starts with "def" (a longer line than just a variable, indicative of a function or method), output the result in the format " -> type". + |- The type should only be one word, such as "str", "int", etc. + |Examples: + |- First situation: + | - Provided code is "name", then the output may be : str + | - Provided code is "age", then the output may be : int + | - Provided code is "data", then the output may be : Tuple[int, str] + | - Provided code is "new_user", then the output may be : User + | - A special case: provided code is "self" and the context is something like "def __init__(self, username :str , age :int)", if the user requires the type annotation for the first parameter "self", then you should generate nothing. + |- Second situation: (actual output depends on the complete code content) + | - Provided code is "process_data(data: List[Tuple[int, str]], config: Dict[str, Union[int, str]])", then the output may be -> Optional[str] + | - Provided code is "def add(a: int, b: int)", then the output may be -> int + |Counterexamples: + | - Provided code is "def __init__(self, username: str, age: int)" and you generate the result: + | The result is The provided code is "def __init__(self, username: str, age: int)", so it fits the second situation, which means the result should be in " -> type" format. However, the __init__ method in Python doesn't return anything or in other words, it implicitly returns None. Hence the correct type hint would be: -> None. + |Details: + |- Provided code: $code + |- Line number of the provided code in the complete code context: $lineNumber + |- Complete code context: $allcode + |Important: (you must follow!!) + |- For the first situation: you must return strictly according to the format ": type", without adding any extra characters. No need for an explanation, just the result : type is enough! + |- For the second situation: you return strictly according to the format " -> type", without adding any extra characters. No need for an explanation, just the result -> type is enough! + """.stripMargin + } } diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index 0160b530300..c062334b8eb 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; -import { HttpClient } from "@angular/common/http"; +import { HttpClient, HttpHeaders } from "@angular/common/http"; export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassistant`; @@ -29,36 +29,11 @@ export class AIAssistantService { } public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Promise { - const prompt = ` - Your task is to analyze the given Python code and provide only the type annotation as stated in the instructions. - Instructions: - - The provided code will only be one of the 2 situations below: - - First situation: The input is not start with "def". If the provided code only contains variable, output the result in the format ":type". - - Second situation: The input is start with "def". If the provided code starts with "def" (a longer line than just a variable, indicative of a function or method), output the result in the format " -> type". - - The type should only be one word, such as "str", "int", etc. - Examples: - - First situation: - - Provided code is "name", then the output may be : str - - Provided code is "age", then the output may be : int - - Provided code is "data", then the output may be : Tuple[int, str] - - Provided code is "new_user", then the output may be : User - - A special case: provided code is "self" and the context is something like "def __init__(self, username :str , age :int)", if the user requires the type annotation for the first parameter "self", then you should generate nothing. - - Second situation: (actual output depends on the complete code content) - - Provided code is "process_data(data: List[Tuple[int, str]], config: Dict[str, Union[int, str]])", then the output may be -> Optional[str] - - Provided code is "def add(a: int, b: int)", then the output may be -> int - Counterexamples: - - Provided code is "def __init__(self, username: str, age: int)" and you generate the result: - The result is The provided code is "def __init__(self, username: str, age: int)", so it fits the second situation, which means the result should be in " -> type" format. However, the __init__ method in Python doesn't return anything or in other words, it implicitly returns None. Hence the correct type hint would be: -> None. - I don't want this result! The correct result you should generate is -> None for this counter case. - Details: - - Provided code: ${code} - - Line number of the provided code in the complete code context: ${lineNumber} - - Complete code context: ${allcode} - Important: (you must follow!!) - - For the first situation: you must return strictly according to the format ": type", without adding any extra characters. No need for an explanation, just the result : type is enough! - - For the second situation: you return strictly according to the format " -> type", without adding any extra characters. No need for an explanation, just the result -> type is enough! - `; - return firstValueFrom(this.http.post(`${AI_ASSISTANT_API_BASE_URL}/getresult`, { prompt })) + const headers = new HttpHeaders({ "Content-Type": "application/json" }); + const requestBody = { code, lineNumber, allcode }; + return firstValueFrom( + this.http.post(`${AI_ASSISTANT_API_BASE_URL}/annotationresult`, requestBody, { headers }) + ) .then(response => { if (response.choices && response.choices.length > 0) { return response.choices[0].message.content.trim(); From 240b0683bb8b47182959365c481dd8daba200a82 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <143851082+IamtherealBrian@users.noreply.github.com> Date: Mon, 9 Sep 2024 11:49:33 -0400 Subject: [PATCH 14/27] Start pyright language server with ts file and deprecate the mjs file (#2823) Use `main.ts` which done by `Typefox` to start pyright instead of `startPyright.mjs`. The work done by `Typefox` can be referred to: https://github.com/TypeFox/monaco-languageclient/tree/main/packages/examples/src/python --------- Co-authored-by: Yicong Huang <17627829+Yicong-Huang@users.noreply.github.com> Co-authored-by: Yicong Huang --- .../PythonLanguageServerManager.scala | 55 +- core/pyright-language-server/README.md | 3 + core/pyright-language-server/package.json | 27 +- .../pythonLanguageServerConfig.json | 5 - .../src/language-server-runner.ts | 37 + core/pyright-language-server/src/main.ts | 42 + .../src/pythonLanguageServerConfig.json | 5 + .../src/server-commons.ts | 105 +++ .../src/types/hocon-parser.d.ts | 8 + core/pyright-language-server/startPyright.mjs | 87 --- core/pyright-language-server/tsconfig.json | 23 + core/pyright-language-server/yarn.lock | 729 ++++++++++++++++++ 12 files changed, 990 insertions(+), 136 deletions(-) create mode 100644 core/pyright-language-server/README.md delete mode 100644 core/pyright-language-server/pythonLanguageServerConfig.json create mode 100644 core/pyright-language-server/src/language-server-runner.ts create mode 100644 core/pyright-language-server/src/main.ts create mode 100644 core/pyright-language-server/src/pythonLanguageServerConfig.json create mode 100644 core/pyright-language-server/src/server-commons.ts create mode 100644 core/pyright-language-server/src/types/hocon-parser.d.ts delete mode 100644 core/pyright-language-server/startPyright.mjs create mode 100644 core/pyright-language-server/tsconfig.json create mode 100644 core/pyright-language-server/yarn.lock diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/pythonlanguageserver/PythonLanguageServerManager.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/pythonlanguageserver/PythonLanguageServerManager.scala index b565faa33d8..9f2d5726486 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/pythonlanguageserver/PythonLanguageServerManager.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/pythonlanguageserver/PythonLanguageServerManager.scala @@ -5,6 +5,8 @@ import edu.uci.ics.amber.engine.common.AmberConfig import java.util.logging.Logger import scala.sys.process._ +import java.io.File + object PythonLanguageServerManager { private val pythonLanguageServerConfig = AmberConfig.pythonLanguageServerConfig val pythonLanguageServerProvider: String = pythonLanguageServerConfig.getString("provider") @@ -21,29 +23,34 @@ object PythonLanguageServerManager { // The situation when the provider is Pyright case "pyright" => logger.info("Starting Pyright...") - releasePort(pythonLanguageServerPort) var tryCount = 0 var started = false while (tryCount < MAX_TRY_COUNT && !started) { try { - val result = { - Process("node ../pyright-language-server/startPyright.mjs").run( - ProcessLogger(_ => (), err => logger.warning(s"Error during Pyright startup: $err")) + val command = Seq("node", "--loader", "ts-node/esm", "src/main.ts") + val workingDir = new File("C:/Users/Owner/new/texera/core/pyright-language-server") + val exitCode = Process(command, workingDir).! + if (exitCode == 0) { + logger.info( + s"Pyright language server started successfully on port $pythonLanguageServerPort" + ) + started = true + } else { + logger.warning( + s"Pyright failed to start with exit code: $exitCode (attempt ${tryCount + 1}/$MAX_TRY_COUNT)" ) } - logger.info(s"Pyright language server is running on port $pythonLanguageServerPort") - started = true } catch { case e: Exception => logger.warning( s"Failed to start Pyright (attempt ${tryCount + 1}/$MAX_TRY_COUNT): ${e.getMessage}" ) - if (tryCount < MAX_TRY_COUNT - 1) { - logger.info(s"Retrying in $UNIT_WAIT_TIME_MS ms...") - Thread.sleep(UNIT_WAIT_TIME_MS) - } - tryCount += 1 } + if (!started && tryCount < MAX_TRY_COUNT - 1) { + logger.info(s"Retrying in $UNIT_WAIT_TIME_MS ms...") + Thread.sleep(UNIT_WAIT_TIME_MS) + } + tryCount += 1 } if (!started) { logger.warning(s"Failed to start Pyright after $MAX_TRY_COUNT attempts. Abort!") @@ -52,7 +59,6 @@ object PythonLanguageServerManager { // The situation when the provider is Pylsp case "pylsp" => logger.info("Starting Pylsp...") - releasePort(pythonLanguageServerPort) var tryCount = 0 var started = false while (tryCount < MAX_TRY_COUNT && !started) { @@ -84,29 +90,4 @@ object PythonLanguageServerManager { logger.warning(s"Unknown language server: $pythonLanguageServerPort") } } - - private def releasePort(port: Int): Unit = { - val findCommand = Seq("cmd", "/c", s"netstat -aon | findstr $port") - try { - val res = findCommand.!! - if (res.contains("LISTENING")) { - val lines = res.split("\\r?\\n") - val pidLine = lines.find(_.contains("LISTENING")).getOrElse("") - val pid = pidLine.split("\\s+").last - - val killCommand = Seq("cmd", "/c", s"taskkill /F /PID $pid") - val killResult = killCommand.! - if (killResult == 0) { - logger.info(s"Successfully killed the process on port: $port") - } else { - logger.warning(s"Failed to kill the process on port: $port with exit code $killResult") - } - } else { - logger.info(s"Port $port is free to use.") - } - } catch { - case e: Exception => - logger.warning(s"Error while releasing port $port: ${e.getMessage}") - } - } } diff --git a/core/pyright-language-server/README.md b/core/pyright-language-server/README.md new file mode 100644 index 00000000000..d180061557e --- /dev/null +++ b/core/pyright-language-server/README.md @@ -0,0 +1,3 @@ +This folder is for the Pyright language server. + +The documents in src which use to start the Pyright language server is done by Typefox, you can refer to "https://github.com/TypeFox/monaco-languageclient/tree/main/packages/examples/src/python" \ No newline at end of file diff --git a/core/pyright-language-server/package.json b/core/pyright-language-server/package.json index f069bd49fd7..0310245a89d 100644 --- a/core/pyright-language-server/package.json +++ b/core/pyright-language-server/package.json @@ -1,13 +1,26 @@ { "name": "pyright-language-server", "version": "0.0.1", - "main": "startPyright.mjs", - "license": "Apache 2.0", + "main": "src/main.ts", + "license": "Apache-2.0", + "type": "module", "dependencies": { - "express": "^4.19.2", - "hocon-parser": "^1.0.1", - "pyright": "^1.1.377", - "vscode-ws-jsonrpc": "^3.3.2", - "ws": "^8.18.0" + "express": "4.19.2", + "hocon-parser": "1.0.1", + "hoconjs": "1.0.0", + "pyright": "1.1.377", + "typescript": "5.5.4", + "vscode-languageserver": "9.0.1", + "vscode-ws-jsonrpc": "3.3.2", + "ws": "8.18.0" + }, + "devDependencies": { + "@types/express": "4.17.21", + "@types/node": "22.5.4", + "@types/ws": "8.5.12", + "ts-node": "10.9.2" + }, + "scripts": { + "start": "node --loader ts-node/esm src/main.ts" } } diff --git a/core/pyright-language-server/pythonLanguageServerConfig.json b/core/pyright-language-server/pythonLanguageServerConfig.json deleted file mode 100644 index 58f03875396..00000000000 --- a/core/pyright-language-server/pythonLanguageServerConfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "languageServerDir": "node_modules/pyright/dist", - "languageServerFile": "pyright-langserver.js", - "amberConfigFilePath": "../amber/src/main/resources/application.conf" -} diff --git a/core/pyright-language-server/src/language-server-runner.ts b/core/pyright-language-server/src/language-server-runner.ts new file mode 100644 index 00000000000..43c660d3121 --- /dev/null +++ b/core/pyright-language-server/src/language-server-runner.ts @@ -0,0 +1,37 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ +// The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/common/node/language-server-runner.ts + + +import { WebSocketServer } from "ws"; +import { Server } from 'node:http'; +import express from 'express'; +import { getLocalDirectory, LanguageServerRunConfig, upgradeWsServer } from './server-commons.ts'; + +/** LSP server runner */ +export const runLanguageServer = ( + languageServerRunConfig: LanguageServerRunConfig +) => { + process.on('uncaughtException', err => { + console.error('Uncaught Exception: ', err.toString()); + if (err.stack !== undefined) { + console.error(err.stack); + } + }); + + // create the express application + const app = express(); + // server the static content, i.e. index.html + const dir = getLocalDirectory(import.meta.url); + app.use(express.static(dir)); + // start the http server + const httpServer: Server = app.listen(languageServerRunConfig.serverPort); + const wss = new WebSocketServer(languageServerRunConfig.wsServerOptions); + // create the web socket + upgradeWsServer(languageServerRunConfig, { + server: httpServer, + wss + }); +}; \ No newline at end of file diff --git a/core/pyright-language-server/src/main.ts b/core/pyright-language-server/src/main.ts new file mode 100644 index 00000000000..26ca49353f0 --- /dev/null +++ b/core/pyright-language-server/src/main.ts @@ -0,0 +1,42 @@ +//The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/python/server/main.ts + +import { resolve } from "node:path"; +import { runLanguageServer } from "./language-server-runner.ts"; +import { getLocalDirectory, LanguageName } from "./server-commons.ts"; +import fs from "fs"; +import hoconParser from "hocon-parser"; + +const runPythonServer = (baseDir: string, relativeDir: string, serverPort: number) => { + const processRunPath = resolve(baseDir, relativeDir); + runLanguageServer({ + serverName: "PYRIGHT", + pathName: clientPathName, + serverPort: serverPort, + runCommand: LanguageName.node, + runCommandArgs: [ + processRunPath, + "--stdio", + ], + wsServerOptions: { + noServer: true, + perMessageDeflate: false, + clientTracking: true, + }, + }); +}; + + +const baseDir = getLocalDirectory(import.meta.url); +const relativeDir = "./node_modules/pyright/dist/pyright-langserver.js"; + +const configFilePath = resolve(baseDir, "pythonLanguageServerConfig.json"); +const config = JSON.parse(fs.readFileSync(configFilePath, "utf-8")); + +const amberConfigFilePath = resolve(baseDir, config.amberConfigFilePath); +const amberConfigContent = fs.readFileSync(amberConfigFilePath, "utf-8"); +const applicationConfig = hoconParser(amberConfigContent) as Record; + +const pythonLanguageServerPort = applicationConfig["python-language-server"].port; +const clientPathName = config.clientPathName + +runPythonServer(baseDir, relativeDir, pythonLanguageServerPort); \ No newline at end of file diff --git a/core/pyright-language-server/src/pythonLanguageServerConfig.json b/core/pyright-language-server/src/pythonLanguageServerConfig.json new file mode 100644 index 00000000000..f51d8a5127c --- /dev/null +++ b/core/pyright-language-server/src/pythonLanguageServerConfig.json @@ -0,0 +1,5 @@ +{ + "languageServerDir": "../node_modules/pyright/dist", + "amberConfigFilePath": "../../amber/src/main/resources/application.conf", + "clientPathName": "/python-language-server" +} diff --git a/core/pyright-language-server/src/server-commons.ts b/core/pyright-language-server/src/server-commons.ts new file mode 100644 index 00000000000..38505fcd4ff --- /dev/null +++ b/core/pyright-language-server/src/server-commons.ts @@ -0,0 +1,105 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) 2024 TypeFox and others. + * Licensed under the MIT License. See LICENSE in the package root for license information. + * ------------------------------------------------------------------------------------------ */ +//The source file can be referred to: https://github.com/TypeFox/monaco-languageclient/blob/main/packages/examples/src/common/node/server-commons.ts +import { ServerOptions, WebSocketServer } from "ws"; +import { IncomingMessage, Server } from "node:http"; +import { fileURLToPath, URL } from "node:url"; +import { Socket } from "node:net"; +import { dirname } from "node:path"; +import { IWebSocket, WebSocketMessageReader, WebSocketMessageWriter } from "vscode-ws-jsonrpc"; +import { createConnection, createServerProcess, forward } from "vscode-ws-jsonrpc/server"; +import { InitializeParams, InitializeRequest, Message } from "vscode-languageserver"; +import * as cp from "child_process"; + +export enum LanguageName { + /** https://nodejs.org/api/cli.html */ + node = "node", + /** https://docs.oracle.com/en/java/javase/21/docs/specs/man/java.html */ + java = "java" +} + +export interface LanguageServerRunConfig { + serverName: string; + pathName: string; + serverPort: number; + runCommand: LanguageName | string; + runCommandArgs: string[]; + wsServerOptions: ServerOptions, + spawnOptions?: cp.SpawnOptions; +} + +/** + * start the language server inside the current process + */ +export const launchLanguageServer = (runconfig: LanguageServerRunConfig, socket: IWebSocket) => { + const { serverName, runCommand, runCommandArgs, spawnOptions } = runconfig; + // start the language server as an external process + const reader = new WebSocketMessageReader(socket); + const writer = new WebSocketMessageWriter(socket); + const socketConnection = createConnection(reader, writer, () => socket.dispose()); + const serverConnection = createServerProcess(serverName, runCommand, runCommandArgs, spawnOptions); + if (serverConnection) { + forward(socketConnection, serverConnection, message => { + if (Message.isRequest(message)) { + console.log(`${serverName} Server received:`); + console.log(message); + if (message.method === InitializeRequest.type.method) { + const initializeParams = message.params as InitializeParams; + initializeParams.processId = process.pid; + } + } + if (Message.isResponse(message)) { + console.log(`${serverName} Server sent:`); + console.log(message); + } + return message; + }); + } +}; + +export const upgradeWsServer = (runconfig: LanguageServerRunConfig, + config: { + server: Server, + wss: WebSocketServer + }) => { + config.server.on("upgrade", (request: IncomingMessage, socket: Socket, head: Buffer) => { + const baseURL = `http://${request.headers.host}/`; + const pathName = request.url !== undefined ? new URL(request.url, baseURL).pathname : undefined; + if (pathName === runconfig.pathName) { + config.wss.handleUpgrade(request, socket, head, webSocket => { + const socket: IWebSocket = { + send: content => webSocket.send(content, error => { + if (error) { + throw error; + } + }), + onMessage: cb => webSocket.on("message", (data) => { + console.log(data.toString()); + cb(data); + }), + onError: cb => webSocket.on("error", cb), + onClose: cb => webSocket.on("close", cb), + dispose: () => webSocket.close(), + }; + // launch the server when the web socket is opened + if (webSocket.readyState === webSocket.OPEN) { + launchLanguageServer(runconfig, socket); + } else { + webSocket.on("open", () => { + launchLanguageServer(runconfig, socket); + }); + } + }); + } + }); +}; + +/** + * Solves: __dirname is not defined in ES module scope + */ +export const getLocalDirectory = (referenceUrl: string | URL) => { + const __filename = fileURLToPath(referenceUrl); + return dirname(__filename); +}; \ No newline at end of file diff --git a/core/pyright-language-server/src/types/hocon-parser.d.ts b/core/pyright-language-server/src/types/hocon-parser.d.ts new file mode 100644 index 00000000000..746700409c9 --- /dev/null +++ b/core/pyright-language-server/src/types/hocon-parser.d.ts @@ -0,0 +1,8 @@ +declare module 'hocon-parser' { + /** + * The module itself is callable, accepting a string (HOCON config) and returning a parsed object. + */ + function hoconParser(input: string): any; + + export = hoconParser; +} \ No newline at end of file diff --git a/core/pyright-language-server/startPyright.mjs b/core/pyright-language-server/startPyright.mjs deleted file mode 100644 index 9e327a09914..00000000000 --- a/core/pyright-language-server/startPyright.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import express from 'express'; -import { WebSocketServer } from 'ws'; -import { fileURLToPath } from 'url'; -import { dirname, resolve} from 'path'; -import { WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'; -import { createConnection, createServerProcess, forward } from 'vscode-ws-jsonrpc/server'; -import hocon from 'hocon-parser'; -import fs from 'fs'; -import * as path from "node:path"; - -// To get the absolute path of this file -const absolutePath = fileURLToPath(import.meta.url); -// To get the absolute path of this file except for the file name -const dir = dirname(absolutePath); - -// To get the config to avoid hard code path -const configFilePath = path.resolve(dir, 'pythonLanguageServerConfig.json'); -const config = JSON.parse(fs.readFileSync(configFilePath, 'utf-8')); - -// To get the backend application.conf for the port -const amberConfigFilePath = path.resolve(dir, config.amberConfigFilePath); -const amberConfigContent = fs.readFileSync(amberConfigFilePath, 'utf-8'); -const applicationConfig = hocon(amberConfigContent); -// The port is decided by the configuration in the backend "python-language-server" flag -const pythonLanguageServerPort = applicationConfig['python-language-server'].port; - -// To get the file to start the pyright -const pyrightPath = path.resolve(dir, config.languageServerDir, config.languageServerFile); - -const app = express(); -app.use(express.static(dir)); -// Listening on a port which is configurable -const server = app.listen(pythonLanguageServerPort); -console.log(pythonLanguageServerPort) - -const wss = new WebSocketServer({ noServer: true }); - -server.on('upgrade', (request, socket, head) => { - wss.handleUpgrade(request, socket, head, (ws) => { - wss.emit('connection', ws, request); - }); -}); - -const startPyrightServer = (remainingRetries = 3) => { - try{ - return createServerProcess('pyright', 'node', [pyrightPath, '--stdio']); - }catch (error) { - console.error(`Failed to start Pyright language server: ${error.message}`); - if (remainingRetries > 0) { - console.log(`Retrying... (${remainingRetries} attempts left)`); - return startPyrightServer(remainingRetries - 1); - } else { - throw new Error('Exceeded maximum retry attempts to start Pyright language server'); - } - } -}; - -wss.on('connection', (ws) => { - console.log('New WebSocket connection established.'); - - //start a new server each time the websocket is on - const serverConnection = startPyrightServer(); - - const socket = { - send: (content) => ws.send(content), - onMessage: (message) => ws.on('message', message), - onError: (error) => ws.on('error', error), - onClose: () => { - ws.on('close', () => { - console.log('WebSocket connection closed.'); - }); - }, - dispose: () => { - if (ws.readyState === ws.OPEN) { - ws.close(); - } - } - }; - const reader = new WebSocketMessageReader(socket); - const writer = new WebSocketMessageWriter(socket); - const socketConnection = createConnection(reader, writer, () => socket.dispose()); - - forward(socketConnection, serverConnection, message => { - return message; - }); - -}); diff --git a/core/pyright-language-server/tsconfig.json b/core/pyright-language-server/tsconfig.json new file mode 100644 index 00000000000..4c7f2c40cac --- /dev/null +++ b/core/pyright-language-server/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "typeRoots": [ + "./src/types", + "./node_modules/@types" + ] + }, + "include": [ + "src/**/*.ts", + "src/types/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/core/pyright-language-server/yarn.lock b/core/pyright-language-server/yarn.lock new file mode 100644 index 00000000000..7ec48fe8a43 --- /dev/null +++ b/core/pyright-language-server/yarn.lock @@ -0,0 +1,729 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node@*", "@types/node@^22.5.4": + version "22.5.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.4.tgz#83f7d1f65bc2ed223bdbf57c7884f1d5a4fa84e8" + integrity sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg== + dependencies: + undici-types "~6.19.2" + +"@types/qs@*": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/ws@^8.5.12": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-walk@^8.1.1: + version "8.3.3" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" + integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== + dependencies: + acorn "^8.11.0" + +acorn@^8.11.0, acorn@^8.4.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hocon-parser@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hocon-parser/-/hocon-parser-1.0.1.tgz#b79b66143999b255e08b673c83291b5e363f0b78" + integrity sha512-qMKuQh6pLPQc0gXsl91hAJEjD4JghV1VukO5gKOzjolCnupCbGHpERzMCkZLwVDLq7sL8xR6P4iWhcM1my3HtA== + +hoconjs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hoconjs/-/hoconjs-1.0.0.tgz#5fe5c5bc1ab300f734c3aa8b1d8d95d83b4e2ca0" + integrity sha512-6jbw9YydPpchxOqrPsIMBZZXGn1F+iSX/iMS5KeBPZfcgj1DfxbjegtzoIHdlyxz24/bCczGGaUCocSXqfNfow== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pyright@^1.1.377: + version "1.1.379" + resolved "https://registry.yarnpkg.com/pyright/-/pyright-1.1.379.tgz#a31087256ec0c8e0c522a0f46869aa5da489336b" + integrity sha512-n0X+IMqot6zL5b54vfU9GattS8jM9IOh8TRFho1k/6VoyjrpzQ7TnU6PtZzwEZNJaZi5izoLIDeMnGmbin8n8Q== + optionalDependencies: + fsevents "~2.3.3" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^5.5.4: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== + +vscode-jsonrpc@~8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz#a322cc0f1d97f794ffd9c4cd2a898a0bde097f34" + integrity sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ== + +vscode-languageserver-protocol@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== + dependencies: + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz#500aef82097eb94df90d008678b0b6b5f474015b" + integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== + dependencies: + vscode-languageserver-protocol "3.17.5" + +vscode-ws-jsonrpc@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/vscode-ws-jsonrpc/-/vscode-ws-jsonrpc-3.3.2.tgz#380e5c5819c493a151236dd20a668eee173fdc5e" + integrity sha512-jxGHxAuow67sNRkkS2svsW00ZACX+Zrbury9Au2A22px6sg4pe858Nnnwvtg0Pm4D0L/W9Yzn7N7X3R/RIMxsQ== + dependencies: + vscode-jsonrpc "~8.2.1" + +ws@^8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From 7bf2483341da696ee66f68e18ac896c2f5a4fb3a Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 9 Sep 2024 21:45:20 -0400 Subject: [PATCH 15/27] resolve conflict? --- ...{AIAssistantManager.scala => AiAssistantManager.scala} | 2 +- ...IAssistantResource.scala => AiAssistantResource.scala} | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/{AIAssistantManager.scala => AiAssistantManager.scala} (98%) rename core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/{AIAssistantResource.scala => AiAssistantResource.scala} (95%) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantManager.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala similarity index 98% rename from core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantManager.scala rename to core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala index 2b1d4637beb..e47da59ffbb 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantManager.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala @@ -2,7 +2,7 @@ package edu.uci.ics.texera.web.resource.aiassistant import edu.uci.ics.amber.engine.common.AmberConfig import java.net.{HttpURLConnection, URL} -object AIAssistantManager { +object AiAssistantManager { private val aiAssistantConfig = AmberConfig.aiAssistantConfig.getOrElse( throw new Exception("ai-assistant-server configuration is missing in application.conf") ) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala similarity index 95% rename from core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala rename to core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index 30b77d31641..5d2b39612f5 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AIAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -1,6 +1,6 @@ package edu.uci.ics.texera.web.resource import edu.uci.ics.texera.web.auth.SessionUser -import edu.uci.ics.texera.web.resource.aiassistant.AIAssistantManager +import edu.uci.ics.texera.web.resource.aiassistant.AiAssistantManager import io.dropwizard.auth.Auth import javax.annotation.security.RolesAllowed import javax.ws.rs._ @@ -13,7 +13,7 @@ case class AIAssistantRequest(code: String, lineNumber: Int, allcode: String) @Path("/aiassistant") class AIAssistantResource { - final private lazy val isEnabled = AIAssistantManager.validAIAssistant + final private lazy val isEnabled = AiAssistantManager.validAIAssistant @GET @RolesAllowed(Array("REGULAR", "ADMIN")) @Path("/isenabled") @@ -44,10 +44,10 @@ class AIAssistantResource { val requestBodyString = Json.stringify(requestBodyJson) try { - val url = new java.net.URL(s"${AIAssistantManager.sharedUrl}/chat/completions") + val url = new java.net.URL(s"${AiAssistantManager.sharedUrl}/chat/completions") val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] connection.setRequestMethod("POST") - connection.setRequestProperty("Authorization", s"Bearer ${AIAssistantManager.accountKey}") + connection.setRequestProperty("Authorization", s"Bearer ${AiAssistantManager.accountKey}") connection.setRequestProperty("Content-Type", "application/json") connection.setDoOutput(true) connection.getOutputStream.write(requestBodyString.getBytes("UTF-8")) From 844e9427ccb2e82814bbe31d6ab96eb98d2c96a3 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 9 Sep 2024 22:00:55 -0400 Subject: [PATCH 16/27] resolve conflict --- .../service/user/ai-assistant/ai-assistant.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index c062334b8eb..d6a7b3d7f8c 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -15,7 +15,7 @@ export class AIAssistantService { const apiUrl = `${AI_ASSISTANT_API_BASE_URL}/isenabled`; return firstValueFrom(this.http.get(apiUrl, { responseType: "text" })) .then(response => { - const isEnabled = response !== undefined ? response : "NoAIAssistant"; + const isEnabled = response !== undefined ? response : "NoAiAssistant"; console.log( isEnabled === "OpenAI" ? "AI Assistant successfully started" @@ -24,7 +24,7 @@ export class AIAssistantService { return isEnabled; }) .catch(() => { - return "NoAIAssistant"; + return "NoAiAssistant"; }); } From 109d173707153572e2abb8e6e5a88fb441aa9d9d Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 9 Sep 2024 22:52:29 -0400 Subject: [PATCH 17/27] fix comments --- .../aiassistant/AiAssistantResource.scala | 28 ++--- .../user/ai-assistant/ai-assistant.service.ts | 49 +++++--- .../code-editor.component.ts | 114 +++++++++--------- 3 files changed, 97 insertions(+), 94 deletions(-) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index 5d2b39612f5..b2d96c56722 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -8,6 +8,7 @@ import javax.ws.rs.core.Response import javax.ws.rs.Consumes import javax.ws.rs.core.MediaType import play.api.libs.json.{Json, JsValue} +import kong.unirest.Unirest case class AIAssistantRequest(code: String, lineNumber: Int, allcode: String) @@ -31,7 +32,7 @@ class AIAssistantResource { @Auth user: SessionUser ): Response = { val finalPrompt = generatePrompt(request.code, request.lineNumber, request.allcode) - val requestBodyJson: JsValue = Json.obj( + val requestBodyJson = Json.obj( "model" -> "gpt-4", "messages" -> Json.arr( Json.obj( @@ -42,25 +43,14 @@ class AIAssistantResource { "max_tokens" -> 15 ) - val requestBodyString = Json.stringify(requestBodyJson) try { - val url = new java.net.URL(s"${AiAssistantManager.sharedUrl}/chat/completions") - val connection = url.openConnection().asInstanceOf[java.net.HttpURLConnection] - connection.setRequestMethod("POST") - connection.setRequestProperty("Authorization", s"Bearer ${AiAssistantManager.accountKey}") - connection.setRequestProperty("Content-Type", "application/json") - connection.setDoOutput(true) - connection.getOutputStream.write(requestBodyString.getBytes("UTF-8")) - val responseCode = connection.getResponseCode - val responseStream = if (responseCode >= 200 && responseCode < 300) { - connection.getInputStream - } else { - connection.getErrorStream - } - val responseString = scala.io.Source.fromInputStream(responseStream).mkString - responseStream.close() - connection.disconnect() - Response.status(responseCode).entity(responseString).build() + val response = Unirest.post(s"${AiAssistantManager.sharedUrl}/chat/completions") + .header("Authorization", s"Bearer ${AiAssistantManager.accountKey}") + .header("Content-Type", "application/json") + .body(Json.stringify(requestBodyJson)) + .asString() + + Response.status(response.getStatus).entity(response.getBody).build() } catch { case e: Exception => e.printStackTrace() diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index d6a7b3d7f8c..2e9b6cb5051 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -2,6 +2,29 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { AppSettings } from "../../../../common/app-setting"; import { HttpClient, HttpHeaders } from "@angular/common/http"; +import { Observable } from "rxjs"; + +export type TypeAnnotationResponse = { + id: string; + object: string; + created: number; + model: string; + choices: { + index: number; + message: { + role: string; + content: string; + }; + logprobs: any; + finish_reason: string; + }[]; + usage: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +}; + export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassistant`; @@ -28,23 +51,17 @@ export class AIAssistantService { }); } - public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Promise { + public getTypeAnnotations( + code: string, + lineNumber: number, + allcode: string + ): Observable { const headers = new HttpHeaders({ "Content-Type": "application/json" }); const requestBody = { code, lineNumber, allcode }; - return firstValueFrom( - this.http.post(`${AI_ASSISTANT_API_BASE_URL}/annotationresult`, requestBody, { headers }) - ) - .then(response => { - if (response.choices && response.choices.length > 0) { - return response.choices[0].message.content.trim(); - } else { - console.error("Error from backend:", response.body); - return ""; - } - }) - .catch(error => { - console.error("Request to backend failed:", error); - return ""; - }); + return this.http.post( + `${AI_ASSISTANT_API_BASE_URL}/annotationresult`, + requestBody, + { headers } + ); } } diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 3be461052b8..89fc2c4638a 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -192,8 +192,8 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy label: "Add Type Annotation", contextMenuGroupId: "1_modification", contextMenuOrder: 1.0, - run: async ed => { - // User selected code(including range and content) + run: ed => { + // User selected code (including range and content) const selection = ed.getSelection(); const model = ed.getModel(); if (!model || !selection) { @@ -205,7 +205,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy const code = model.getValueInRange(selection); // Start line of the selected code const lineNumber = selection.startLineNumber; - await this.handleTypeAnnotation( + this.handleTypeAnnotation( code, selection, ed as monaco.editor.IStandaloneCodeEditor, @@ -220,70 +220,66 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy } } - private async handleTypeAnnotation( + private handleTypeAnnotation( code: string, range: monaco.Range, editor: monaco.editor.IStandaloneCodeEditor, lineNumber: number, allcode: string - ): Promise { - return new Promise(resolve => { - this.aiAssistantService.getTypeAnnotations(code, lineNumber, allcode).then(typeAnnotations => { - console.log("The result from OpenAI is", typeAnnotations); - - let acceptButton: HTMLButtonElement | null = null; - let declineButton: HTMLButtonElement | null = null; - - this.currentCode = code; - this.currentSuggestion = typeAnnotations; - this.currentRange = range; - this.showAnnotationSuggestion = true; - - // Let the suggestion pop up next to the selected code - setTimeout(() => { - const position = editor.getScrolledVisiblePosition(range.getStartPosition()); - const popupElement = document.querySelector(".annotation-suggestion") as HTMLElement; - - if (popupElement && position) { - popupElement.style.top = `${position.top + 100}px`; - popupElement.style.left = `${position.left + 100}px`; - } - - // Make sure the user click the button - const cleanup = () => { - console.log("Cleaning up and resolving..."); - if (acceptButton) acceptButton.removeEventListener("click", acceptListener); - if (declineButton) declineButton.removeEventListener("click", declineListener); - this.showAnnotationSuggestion = false; - resolve(); - console.log("Resolved!"); - }; - - const acceptListener = () => { - this.acceptCurrentAnnotation(); - cleanup(); - }; - - const declineListener = () => { - cleanup(); - }; - acceptButton = document.querySelector(".accept-button") as HTMLButtonElement; - declineButton = document.querySelector(".decline-button") as HTMLButtonElement; - - if (acceptButton && declineButton) { - console.log("Buttons found, adding event listeners"); - //clean the old one for the "add all type annotation" - acceptButton.removeEventListener("click", acceptListener); - declineButton.removeEventListener("click", declineListener); - - acceptButton.addEventListener("click", acceptListener, { once: true }); - declineButton.addEventListener("click", declineListener, { once: true }); + ): void { + this.aiAssistantService.getTypeAnnotations(code, lineNumber, allcode) + .subscribe({ + next: (response) => { + if (response.choices && response.choices.length > 0 && response.choices[0].message.content) { + this.currentSuggestion = response.choices[0].message.content.trim(); + + let acceptButton: HTMLButtonElement | null = null; + let declineButton: HTMLButtonElement | null = null; + + this.currentCode = code; + this.currentRange = range; + this.showAnnotationSuggestion = true; + + setTimeout(() => { + const position = editor.getScrolledVisiblePosition(range.getStartPosition()); + const popupElement = document.querySelector(".annotation-suggestion") as HTMLElement; + + if (popupElement && position) { + popupElement.style.top = `${position.top + 100}px`; + popupElement.style.left = `${position.left + 100}px`; + } + + const cleanup = () => { + if (acceptButton) acceptButton.removeEventListener("click", acceptListener); + if (declineButton) declineButton.removeEventListener("click", declineListener); + this.showAnnotationSuggestion = false; + }; + + const acceptListener = () => { + this.acceptCurrentAnnotation(); + cleanup(); + }; + + const declineListener = () => { + cleanup(); + }; + + acceptButton = document.querySelector(".accept-button") as HTMLButtonElement; + declineButton = document.querySelector(".decline-button") as HTMLButtonElement; + + if (acceptButton && declineButton) { + acceptButton.addEventListener("click", acceptListener, { once: true }); + declineButton.addEventListener("click", declineListener, { once: true }); + } + }, 0); } else { - console.error("Buttons not found!"); + console.error("Error: OpenAI response does not contain a valid message content", response); } - }, 0); + }, + error: (error) => { + console.error("Error fetching type annotations:", error); + } }); - }); } // Called when the user clicks the "accept" button From b51bc81b5e9daf4fe4639e3412621d8a49b00987 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Mon, 9 Sep 2024 22:59:09 -0400 Subject: [PATCH 18/27] fmt --- .../texera/web/resource/aiassistant/AiAssistantManager.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala index c5008d72f0c..e67dd6e3185 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantManager.scala @@ -55,4 +55,4 @@ object AiAssistantManager { } } } -} \ No newline at end of file +} From dfa885cee11ba9b3daee4608ff7aea8c5fed57c6 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Tue, 10 Sep 2024 02:50:22 -0400 Subject: [PATCH 19/27] fix comments. --- .../aiassistant/AiAssistantResource.scala | 11 +++---- .../user/ai-assistant/ai-assistant.service.ts | 29 ++++--------------- .../code-editor.component.ts | 15 ++++++---- 3 files changed, 20 insertions(+), 35 deletions(-) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index b2d96c56722..d88d86c1c48 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -7,7 +7,7 @@ import javax.ws.rs._ import javax.ws.rs.core.Response import javax.ws.rs.Consumes import javax.ws.rs.core.MediaType -import play.api.libs.json.{Json, JsValue} +import play.api.libs.json.Json import kong.unirest.Unirest case class AIAssistantRequest(code: String, lineNumber: Int, allcode: String) @@ -28,9 +28,9 @@ class AIAssistantResource { @Path("/annotationresult") @Consumes(Array(MediaType.APPLICATION_JSON)) def getAnnotation( - request: AIAssistantRequest, - @Auth user: SessionUser - ): Response = { + request: AIAssistantRequest, + @Auth user: SessionUser + ): Response = { val finalPrompt = generatePrompt(request.code, request.lineNumber, request.allcode) val requestBodyJson = Json.obj( "model" -> "gpt-4", @@ -44,7 +44,8 @@ class AIAssistantResource { ) try { - val response = Unirest.post(s"${AiAssistantManager.sharedUrl}/chat/completions") + val response = Unirest + .post(s"${AiAssistantManager.sharedUrl}/chat/completions") .header("Authorization", s"Bearer ${AiAssistantManager.accountKey}") .header("Content-Type", "application/json") .body(Json.stringify(requestBodyJson)) diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts index 2e9b6cb5051..d9074eb3444 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts @@ -4,28 +4,15 @@ import { AppSettings } from "../../../../common/app-setting"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { Observable } from "rxjs"; +// The type annotation return from the LLM export type TypeAnnotationResponse = { - id: string; - object: string; - created: number; - model: string; choices: { - index: number; message: { - role: string; content: string; }; - logprobs: any; - finish_reason: string; }[]; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; }; - export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassistant`; @Injectable({ @@ -51,17 +38,11 @@ export class AIAssistantService { }); } - public getTypeAnnotations( - code: string, - lineNumber: number, - allcode: string - ): Observable { + public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Observable { const headers = new HttpHeaders({ "Content-Type": "application/json" }); const requestBody = { code, lineNumber, allcode }; - return this.http.post( - `${AI_ASSISTANT_API_BASE_URL}/annotationresult`, - requestBody, - { headers } - ); + return this.http.post(`${AI_ASSISTANT_API_BASE_URL}/annotationresult`, requestBody, { + headers, + }); } } diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 89fc2c4638a..24d046358ed 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -17,7 +17,7 @@ import { isUndefined } from "lodash"; import { CloseAction, ErrorAction } from "vscode-languageclient/lib/common/client.js"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { FormControl } from "@angular/forms"; -import { AIAssistantService } from "../../../dashboard/service/user/ai-assistant/ai-assistant.service"; +import { AIAssistantService, TypeAnnotationResponse } from "../../../dashboard/service/user/ai-assistant/ai-assistant.service"; /** * CodeEditorComponent is the content of the dialogue invoked by CodeareaCustomTemplateComponent. @@ -228,10 +228,12 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy allcode: string ): void { this.aiAssistantService.getTypeAnnotations(code, lineNumber, allcode) + .pipe(takeUntil(this.workflowVersionStreamSubject)) .subscribe({ - next: (response) => { - if (response.choices && response.choices.length > 0 && response.choices[0].message.content) { - this.currentSuggestion = response.choices[0].message.content.trim(); + next: (response: TypeAnnotationResponse) => { + const choices = response.choices || []; + if (choices.length > 0 && choices[0].message && choices[0].message.content) { + this.currentSuggestion = choices[0].message.content.trim(); let acceptButton: HTMLButtonElement | null = null; let declineButton: HTMLButtonElement | null = null; @@ -244,6 +246,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy const position = editor.getScrolledVisiblePosition(range.getStartPosition()); const popupElement = document.querySelector(".annotation-suggestion") as HTMLElement; + // Make sure the UI pop up next to the selected argument if (popupElement && position) { popupElement.style.top = `${position.top + 100}px`; popupElement.style.left = `${position.left + 100}px`; @@ -273,10 +276,10 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy } }, 0); } else { - console.error("Error: OpenAI response does not contain a valid message content", response); + console.error("Error: OpenAI response does not contain valid message content", response); } }, - error: (error) => { + error: (error: unknown) => { console.error("Error fetching type annotations:", error); } }); From f5e7c8fa179480ef46bb6e895bfe6e4ebd087072 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Tue, 10 Sep 2024 02:53:10 -0400 Subject: [PATCH 20/27] fmt --- .../code-editor.component.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 24d046358ed..c6114e7bdfd 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -17,7 +17,10 @@ import { isUndefined } from "lodash"; import { CloseAction, ErrorAction } from "vscode-languageclient/lib/common/client.js"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { FormControl } from "@angular/forms"; -import { AIAssistantService, TypeAnnotationResponse } from "../../../dashboard/service/user/ai-assistant/ai-assistant.service"; +import { + AIAssistantService, + TypeAnnotationResponse, +} from "../../../dashboard/service/user/ai-assistant/ai-assistant.service"; /** * CodeEditorComponent is the content of the dialogue invoked by CodeareaCustomTemplateComponent. @@ -205,13 +208,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy const code = model.getValueInRange(selection); // Start line of the selected code const lineNumber = selection.startLineNumber; - this.handleTypeAnnotation( - code, - selection, - ed as monaco.editor.IStandaloneCodeEditor, - lineNumber, - allcode - ); + this.handleTypeAnnotation(code, selection, ed as monaco.editor.IStandaloneCodeEditor, lineNumber, allcode); }, }); } @@ -227,7 +224,8 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy lineNumber: number, allcode: string ): void { - this.aiAssistantService.getTypeAnnotations(code, lineNumber, allcode) + this.aiAssistantService + .getTypeAnnotations(code, lineNumber, allcode) .pipe(takeUntil(this.workflowVersionStreamSubject)) .subscribe({ next: (response: TypeAnnotationResponse) => { @@ -281,7 +279,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy }, error: (error: unknown) => { console.error("Error fetching type annotations:", error); - } + }, }); } From c39860a3b15df4d244015aebea3f2c106f62df17 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Tue, 10 Sep 2024 03:04:10 -0400 Subject: [PATCH 21/27] remove import typing modules --- .../code-editor.component.ts | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index c6114e7bdfd..3f888d83e68 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -186,9 +186,6 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy // Check if the AI provider is "openai" if ((await this.aiAssistantService.checkAIAssistantEnabled()) == "OpenAI") { - // Add all needed modules for add type annotation - this.addAnnotationModule(editor); - // "Add Type Annotation" Button editor.addAction({ id: "type-annotation-action", @@ -333,52 +330,6 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy editor.executeEdits("add annotation", [op]); } - // Add all necessary modules for type annotation at the first line of the Python UDF - private addAnnotationModule(editor: monaco.editor.IStandaloneCodeEditor) { - const model = editor.getModel(); - if (!model) { - return; - } - const allCode = model.getValue(); - const typingImports = [ - "Any", - "Awaitable", - "Callable", - "Coroutine", - "Dict", - "FrozenSet", - "Generator", - "Generic", - "Iterable", - "Iterator", - "List", - "Mapping", - "Optional", - "Sequence", - "Set", - "Tuple", - "Type", - "TypeVar", - "Union", - "Deque", - "NamedTuple", - "TypedDict", - "Protocol", - "Literal", - "NewType", - "NoReturn", - ]; - const importStatement = `from typing import (\n ${typingImports.join(",\n ")}\n)`; - if (!allCode.includes(importStatement)) { - const importOp = { - // Add the module at the first line - range: new monaco.Range(1, 1, 1, 1), - text: `${importStatement}\n\n`, - }; - editor.executeEdits("add module", [importOp]); - } - } - private connectLanguageServer() { if (this.languageServerSocket === undefined) { this.languageServerSocket = new WebSocket(getWebsocketUrl("/python-language-server", "3000")); From 12198680c3a507ec6c7b4db19369db44bb96845b Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Thu, 19 Sep 2024 15:54:22 -0400 Subject: [PATCH 22/27] fix part of the comments --- .../aiassistant/AiAssistantResource.scala | 22 ++++++++----------- .../ai-assistant/ai-assistant.service.ts | 8 +++---- .../code-editor.component.ts | 8 +++---- 3 files changed, 17 insertions(+), 21 deletions(-) rename core/gui/src/app/{dashboard/service/user => workspace}/ai-assistant/ai-assistant.service.ts (89%) diff --git a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala index d88d86c1c48..f784165dc65 100644 --- a/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala +++ b/core/amber/src/main/scala/edu/uci/ics/texera/web/resource/aiassistant/AiAssistantResource.scala @@ -43,20 +43,16 @@ class AIAssistantResource { "max_tokens" -> 15 ) - try { - val response = Unirest - .post(s"${AiAssistantManager.sharedUrl}/chat/completions") - .header("Authorization", s"Bearer ${AiAssistantManager.accountKey}") - .header("Content-Type", "application/json") - .body(Json.stringify(requestBodyJson)) - .asString() - - Response.status(response.getStatus).entity(response.getBody).build() - } catch { - case e: Exception => - e.printStackTrace() - Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Error occurred").build() + val response = Unirest + .post(s"${AiAssistantManager.sharedUrl}/chat/completions") + .header("Authorization", s"Bearer ${AiAssistantManager.accountKey}") + .header("Content-Type", "application/json") + .body(Json.stringify(requestBodyJson)) + .asString() + if (response.getStatus >= 400) { + throw new RuntimeException(s"getAnnotation error: ${response.getStatus}: ${response.getBody}") } + Response.status(response.getStatus).entity(response.getBody).build() } // Helper function to get the type annotation diff --git a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/workspace/ai-assistant/ai-assistant.service.ts similarity index 89% rename from core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts rename to core/gui/src/app/workspace/ai-assistant/ai-assistant.service.ts index d9074eb3444..6f2d68aeaeb 100644 --- a/core/gui/src/app/dashboard/service/user/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/workspace/ai-assistant/ai-assistant.service.ts @@ -1,16 +1,16 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { AppSettings } from "../../../../common/app-setting"; +import { AppSettings } from "../../common/app-setting"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { Observable } from "rxjs"; // The type annotation return from the LLM export type TypeAnnotationResponse = { - choices: { + choices: ReadonlyArray<{ message: { content: string; }; - }[]; + }>; }; export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassistant`; @@ -39,7 +39,7 @@ export class AIAssistantService { } public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Observable { - const headers = new HttpHeaders({ "Content-Type": "application/json" }); + const headers = new HttpHeaders({}); const requestBody = { code, lineNumber, allcode }; return this.http.post(`${AI_ASSISTANT_API_BASE_URL}/annotationresult`, requestBody, { headers, diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 3f888d83e68..47a82b1ffa3 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -20,7 +20,7 @@ import { FormControl } from "@angular/forms"; import { AIAssistantService, TypeAnnotationResponse, -} from "../../../dashboard/service/user/ai-assistant/ai-assistant.service"; +} from "../../ai-assistant/ai-assistant.service"; /** * CodeEditorComponent is the content of the dialogue invoked by CodeareaCustomTemplateComponent. @@ -57,7 +57,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy // The result returned by the backend AI assistant public currentSuggestion: string = ""; // The range selected by the user - public currentRange: monaco.Range | null = null; + public currentRange: monaco.Range | undefined; private generateLanguageTitle(language: string): string { return `${language.charAt(0).toUpperCase()}${language.slice(1)} UDF`; @@ -230,8 +230,8 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy if (choices.length > 0 && choices[0].message && choices[0].message.content) { this.currentSuggestion = choices[0].message.content.trim(); - let acceptButton: HTMLButtonElement | null = null; - let declineButton: HTMLButtonElement | null = null; + let acceptButton: HTMLButtonElement | undefined; + let declineButton: HTMLButtonElement | undefined; this.currentCode = code; this.currentRange = range; From eefc603f1ead3d8d173283b154ad1a171476c186 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Thu, 19 Sep 2024 16:10:24 -0400 Subject: [PATCH 23/27] fix part of the comments --- .../component/code-editor-dialog/code-editor.component.ts | 2 +- .../{ => service}/ai-assistant/ai-assistant.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename core/gui/src/app/workspace/{ => service}/ai-assistant/ai-assistant.service.ts (96%) diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 47a82b1ffa3..543fcc7fe00 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -20,7 +20,7 @@ import { FormControl } from "@angular/forms"; import { AIAssistantService, TypeAnnotationResponse, -} from "../../ai-assistant/ai-assistant.service"; +} from "../../service/ai-assistant/ai-assistant.service"; /** * CodeEditorComponent is the content of the dialogue invoked by CodeareaCustomTemplateComponent. diff --git a/core/gui/src/app/workspace/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts similarity index 96% rename from core/gui/src/app/workspace/ai-assistant/ai-assistant.service.ts rename to core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts index 6f2d68aeaeb..287bd8b5d9e 100644 --- a/core/gui/src/app/workspace/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { AppSettings } from "../../common/app-setting"; +import { AppSettings } from "../../../common/app-setting"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { Observable } from "rxjs"; From 2cde0808c324379d7258c2e2208878f0cf8b3f82 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Thu, 19 Sep 2024 16:43:00 -0400 Subject: [PATCH 24/27] add annotation-suggestion.component --- core/gui/src/app/app.module.ts | 2 + .../annotation-suggestion.component.ts | 72 +++++++++++++++++++ .../code-editor.component.html | 24 +++---- .../code-editor.component.scss | 40 ----------- .../code-editor.component.ts | 52 +++++--------- 5 files changed, 98 insertions(+), 92 deletions(-) create mode 100644 core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts diff --git a/core/gui/src/app/app.module.ts b/core/gui/src/app/app.module.ts index 76bb1e7c991..060b4733ac3 100644 --- a/core/gui/src/app/app.module.ts +++ b/core/gui/src/app/app.module.ts @@ -41,6 +41,7 @@ import { UserQuotaComponent } from "./dashboard/component/user/user-quota/user-q import { UserIconComponent } from "./dashboard/component/user/user-icon/user-icon.component"; import { UserAvatarComponent } from "./dashboard/component/user/user-avatar/user-avatar.component"; import { CodeEditorComponent } from "./workspace/component/code-editor-dialog/code-editor.component"; +import { AnnotationSuggestionComponent } from "./workspace/component/code-editor-dialog/annotation-suggestion.component"; import { CodeareaCustomTemplateComponent } from "./workspace/component/codearea-custom-template/codearea-custom-template.component"; import { MiniMapComponent } from "./workspace/component/workflow-editor/mini-map/mini-map.component"; import { MenuComponent } from "./workspace/component/menu/menu.component"; @@ -168,6 +169,7 @@ registerLocaleData(en); VisualizationFrameContentComponent, CodeareaCustomTemplateComponent, CodeEditorComponent, + AnnotationSuggestionComponent, TypeCastingDisplayComponent, ShareAccessComponent, WorkflowExecutionHistoryComponent, diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts new file mode 100644 index 00000000000..551750c8796 --- /dev/null +++ b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts @@ -0,0 +1,72 @@ +import { Component, Input, Output, EventEmitter } from "@angular/core"; + +@Component({ + selector: "texera-annotation-suggestion", + template: ` +
+

Do you agree with the type annotation suggestion?

+
Adding annotation for code: {{ code }}
+

Given suggestion: {{ suggestion }}

+ + +
+ `, + styles: [` + .annotation-suggestion { + position: absolute; + background: #222; + color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); + z-index: 1000; + } + + .annotation-suggestion button { + margin-right: 10px; + } + + .annotation-suggestion button.accept-button { + background-color: #28a745; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + } + + .annotation-suggestion button.accept-button:hover { + background-color: #218838; + } + + .annotation-suggestion button.decline-button { + background-color: #dc3545; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + } + + .annotation-suggestion button.decline-button:hover { + background-color: #c82333; + } + `] +}) + +export class AnnotationSuggestionComponent { + @Input() code: string = ""; + @Input() suggestion: string = ""; + @Input() top: number = 0; + @Input() left: number = 0; + @Output() accept = new EventEmitter(); + @Output() decline = new EventEmitter(); + + onAccept() { + this.accept.emit(); + } + + onDecline() { + this.decline.emit(); + } +} diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html index 481ecbcec39..63f615a31ac 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.html @@ -30,20 +30,12 @@ -
-

Do you agree with the type annotation suggestion?

-
Adding annotation for code: {{ currentCode }}
-

Given suggestion: {{ currentSuggestion }}

- - -
+ [code]="currentCode" + [suggestion]="currentSuggestion" + [top]="suggestionTop" + [left]="suggestionLeft" + (accept)="acceptCurrentAnnotation()" + (decline)="rejectCurrentAnnotation()"> + diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss index 53eef3440b4..d4920dddeff 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.scss @@ -49,43 +49,3 @@ left: -4px; top: -5px; } - -.annotation-suggestion { - position: absolute; - background: #222; - color: #fff; - padding: 20px; - border-radius: 8px; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); - z-index: 1000; -} - -.annotation-suggestion button { - margin-right: 10px; -} - -.annotation-suggestion button.accept-button { - background-color: #28a745; - color: #000; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; -} - -.annotation-suggestion button.accept-button:hover { - background-color: #218838; -} - -.annotation-suggestion button.decline-button { - background-color: #dc3545; - color: #000; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; -} - -.annotation-suggestion button.decline-button:hover { - background-color: #c82333; -} diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 543fcc7fe00..c3f6abb6c84 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -21,6 +21,7 @@ import { AIAssistantService, TypeAnnotationResponse, } from "../../service/ai-assistant/ai-assistant.service"; +import { AnnotationSuggestionComponent } from "./annotation-suggestion.component"; /** * CodeEditorComponent is the content of the dialogue invoked by CodeareaCustomTemplateComponent. @@ -39,6 +40,7 @@ import { export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy { @ViewChild("editor", { static: true }) editorElement!: ElementRef; @ViewChild("container", { static: true }) containerElement!: ElementRef; + @ViewChild(AnnotationSuggestionComponent) annotationSuggestion!: AnnotationSuggestionComponent; private code?: YText; private editor?: any; private languageServerSocket?: WebSocket; @@ -58,6 +60,8 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy public currentSuggestion: string = ""; // The range selected by the user public currentRange: monaco.Range | undefined; + public suggestionTop: number = 0; + public suggestionLeft: number = 0; private generateLanguageTitle(language: string): string { return `${language.charAt(0).toUpperCase()}${language.slice(1)} UDF`; @@ -229,47 +233,23 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy const choices = response.choices || []; if (choices.length > 0 && choices[0].message && choices[0].message.content) { this.currentSuggestion = choices[0].message.content.trim(); - - let acceptButton: HTMLButtonElement | undefined; - let declineButton: HTMLButtonElement | undefined; - this.currentCode = code; this.currentRange = range; - this.showAnnotationSuggestion = true; - - setTimeout(() => { - const position = editor.getScrolledVisiblePosition(range.getStartPosition()); - const popupElement = document.querySelector(".annotation-suggestion") as HTMLElement; - // Make sure the UI pop up next to the selected argument - if (popupElement && position) { - popupElement.style.top = `${position.top + 100}px`; - popupElement.style.left = `${position.left + 100}px`; - } + const position = editor.getScrolledVisiblePosition(range.getStartPosition()); + if (position) { + this.suggestionTop = position.top + 100; + this.suggestionLeft = position.left + 100; + } - const cleanup = () => { - if (acceptButton) acceptButton.removeEventListener("click", acceptListener); - if (declineButton) declineButton.removeEventListener("click", declineListener); - this.showAnnotationSuggestion = false; - }; - - const acceptListener = () => { - this.acceptCurrentAnnotation(); - cleanup(); - }; - - const declineListener = () => { - cleanup(); - }; - - acceptButton = document.querySelector(".accept-button") as HTMLButtonElement; - declineButton = document.querySelector(".decline-button") as HTMLButtonElement; + this.showAnnotationSuggestion = true; - if (acceptButton && declineButton) { - acceptButton.addEventListener("click", acceptListener, { once: true }); - declineButton.addEventListener("click", declineListener, { once: true }); - } - }, 0); + if (this.annotationSuggestion) { + this.annotationSuggestion.code = this.currentCode; + this.annotationSuggestion.suggestion = this.currentSuggestion; + this.annotationSuggestion.top = this.suggestionTop; + this.annotationSuggestion.left = this.suggestionLeft; + } } else { console.error("Error: OpenAI response does not contain valid message content", response); } From ee50fd700aad4d0842537ae21d11b8722a5c2ae7 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Thu, 19 Sep 2024 17:27:29 -0400 Subject: [PATCH 25/27] remove async and change promise to observable --- .../code-editor.component.ts | 56 ++++++++++--------- .../ai-assistant/ai-assistant.service.ts | 16 +++--- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index c3f6abb6c84..573160430c5 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -170,7 +170,7 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy * Create a Monaco editor and connect it to MonacoBinding. * @private */ - private async initMonaco() { + private initMonaco() { const editor = monaco.editor.create(this.editorElement.nativeElement, { language: this.language, fontSize: 11, @@ -189,30 +189,36 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.editor = editor; // Check if the AI provider is "openai" - if ((await this.aiAssistantService.checkAIAssistantEnabled()) == "OpenAI") { - // "Add Type Annotation" Button - editor.addAction({ - id: "type-annotation-action", - label: "Add Type Annotation", - contextMenuGroupId: "1_modification", - contextMenuOrder: 1.0, - run: ed => { - // User selected code (including range and content) - const selection = ed.getSelection(); - const model = ed.getModel(); - if (!model || !selection) { - return; - } - // All the code in Python UDF - const allcode = model.getValue(); - // Content of user selected code - const code = model.getValueInRange(selection); - // Start line of the selected code - const lineNumber = selection.startLineNumber; - this.handleTypeAnnotation(code, selection, ed as monaco.editor.IStandaloneCodeEditor, lineNumber, allcode); - }, - }); - } + this.aiAssistantService.checkAIAssistantEnabled() + .pipe(untilDestroyed(this)) + .subscribe({ + next: (isEnabled: string) => { + if (isEnabled === "OpenAI") { + // "Add Type Annotation" Button + editor.addAction({ + id: "type-annotation-action", + label: "Add Type Annotation", + contextMenuGroupId: "1_modification", + contextMenuOrder: 1.0, + run: ed => { + // User selected code (including range and content) + const selection = ed.getSelection(); + const model = ed.getModel(); + if (!model || !selection) { + return; + } + // All the code in Python UDF + const allcode = model.getValue(); + // Content of user selected code + const code = model.getValueInRange(selection); + // Start line of the selected code + const lineNumber = selection.startLineNumber; + this.handleTypeAnnotation(code, selection, ed as monaco.editor.IStandaloneCodeEditor, lineNumber, allcode); + }, + }); + } + }, + }); if (this.language == "python") { this.connectLanguageServer(); } diff --git a/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts index 287bd8b5d9e..7e07a2c1098 100644 --- a/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts @@ -2,7 +2,8 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { AppSettings } from "../../../common/app-setting"; import { HttpClient, HttpHeaders } from "@angular/common/http"; -import { Observable } from "rxjs"; +import { Observable, of } from "rxjs"; +import { map, catchError } from "rxjs/operators"; // The type annotation return from the LLM export type TypeAnnotationResponse = { @@ -21,10 +22,10 @@ export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassi export class AIAssistantService { constructor(private http: HttpClient) {} - public checkAIAssistantEnabled(): Promise { + public checkAIAssistantEnabled(): Observable { const apiUrl = `${AI_ASSISTANT_API_BASE_URL}/isenabled`; - return firstValueFrom(this.http.get(apiUrl, { responseType: "text" })) - .then(response => { + return this.http.get(apiUrl, { responseType: "text" }).pipe( + map(response => { const isEnabled = response !== undefined ? response : "NoAiAssistant"; console.log( isEnabled === "OpenAI" @@ -32,10 +33,11 @@ export class AIAssistantService { : "No AI Assistant or OpenAI authentication key error" ); return isEnabled; + }), + catchError(() => { + return of("NoAiAssistant"); }) - .catch(() => { - return "NoAiAssistant"; - }); + ); } public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Observable { From 0b108c5d83cdc7c23575f43d49c9699b63f40ac2 Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Thu, 19 Sep 2024 17:35:37 -0400 Subject: [PATCH 26/27] fmt gui --- .../annotation-suggestion.component.ts | 96 +++++++++++-------- .../code-editor.component.ts | 68 ++++++------- 2 files changed, 91 insertions(+), 73 deletions(-) diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts index 551750c8796..0026734033b 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts @@ -3,57 +3,71 @@ import { Component, Input, Output, EventEmitter } from "@angular/core"; @Component({ selector: "texera-annotation-suggestion", template: ` -
+

Do you agree with the type annotation suggestion?

Adding annotation for code: {{ code }}
-

Given suggestion: {{ suggestion }}

- - +

+ Given suggestion: {{ suggestion }} +

+ +
`, - styles: [` - .annotation-suggestion { - position: absolute; - background: #222; - color: #fff; - padding: 20px; - border-radius: 8px; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); - z-index: 1000; - } + styles: [ + ` + .annotation-suggestion { + position: absolute; + background: #222; + color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); + z-index: 1000; + } - .annotation-suggestion button { - margin-right: 10px; - } + .annotation-suggestion button { + margin-right: 10px; + } - .annotation-suggestion button.accept-button { - background-color: #28a745; - color: #000; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - } + .annotation-suggestion button.accept-button { + background-color: #28a745; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + } - .annotation-suggestion button.accept-button:hover { - background-color: #218838; - } + .annotation-suggestion button.accept-button:hover { + background-color: #218838; + } - .annotation-suggestion button.decline-button { - background-color: #dc3545; - color: #000; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - } + .annotation-suggestion button.decline-button { + background-color: #dc3545; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + } - .annotation-suggestion button.decline-button:hover { - background-color: #c82333; - } - `] + .annotation-suggestion button.decline-button:hover { + background-color: #c82333; + } + `, + ], }) - export class AnnotationSuggestionComponent { @Input() code: string = ""; @Input() suggestion: string = ""; diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 573160430c5..9c69c2ea7d4 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -17,10 +17,7 @@ import { isUndefined } from "lodash"; import { CloseAction, ErrorAction } from "vscode-languageclient/lib/common/client.js"; import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"; import { FormControl } from "@angular/forms"; -import { - AIAssistantService, - TypeAnnotationResponse, -} from "../../service/ai-assistant/ai-assistant.service"; +import { AIAssistantService, TypeAnnotationResponse } from "../../service/ai-assistant/ai-assistant.service"; import { AnnotationSuggestionComponent } from "./annotation-suggestion.component"; /** @@ -189,36 +186,43 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy this.editor = editor; // Check if the AI provider is "openai" - this.aiAssistantService.checkAIAssistantEnabled() + this.aiAssistantService + .checkAIAssistantEnabled() .pipe(untilDestroyed(this)) .subscribe({ - next: (isEnabled: string) => { - if (isEnabled === "OpenAI") { - // "Add Type Annotation" Button - editor.addAction({ - id: "type-annotation-action", - label: "Add Type Annotation", - contextMenuGroupId: "1_modification", - contextMenuOrder: 1.0, - run: ed => { - // User selected code (including range and content) - const selection = ed.getSelection(); - const model = ed.getModel(); - if (!model || !selection) { - return; - } - // All the code in Python UDF - const allcode = model.getValue(); - // Content of user selected code - const code = model.getValueInRange(selection); - // Start line of the selected code - const lineNumber = selection.startLineNumber; - this.handleTypeAnnotation(code, selection, ed as monaco.editor.IStandaloneCodeEditor, lineNumber, allcode); - }, - }); - } - }, - }); + next: (isEnabled: string) => { + if (isEnabled === "OpenAI") { + // "Add Type Annotation" Button + editor.addAction({ + id: "type-annotation-action", + label: "Add Type Annotation", + contextMenuGroupId: "1_modification", + contextMenuOrder: 1.0, + run: ed => { + // User selected code (including range and content) + const selection = ed.getSelection(); + const model = ed.getModel(); + if (!model || !selection) { + return; + } + // All the code in Python UDF + const allcode = model.getValue(); + // Content of user selected code + const code = model.getValueInRange(selection); + // Start line of the selected code + const lineNumber = selection.startLineNumber; + this.handleTypeAnnotation( + code, + selection, + ed as monaco.editor.IStandaloneCodeEditor, + lineNumber, + allcode + ); + }, + }); + } + }, + }); if (this.language == "python") { this.connectLanguageServer(); } From c5759ecf39b50ae523bf2d335d094e0604099fbe Mon Sep 17 00:00:00 2001 From: "Minchong(Brian) Wu" <2638932112@qq.com> Date: Fri, 20 Sep 2024 14:07:09 -0400 Subject: [PATCH 27/27] fix gui comments --- .../annotation-suggestion.component.html | 18 +++++ .../annotation-suggestion.component.scss | 39 +++++++++++ .../annotation-suggestion.component.ts | 67 +------------------ .../code-editor.component.ts | 2 + .../ai-assistant/ai-assistant.service.ts | 39 ++++++++--- 5 files changed, 90 insertions(+), 75 deletions(-) create mode 100644 core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.html create mode 100644 core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.scss diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.html b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.html new file mode 100644 index 00000000000..9346ed85df6 --- /dev/null +++ b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.html @@ -0,0 +1,18 @@ +
+

Do you agree with the type annotation suggestion?

+
Adding annotation for code: {{ code }}
+

Given suggestion: {{ suggestion }}

+ + +
diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.scss b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.scss new file mode 100644 index 00000000000..b986983e1d2 --- /dev/null +++ b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.scss @@ -0,0 +1,39 @@ +.annotation-suggestion { + position: absolute; + background: #222; + color: #fff; + padding: 20px; + border-radius: 8px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); + z-index: 1000; +} + +.annotation-suggestion button { + margin-right: 10px; +} + +.annotation-suggestion button.accept-button { + background-color: #28a745; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; +} + +.annotation-suggestion button.accept-button:hover { + background-color: #218838; +} + +.annotation-suggestion button.decline-button { + background-color: #dc3545; + color: #000; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; +} + +.annotation-suggestion button.decline-button:hover { + background-color: #c82333; +} diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts index 0026734033b..282f1b9578c 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/annotation-suggestion.component.ts @@ -2,71 +2,8 @@ import { Component, Input, Output, EventEmitter } from "@angular/core"; @Component({ selector: "texera-annotation-suggestion", - template: ` -
-

Do you agree with the type annotation suggestion?

-
Adding annotation for code: {{ code }}
-

- Given suggestion: {{ suggestion }} -

- - -
- `, - styles: [ - ` - .annotation-suggestion { - position: absolute; - background: #222; - color: #fff; - padding: 20px; - border-radius: 8px; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); - z-index: 1000; - } - - .annotation-suggestion button { - margin-right: 10px; - } - - .annotation-suggestion button.accept-button { - background-color: #28a745; - color: #000; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - } - - .annotation-suggestion button.accept-button:hover { - background-color: #218838; - } - - .annotation-suggestion button.decline-button { - background-color: #dc3545; - color: #000; - border: none; - padding: 10px 20px; - border-radius: 5px; - cursor: pointer; - } - - .annotation-suggestion button.decline-button:hover { - background-color: #c82333; - } - `, - ], + templateUrl: "./annotation-suggestion.component.html", + styleUrls: ["./annotation-suggestion.component.scss"], }) export class AnnotationSuggestionComponent { @Input() code: string = ""; diff --git a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts index 9c69c2ea7d4..c772ad7786c 100644 --- a/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts +++ b/core/gui/src/app/workspace/component/code-editor-dialog/code-editor.component.ts @@ -294,6 +294,8 @@ export class CodeEditorComponent implements AfterViewInit, SafeStyle, OnDestroy public rejectCurrentAnnotation(): void { // Do nothing except for closing the UI this.showAnnotationSuggestion = false; + this.currentCode = ""; + this.currentSuggestion = ""; } // Add the type annotation into monaco editor diff --git a/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts b/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts index 7e07a2c1098..0463b6af309 100644 --- a/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts +++ b/core/gui/src/app/workspace/service/ai-assistant/ai-assistant.service.ts @@ -1,5 +1,4 @@ import { Injectable } from "@angular/core"; -import { firstValueFrom } from "rxjs"; import { AppSettings } from "../../../common/app-setting"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { Observable, of } from "rxjs"; @@ -8,13 +7,19 @@ import { map, catchError } from "rxjs/operators"; // The type annotation return from the LLM export type TypeAnnotationResponse = { choices: ReadonlyArray<{ - message: { + message: Readonly<{ content: string; - }; + }>; }>; }; +// Define AI model type export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassistant`; +export const AI_MODEL = { + OpenAI: "OpenAI", + NoAiAssistant: "NoAiAssistant", +} as const; +export type AI_MODEL = (typeof AI_MODEL)[keyof typeof AI_MODEL]; @Injectable({ providedIn: "root", @@ -22,11 +27,18 @@ export const AI_ASSISTANT_API_BASE_URL = `${AppSettings.getApiEndpoint()}/aiassi export class AIAssistantService { constructor(private http: HttpClient) {} - public checkAIAssistantEnabled(): Observable { + /** + * Checks if AI Assistant is enabled and returns the AI model in use. + * + * @returns {Observable} - An Observable that emits the type of AI model in use ("OpenAI" or "NoAiAssistant"). + */ + // To get the backend AI flag to check if the user want to use the AI feature + // valid returns: ["OpenAI", "NoAiAssistant"] + public checkAIAssistantEnabled(): Observable { const apiUrl = `${AI_ASSISTANT_API_BASE_URL}/isenabled`; return this.http.get(apiUrl, { responseType: "text" }).pipe( map(response => { - const isEnabled = response !== undefined ? response : "NoAiAssistant"; + const isEnabled: AI_MODEL = response === "OpenAI" ? "OpenAI" : "NoAiAssistant"; console.log( isEnabled === "OpenAI" ? "AI Assistant successfully started" @@ -35,16 +47,23 @@ export class AIAssistantService { return isEnabled; }), catchError(() => { - return of("NoAiAssistant"); + return of("NoAiAssistant" as AI_MODEL); }) ); } + /** + * Sends a request to the backend to get type annotation suggestions from LLM for the provided code. + * + * @param {string} code - The selected code for which the user wants type annotation suggestions. + * @param {number} lineNumber - The line number where the selected code locates. + * @param {string} allcode - The entire code of the UDF (User Defined Function) to provide context for the AI assistant. + * + * @returns {Observable} - An Observable that emits the type annotation suggestions + * returned by the LLM. + */ public getTypeAnnotations(code: string, lineNumber: number, allcode: string): Observable { - const headers = new HttpHeaders({}); const requestBody = { code, lineNumber, allcode }; - return this.http.post(`${AI_ASSISTANT_API_BASE_URL}/annotationresult`, requestBody, { - headers, - }); + return this.http.post(`${AI_ASSISTANT_API_BASE_URL}/annotationresult`, requestBody, {}); } }