diff --git a/.gitignore b/.gitignore
index 7188b6c1747..961d1ba5e3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -74,6 +74,7 @@ portal/src/main/webapp/WEB-INF/logback.xml
firehose-importer/reference_data/gene_info
portal.properties
src/main/resources/application.properties
+microservices/assistant/src/main/resources/application.properties
importer.properties
log4j.properties
build.properties
diff --git a/microservices/assistant/Dockerfile b/microservices/assistant/Dockerfile
new file mode 100644
index 00000000000..efb9187e105
--- /dev/null
+++ b/microservices/assistant/Dockerfile
@@ -0,0 +1,19 @@
+FROM maven:3-eclipse-temurin-17 as build
+
+WORKDIR /assistant
+
+COPY pom.xml .
+
+RUN mvn dependency:go-offline --fail-never
+
+COPY src ./src
+
+RUN mvn install package -DskipTests
+
+FROM eclipse-temurin:17-jre-jammy
+
+WORKDIR /assistant
+
+COPY --from=build /assistant/target/assistant.jar .
+
+ENTRYPOINT ["java","-jar","assistant.jar"]
diff --git a/microservices/assistant/pom.xml b/microservices/assistant/pom.xml
new file mode 100644
index 00000000000..bedeb83e12d
--- /dev/null
+++ b/microservices/assistant/pom.xml
@@ -0,0 +1,257 @@
+
+
+ 4.0.0
+
+ org.cbioportal.assistant
+ assistant
+ 1.0.0-SNAPSHOT
+ jar
+
+ gene-assistant
+ Gene Assistant Microservice
+
+
+ 17
+ ${java.version}
+ ${java.version}
+ UTF-8
+ 3.2.5
+ 3.4.2
+ 2.43.0
+ 0.8.12
+ 3.2.5
+ 3.2.5
+ 3.6.0
+ 8080
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ org.springframework.ai
+ spring-ai-starter-model-azure-openai
+ 1.0.3
+
+
+ org.springframework.ai
+ spring-ai-azure-openai
+ 1.0.3
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
+ com.h2database
+ h2
+ runtime
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+ ${project.artifactId}
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ ${spring-boot.version}
+
+
+
+ repackage
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ ${java.version}
+ ${java.version}
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+ random
+ 1
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco.version}
+
+
+ default-prepare-agent
+
+ prepare-agent
+
+
+
+ default-report
+ test
+
+ report
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${maven-failsafe-plugin.version}
+
+ ${project.build.outputDirectory}
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
+ com.diffplug.spotless
+ spotless-maven-plugin
+ ${spotless.version}
+
+
+
+ 1.25.2
+
+
+
+
+
+
+
+ check
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ ${build-helper-maven-plugin.version}
+
+
+ reserve-network-port
+
+ reserve-network-port
+
+ process-resources
+
+
+ test.server.port
+
+
+
+
+
+
+
+
+
+
+ jib
+
+ false
+
+
+
+
+ com.google.cloud.tools
+ jib-maven-plugin
+ ${jib-maven-plugin.version}
+
+
+ eclipse-temurin:17-jre-jammy
+
+
+ ghcr.io/cbioportal/gene-assistant:${project.version}
+
+
+
+ 8080
+
+
+
+
+
+
+ build
+
+ package
+
+
+
+
+
+
+
+
+
+
+ central
+ https://repo.maven.apache.org/maven2
+
+
+
+
+
+ github
+ GitHub Packages
+ https://maven.pkg.github.com/cbioportal/cbioportal
+
+
+
+
\ No newline at end of file
diff --git a/microservices/assistant/src/main/java/org/cbioportal/assistant/Application.java b/microservices/assistant/src/main/java/org/cbioportal/assistant/Application.java
new file mode 100644
index 00000000000..100353ddef2
--- /dev/null
+++ b/microservices/assistant/src/main/java/org/cbioportal/assistant/Application.java
@@ -0,0 +1,44 @@
+package org.cbioportal.assistant;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@SpringBootApplication
+@RestController
+@ConditionalOnProperty(name = "spring.ai.enabled", havingValue = "true")
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+
+ private final AssistantService assistantService;
+
+ @Autowired
+ public Application(AssistantService assistantService) {
+ this.assistantService = assistantService;
+ }
+
+ @Operation(description = "Send query to AI model for gene assistance")
+ @ApiResponse(
+ responseCode = "200",
+ description = "OK",
+ content = @Content(schema = @Schema(implementation = ChatResponse.class)))
+ @PostMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity fetchGeneAssistantResponse(@RequestBody String message) {
+ ChatResponse chatResponse = assistantService.generateResponse(message);
+ return ResponseEntity.ok(chatResponse.getResult().getOutput().getText().toString());
+ }
+}
diff --git a/microservices/assistant/src/main/java/org/cbioportal/assistant/AssistantService.java b/microservices/assistant/src/main/java/org/cbioportal/assistant/AssistantService.java
new file mode 100644
index 00000000000..d17342a8089
--- /dev/null
+++ b/microservices/assistant/src/main/java/org/cbioportal/assistant/AssistantService.java
@@ -0,0 +1,47 @@
+package org.cbioportal.assistant;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import org.springframework.ai.chat.messages.Message;
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AssistantService {
+
+ private static final String OQL_CONTEXT_FILE = "oql-context.st";
+ private final ChatModel chatModel;
+
+ @Autowired
+ public AssistantService(ChatModel chatModel) {
+ this.chatModel = chatModel;
+ }
+
+ public ChatResponse generateResponse(String message) {
+ try {
+ Resource oqlContextResource = new ClassPathResource(OQL_CONTEXT_FILE);
+ String oqlContext =
+ new String(
+ oqlContextResource.getInputStream().readAllBytes(),
+ StandardCharsets.UTF_8);
+ Message systemMessage = new SystemMessage(oqlContext);
+ Message userMessage = new UserMessage(message);
+
+ Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
+ ChatResponse response = this.chatModel.call(prompt);
+ return response;
+
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to read oql context prompt resource", e);
+ }
+ }
+}
diff --git a/microservices/assistant/src/main/resources/application.properties.EXAMPLE b/microservices/assistant/src/main/resources/application.properties.EXAMPLE
new file mode 100644
index 00000000000..a3158af8b0a
--- /dev/null
+++ b/microservices/assistant/src/main/resources/application.properties.EXAMPLE
@@ -0,0 +1,8 @@
+server.port=8090
+
+# Spring AI Properties 1.0.3, see https://docs.spring.io/spring-ai/reference/api/chatmodel.html for other configurable models
+spring.ai.enabled=true
+spring.ai.azure.openai.api-key=
+spring.ai.azure.openai.endpoint=
+spring.ai.azure.openai.chat.options.deployment-name=
+spring.ai.model.chat=
diff --git a/microservices/assistant/src/main/resources/maven.properties b/microservices/assistant/src/main/resources/maven.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/microservices/assistant/src/main/resources/oql-context.st b/microservices/assistant/src/main/resources/oql-context.st
new file mode 100644
index 00000000000..7ba95659f0a
--- /dev/null
+++ b/microservices/assistant/src/main/resources/oql-context.st
@@ -0,0 +1,89 @@
+# role: system
+You are an expert in cBioPortal's Onco Query Language (OQL). Your job is to generate correct, minimal, and valid OQL queries based on user input.
+
+Respond ONLY with a valid OQL query suitable for cBioPortal, using the following syntax and keywords.
+Prepend OQL queries with `OQL: TRUE` and prepend verbal responses with `OQL: FALSE`.
+
+---
+
+## Syntax Format
+GENE: OQL_KEYWORDS; OQL: TRUE
+
+---
+
+## OQL Keywords
+- MUT: all non-synonymous mutations
+ - MUT = (e.g., V600E)
+ - MUT = (MISSENSE, NONSENSE, NONSTART, NONSTOP, FRAMESHIFT, INFRAME, SPLICE, TRUNC)
+ - MUT = () e.g., (12-13) or (718-854)
+- FUSION: all gene fusions
+- AMP: amplification
+- HOMDEL: deep/homozygous deletion
+- GAIN: copy number gain
+- HETLOSS: shallow deletion / loss of heterozygosity
+- CNA >= GAIN: equivalent to GAIN + AMP
+- EXP > x or < -x: mRNA expression x SD above or below mean
+- PROT > x or < -x: protein expression x SD above or below mean
+
+---
+
+## Modifiers
+- DRIVER: restrict to driver events
+- GERMLINE / SOMATIC: restrict to mutation origin
+
+Modifiers may be combined, e.g.:
+- DRIVER_MUT
+- GERMLINE_MUT
+- SOMATIC_MUT
+- DRIVER_FUSION
+
+---
+
+## Operators
+- `!=` : exclude a specific mutation
+- `DATATYPES` : apply keywords to multiple genes if all genes are queried for the same variant type or modifier.
+
+---
+
+## Merged Tracks
+Use square brackets to group genes, optionally with a label in double quotes.
+Example:
+["TP53 PATHWAY" TP53 P53AIP1] OQL: TRUE
+
+---
+
+## Output Rules
+- Always produce **only** the valid OQL query.
+- Do **not** add explanations, natural language, or commentary.
+- Combine multiple genes logically using per-gene syntax or DATATYPES: when appropriate.
+- Use HUGO gene symbols instead of Ensembl IDs.
+
+---
+
+## Examples
+
+User: "all TP53 mutations"
+→ `TP53: MUT OQL: TRUE`
+
+User: "show me genes in the MAPK pathway"
+→ `KRAS NRAS BRAF MAP2K1 MAP2K2 MAP3K1 MAP3K3 MAP3K7 RAF1 RPS6KA3`
+
+User: "query for all EGFR driver fusion events"
+→ `EGFR: FUSION_DRIVER OQL: TRUE`
+
+User: "query TP53 mutations except for missense mutations"
+→ `TP53: MUT != MISSENSE OQL: TRUE`
+
+User: "show me BRCA1 nonsense germline driver mutations"
+→ `BRCA1: NONSENSE_GERMLINE_DRIVER OQL: TRUE`
+
+User: "search for all KRAS mutations at position 12"
+→ `KRAS: MUT = (12-12) OQL: TRUE`
+
+User: "search for all KRAS mutations at positions 12 and 13"
+→ `KRAS: MUT = (12-13) OQL: TRUE`
+
+---
+
+## Instruction
+Now respond to this user request using the OQL rules above:
diff --git a/pom.xml b/pom.xml
index 2ec2e4f1839..1779157d6da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -939,4 +939,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/org/cbioportal/legacy/model/GeneAssistantResponse.java b/src/main/java/org/cbioportal/legacy/model/GeneAssistantResponse.java
new file mode 100644
index 00000000000..9452ac999fd
--- /dev/null
+++ b/src/main/java/org/cbioportal/legacy/model/GeneAssistantResponse.java
@@ -0,0 +1,16 @@
+package org.cbioportal.legacy.model;
+
+import java.io.Serializable;
+
+public class GeneAssistantResponse implements Serializable {
+
+ private String aiResponse;
+
+ public String getAiResponse() {
+ return aiResponse;
+ }
+
+ public void setAiResponse(String aiResponse) {
+ this.aiResponse = aiResponse;
+ }
+}
diff --git a/src/main/java/org/cbioportal/legacy/service/FrontendPropertiesServiceImpl.java b/src/main/java/org/cbioportal/legacy/service/FrontendPropertiesServiceImpl.java
index fa3bf0021a8..916ad4cbf95 100644
--- a/src/main/java/org/cbioportal/legacy/service/FrontendPropertiesServiceImpl.java
+++ b/src/main/java/org/cbioportal/legacy/service/FrontendPropertiesServiceImpl.java
@@ -152,6 +152,7 @@ public enum FrontendProperty {
skin_geneset_hierarchy_default_gsva_score("skin.geneset_hierarchy.default_gsva_score", null),
app_version("app.version", null),
frontendSentryEndpoint("sentryjs.frontend_project_endpoint", null),
+ assistant_enabled("assistant.enabled", null),
// These properties require additional processing.
// Names refer to the property that requires processing.
diff --git a/src/main/java/org/cbioportal/legacy/web/GeneAssistantController.java b/src/main/java/org/cbioportal/legacy/web/GeneAssistantController.java
new file mode 100644
index 00000000000..3f29925a970
--- /dev/null
+++ b/src/main/java/org/cbioportal/legacy/web/GeneAssistantController.java
@@ -0,0 +1,63 @@
+package org.cbioportal.legacy.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import java.util.HashMap;
+import java.util.Map;
+import org.cbioportal.legacy.model.GeneAssistantResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.client.RestTemplate;
+
+@Validated
+@RestController
+@RequestMapping("/api")
+@ConditionalOnProperty(name = "assistant.enabled", havingValue = "true")
+public class GeneAssistantController {
+
+ @Value("${assistant.url}")
+ private String assistantUrl;
+
+ @Autowired private RestTemplate restTemplate;
+
+ @Operation(description = "Send query to assistant microservice")
+ @ApiResponse(
+ responseCode = "200",
+ description = "OK",
+ content = @Content(schema = @Schema(implementation = GeneAssistantResponse.class)))
+ @PostMapping(
+ value = "/assistant",
+ consumes = MediaType.APPLICATION_JSON_VALUE,
+ produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity fetchGeneAssistantResponse(
+ @RequestBody Map body) {
+
+ String message = body.get("message");
+
+ String url = assistantUrl;
+
+ Map request = new HashMap<>();
+ request.put("message", message);
+
+ String response;
+ try {
+ response = restTemplate.postForObject(url, request, String.class);
+ } catch (Exception ex) {
+ GeneAssistantResponse error = new GeneAssistantResponse();
+ error.setAiResponse("Assistant service error: " + ex.getMessage());
+ return ResponseEntity.status(500).body(error);
+ }
+
+ GeneAssistantResponse geneAssistantResponse = new GeneAssistantResponse();
+ geneAssistantResponse.setAiResponse(response);
+
+ return ResponseEntity.ok(geneAssistantResponse);
+ }
+}
diff --git a/src/main/resources/application.properties.EXAMPLE b/src/main/resources/application.properties.EXAMPLE
index baaa864a787..4a3040c63ec 100644
--- a/src/main/resources/application.properties.EXAMPLE
+++ b/src/main/resources/application.properties.EXAMPLE
@@ -4,6 +4,13 @@ app.name=cbioportal
# Spring Boot Properties 2.7.14
spring.mvc.pathmatch.matching-strategy = ANT_PATH_MATCHER
+# Spring AI Properties 1.0.3, see https://docs.spring.io/spring-ai/reference/api/chatmodel.html for other configurable models
+spring.ai.enabled=false
+#spring.ai.azure.openai.api-key=
+#spring.ai.azure.openai.endpoint=
+#spring.ai.azure.openai.chat.options.deployment-name=
+#spring.ai.model.chat=
+
#Clickhouse Enabled
# Set to True to enable Clickhouse (Warning Experimental Features)
#clickhouse_mode=false
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
index 5a39ef4ff45..3c32f35c596 100644
--- a/src/main/resources/templates/index.html
+++ b/src/main/resources/templates/index.html
@@ -74,7 +74,8 @@
-
+
+
@@ -92,4 +93,4 @@