Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/src/components/DatasetInformation/DatasetError.vue
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
</a>
</b>
</p>
<h4 class="mb-3 h-md">What might have happened?</h4>
<b-card>
<GalaxyWizard view="error" :query="jobDetails.tool_stderr" context="tool_error" />
</b-card>
<h4 class="mb-3 h-md">Issue Report</h4>
<b-alert
v-for="(resultMessage, index) in resultMessages"
Expand Down Expand Up @@ -89,6 +93,7 @@
<script>
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";
Expand All @@ -105,6 +110,7 @@ export default {
DatasetErrorDetails,
FontAwesomeIcon,
FormElement,
GalaxyWizard,
JobDetailsProvider,
JobProblemProvider,
CurrentUser,
Expand Down
87 changes: 87 additions & 0 deletions client/src/components/GalaxyWizard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script setup lang="ts">
import axios from "axios";
import { ref } from "vue";
import Heading from "./Common/Heading.vue";
import LoadingSpan from "./LoadingSpan.vue";

const props = defineProps({
view: {
type: String,
default: "wizard",
},
query: {
type: String,
default: "",
},
context: {
type: String,
default: "",
},
});

const query = ref(props.query);
const queryResponse = ref("");

const busy = ref(false);

// on submit, query the server and put response in display box
function submitQuery() {
busy.value = true;
queryResponse.value = "";
const context = props.context || "username";
axios
.post("/api/chat", {
query: query.value,
context: context,
})
.then(function (response) {
console.log(response);
queryResponse.value = response.data;
})
.catch(function (error) {
console.error(error);
})
.finally(() => {
busy.value = false;
});
}
</script>
<template>
<div>
<!-- input text, full width top of page -->
<Heading v-if="props.view == 'wizard'" inline h2>Ask the wizard</Heading>
<div :class="props.view == 'wizard' && 'mt-2'">
<b-input
v-if="props.query == ''"
id="wizardinput"
v-model="query"
style="width: 100%"
placeholder="What's the difference in fasta and fastq files?"
@keyup.enter="submitQuery" />
<b-button
v-else-if="!queryResponse"
variant="info"
:disabled="busy"
@click="submitQuery">
<span v-if="!busy">
Let our Help Wizard Figure it out!
</span>
<LoadingSpan v-else message="Thinking..." />
</b-button>
</div>
<!-- spinner when busy -->
<div :class="props.view == 'wizard' && 'mt-4'">
<div v-if="busy">
<b-skeleton animation="wave" width="85%"></b-skeleton>
<b-skeleton animation="wave" width="55%"></b-skeleton>
<b-skeleton animation="wave" width="70%"></b-skeleton>
</div>
<div v-else class="chatResponse">{{ queryResponse }}</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.chatResponse {
white-space: pre-wrap;
}
</style>
4 changes: 4 additions & 0 deletions client/src/entry/analysis/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions client/src/entry/analysis/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -318,6 +320,10 @@ export function getRouter(Galaxy) {
path: "tours",
component: TourList,
},
{
path: "wizard",
component: GalaxyWizard,
},
{
path: "tours/:tourId",
component: TourRunner,
Expand Down
49 changes: 49 additions & 0 deletions client/src/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions doc/source/admin/galaxy_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
4 changes: 4 additions & 0 deletions lib/galaxy/config/sample/galaxy.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/galaxy/config/schemas/config_schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/galaxy/dependencies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions lib/galaxy/dependencies/conditional-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ python-pam
galaxycloudrunner
pkce
total-perspective-vortex<3
openai

# For file sources plugins
fs.webdavfs>=0.4.2 # type: webdav
Expand Down
13 changes: 13 additions & 0 deletions lib/galaxy/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions lib/galaxy/webapps/galaxy/api/chat.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions lib/galaxy/webapps/galaxy/buildapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down