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 @@
- + \ No newline at end of file diff --git a/src/main/resources/webapp/index.jsp b/src/main/resources/webapp/index.jsp index b66f7a3693f..dc27ed79f48 100644 --- a/src/main/resources/webapp/index.jsp +++ b/src/main/resources/webapp/index.jsp @@ -98,7 +98,8 @@ <%@include file="./tracking_include.jsp" %> - + + @@ -110,4 +111,4 @@
- + \ No newline at end of file