diff --git a/pom.xml b/pom.xml
index 2ec2e4f1839..50c1c24b204 100644
--- a/pom.xml
+++ b/pom.xml
@@ -141,6 +141,16 @@
org.springframework.boot
spring-boot-starter
+
+ 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-test
@@ -939,4 +949,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/org/cbioportal/application/assistant/GeneAssistantService.java b/src/main/java/org/cbioportal/application/assistant/GeneAssistantService.java
new file mode 100644
index 00000000000..ea8c44132be
--- /dev/null
+++ b/src/main/java/org/cbioportal/application/assistant/GeneAssistantService.java
@@ -0,0 +1,48 @@
+package org.cbioportal.application.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.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.stereotype.Service;
+
+@Service
+@ConditionalOnProperty(name = "spring.ai.enabled", havingValue = "true")
+public class GeneAssistantService {
+
+ private static final String OQL_CONTEXT_FILE = "oql-context.st";
+
+ private final ChatModel chatModel;
+
+ @Autowired
+ public GeneAssistantService(ChatModel chatModel) {
+ this.chatModel = chatModel;
+ }
+
+ public String 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.getResult().getOutput().getText().toString();
+
+ } catch (IOException e) {
+ throw new UncheckedIOException("Failed to read oql context prompt resource", e);
+ }
+ }
+}
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..4c0853f31a5 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),
+ spring_ai_enabled("spring.ai.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..a639fe38aad
--- /dev/null
+++ b/src/main/java/org/cbioportal/legacy/web/GeneAssistantController.java
@@ -0,0 +1,45 @@
+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 org.cbioportal.application.assistant.GeneAssistantService;
+import org.cbioportal.legacy.model.GeneAssistantResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+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.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@Validated
+@RestController()
+@ConditionalOnProperty(name = "spring.ai.enabled", havingValue = "true")
+public class GeneAssistantController {
+
+ private final GeneAssistantService geneAssistantService;
+
+ @Autowired
+ public GeneAssistantController(GeneAssistantService geneAssistantService) {
+ this.geneAssistantService = geneAssistantService;
+ }
+
+ @Operation(description = "Send query to AI model for gene assistance")
+ @ApiResponse(
+ responseCode = "200",
+ description = "OK",
+ content = @Content(schema = @Schema(implementation = GeneAssistantResponse.class)))
+ @PostMapping(value = "/api/assistant", produces = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity fetchGeneAssistantResponse(
+ @RequestBody String message) {
+
+ String response = geneAssistantService.generateResponse(message);
+ 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/oql-context.st b/src/main/resources/oql-context.st
new file mode 100644
index 00000000000..7ba95659f0a
--- /dev/null
+++ b/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/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 @@