diff --git a/client/src/components/DatasetInformation/DatasetError.vue b/client/src/components/DatasetInformation/DatasetError.vue index 001bb403e0ac..75ef23f86c5e 100644 --- a/client/src/components/DatasetInformation/DatasetError.vue +++ b/client/src/components/DatasetInformation/DatasetError.vue @@ -53,6 +53,10 @@

+

What might have happened?

+ + +

Issue Report

import DatasetErrorDetails from "./DatasetErrorDetails"; import FormElement from "components/Form/FormElement"; +import GalaxyWizard from "components/GalaxyWizard"; import { DatasetProvider } from "components/providers"; import { JobDetailsProvider, JobProblemProvider } from "components/providers/JobProvider"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; @@ -105,6 +110,7 @@ export default { DatasetErrorDetails, FontAwesomeIcon, FormElement, + GalaxyWizard, JobDetailsProvider, JobProblemProvider, CurrentUser, diff --git a/client/src/components/GalaxyWizard.vue b/client/src/components/GalaxyWizard.vue new file mode 100644 index 000000000000..9206db372506 --- /dev/null +++ b/client/src/components/GalaxyWizard.vue @@ -0,0 +1,87 @@ + + + diff --git a/client/src/entry/analysis/menu.js b/client/src/entry/analysis/menu.js index ddbfcb3f0e65..fdb84c2d8fc7 100644 --- a/client/src/entry/analysis/menu.js +++ b/client/src/entry/analysis/menu.js @@ -113,6 +113,10 @@ export function fetchMenu(options = {}) { target: "_blank", hidden: !options.helpsite_url, }, + { + title: _l("Help Wizard"), + url: "/wizard", + }, { title: _l("Support"), url: options.support_url, diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js index 35f234452422..74317399691e 100644 --- a/client/src/entry/analysis/router.js +++ b/client/src/entry/analysis/router.js @@ -66,6 +66,8 @@ import { ExternalIdentities } from "components/User/ExternalIdentities"; import { HistoryExport } from "components/HistoryExport/index"; import HistoryExportTasks from "components/History/Export/HistoryExport"; +import GalaxyWizard from "components/GalaxyWizard"; + Vue.use(VueRouter); // patches $router.push() to trigger an event and hide duplication warnings @@ -318,6 +320,10 @@ export function getRouter(Galaxy) { path: "tours", component: TourList, }, + { + path: "wizard", + component: GalaxyWizard, + }, { path: "tours/:tourId", component: TourRunner, diff --git a/client/src/schema/schema.ts b/client/src/schema/schema.ts index c8ac40bba525..4b6046685e29 100644 --- a/client/src/schema/schema.ts +++ b/client/src/schema/schema.ts @@ -8,6 +8,13 @@ export interface paths { /** Returns returns an API key for authenticated user based on BaseAuth headers. */ get: operations["get_api_key_api_authenticate_baseauth_get"]; }; + "/api/chat": { + /** + * Query + * @description We're off to ask the wizard + */ + post: operations["query_api_chat_post"]; + }; "/api/configuration": { /** * Return an object containing exposable configuration settings @@ -1600,6 +1607,23 @@ export interface components { */ type: "change_dbkey"; }; + /** + * ChatPayload + * @description Base model definition with common configuration used by all derived models. + */ + ChatPayload: { + /** + * Message + * @description The message to be sent to the chat. + */ + query: string; + /** + * Context + * @description The context identifier to be used by the chat. + * @enum {string} + */ + context?: "username" | "tool_error"; + }; /** * CheckForUpdatesResponse * @description Base model definition with common configuration used by all derived models. @@ -7630,6 +7654,31 @@ export interface operations { }; }; }; + query_api_chat_post: { + /** + * Query + * @description We're off to ask the wizard + */ + requestBody: { + content: { + "application/json": components["schemas"]["ChatPayload"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + content: { + "application/json": string; + }; + }; + /** @description Validation Error */ + 422: { + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; index_api_configuration_get: { /** * Return an object containing exposable configuration settings diff --git a/doc/source/admin/galaxy_options.rst b/doc/source/admin/galaxy_options.rst index 3d54bea52166..2df35e9f8f5a 100644 --- a/doc/source/admin/galaxy_options.rst +++ b/doc/source/admin/galaxy_options.rst @@ -4944,6 +4944,17 @@ :Type: int +~~~~~~~~~~~~~~~~~~ +``openai_api_key`` +~~~~~~~~~~~~~~~~~~ + +:Description: + API key for OpenAI (https://openai.com/) to enable the wizard (or + more?) +:Default: ``None`` +:Type: str + + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``enable_tool_recommendations`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/galaxy/config/sample/galaxy.yml.sample b/lib/galaxy/config/sample/galaxy.yml.sample index fdb8f94fbc7e..59a4d9db2d39 100644 --- a/lib/galaxy/config/sample/galaxy.yml.sample +++ b/lib/galaxy/config/sample/galaxy.yml.sample @@ -2648,6 +2648,10 @@ galaxy: # as threshold (above threshold: regular select fields will be used) #select_type_workflow_threshold: -1 + # API key for OpenAI (https://openai.com/) to enable the wizard (or + # more?) + #openai_api_key: null + # Allow the display of tool recommendations in workflow editor and # after tool execution. If it is enabled and set to true, please # enable 'tool_recommendation_model_path' as well diff --git a/lib/galaxy/config/schemas/config_schema.yml b/lib/galaxy/config/schemas/config_schema.yml index 3e6b1881c627..027d8008ae1c 100644 --- a/lib/galaxy/config/schemas/config_schema.yml +++ b/lib/galaxy/config/schemas/config_schema.yml @@ -3609,6 +3609,12 @@ mapping: use -1 (default) in order to always use the regular select fields, use any other positive number as threshold (above threshold: regular select fields will be used) + openai_api_key: + type: str + required: false + desc: | + API key for OpenAI (https://openai.com/) to enable the wizard (or more?) + enable_tool_recommendations: type: bool default: false diff --git a/lib/galaxy/dependencies/__init__.py b/lib/galaxy/dependencies/__init__.py index 79526e490304..42d2ea49a339 100644 --- a/lib/galaxy/dependencies/__init__.py +++ b/lib/galaxy/dependencies/__init__.py @@ -280,6 +280,9 @@ def check_influxdb(self): def check_tensorflow(self): return asbool(self.config["enable_tool_recommendations"]) + def check_openai(self): + return self.config.get("openai_api_key", None) is not None + def check_weasyprint(self): # See notes in ./conditional-requirements.txt for more information. return os.environ.get("GALAXY_DEPENDENCIES_INSTALL_WEASYPRINT") == "1" diff --git a/lib/galaxy/dependencies/conditional-requirements.txt b/lib/galaxy/dependencies/conditional-requirements.txt index 8d8a5d9bd55c..28335c054a96 100644 --- a/lib/galaxy/dependencies/conditional-requirements.txt +++ b/lib/galaxy/dependencies/conditional-requirements.txt @@ -14,6 +14,7 @@ python-pam galaxycloudrunner pkce total-perspective-vortex<3 +openai # For file sources plugins fs.webdavfs>=0.4.2 # type: webdav diff --git a/lib/galaxy/schema/schema.py b/lib/galaxy/schema/schema.py index f70f7680bb3a..ef63c08d7c46 100644 --- a/lib/galaxy/schema/schema.py +++ b/lib/galaxy/schema/schema.py @@ -3191,6 +3191,19 @@ class MaterializeDatasetInstanceRequest(MaterializeDatasetInstanceAPIRequest): history_id: DecodedDatabaseIdField +class ChatPayload(Model): + query: str = Field( + ..., + title="Message", + description="The message to be sent to the chat.", + ) + context: Optional[str] = Field( + default="", + title="Context", + description="A context identifier to be used by the chat.", + ) + + class CreatePagePayload(PageSummaryBase): content_format: PageContentFormat = ContentFormatField content: Optional[str] = ContentField diff --git a/lib/galaxy/webapps/galaxy/api/chat.py b/lib/galaxy/webapps/galaxy/api/chat.py new file mode 100644 index 000000000000..3f78dfecb9e6 --- /dev/null +++ b/lib/galaxy/webapps/galaxy/api/chat.py @@ -0,0 +1,73 @@ +""" +API Controller providing Chat functionality +""" +import logging + +try: + import openai +except ImportError: + openai = None + +from galaxy.config import GalaxyAppConfiguration +from galaxy.managers.context import ProvidesUserContext +from galaxy.webapps.galaxy.api import ( + depends, + DependsOnTrans, +) +from galaxy.exceptions import ConfigurationError +from galaxy.schema.schema import ChatPayload +from . import ( + depends, + Router, +) + +log = logging.getLogger(__name__) + +router = Router(tags=["chat"]) + +PROMPT = """ +You are a highly intelligent question answering agent, expert on the Galaxy analysis platform and in the fields of computer science, bioinformatics, and genomics. +You will try to answer questions about Galaxy, and if you don't know the answer, you will ask the user to rephrase the question. +""" + + +@router.cbv +class ChatAPI: + config: GalaxyAppConfiguration = depends(GalaxyAppConfiguration) + + @router.post("/api/chat") + def query(self, query: ChatPayload, trans: ProvidesUserContext = DependsOnTrans) -> str: + """We're off to ask the wizard""" + + if openai is None or self.config.openai_api_key is None: + raise ConfigurationError("OpenAI is not configured for this instance.") + else: + openai.api_key = self.config.openai_api_key + + messages=[ + {"role": "system", "content": PROMPT}, + {"role": "user", "content": query.query}, + ] + + if query.context == "username": + user = trans.user + if user is not None: + log.debug(f"CHATGPTuser: {user.username}") + msg = f"You will address the user as {user.username}" + else: + msg = f"You will address the user as Anonymous User" + messages.append({"role": "system", "content": msg}) + elif query.context == "tool_error": + msg = "The user will provide you a Galaxy tool error, and you will try to debug and explain what happened" + messages.append({"role": "system", "content": msg}) + + log.debug(f"CHATGPTmessages: {messages}") + + response = openai.ChatCompletion.create( + model="gpt-3.5-turbo", + messages=messages, + temperature=0, + ) + + answer = response["choices"][0]["message"]["content"] + return answer diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py index a9bcaba11314..557022d50de9 100644 --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -254,6 +254,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs): webapp.add_client_route("/collection/{collection_id}/edit") webapp.add_client_route("/jobs/submission/success") webapp.add_client_route("/jobs/{job_id}/view") + webapp.add_client_route("/wizard") webapp.add_client_route("/workflows/list") webapp.add_client_route("/workflows/list_published") webapp.add_client_route("/workflows/edit")