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
12 changes: 11 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-azure-openai</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-azure-openai</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down Expand Up @@ -939,4 +949,4 @@
</profile>
</profiles>

</project>
</project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if we need to set CORS restrictions somewhere for these endpoints? So only users of the website can use m

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<GeneAssistantResponse> fetchGeneAssistantResponse(
@RequestBody String message) {

String response = geneAssistantService.generateResponse(message);
GeneAssistantResponse geneAssistantResponse = new GeneAssistantResponse();
geneAssistantResponse.setAiResponse(response);

return ResponseEntity.ok(geneAssistantResponse);
}
}
7 changes: 7 additions & 0 deletions src/main/resources/application.properties.EXAMPLE
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 89 additions & 0 deletions src/main/resources/oql-context.st
Original file line number Diff line number Diff line change
@@ -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 = <protein change> (e.g., V600E)
- MUT = <mutation type> (MISSENSE, NONSENSE, NONSTART, NONSTOP, FRAMESHIFT, INFRAME, SPLICE, TRUNC)
- MUT = (<position range>) 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:
5 changes: 3 additions & 2 deletions src/main/resources/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@

<div th:replace="~{tracking_include}" />

<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" />


</head>

Expand All @@ -92,4 +93,4 @@
<div id="reactRoot"></div>

</body>
</html>
</html>
5 changes: 3 additions & 2 deletions src/main/resources/webapp/index.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@

<%@include file="./tracking_include.jsp" %>

<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet" />


</head>

Expand All @@ -110,4 +111,4 @@
<div id="reactRoot"></div>

</body>
</html>
</html>
Loading