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 @@
- + \ 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