diff --git a/Cargo.lock b/Cargo.lock index e2ec8bd..4142b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,7 +316,7 @@ dependencies = [ "futures", "hmac", "include_dir", - "itertools 0.12.1", + "itertools 0.13.0", "jsonwebtoken", "kiddo", "lazy_static", @@ -344,6 +344,7 @@ dependencies = [ "tempfile", "tokio", "url", + "urlencoding", "uuid", "validator", "wasm-bindgen", @@ -1732,6 +1733,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -4721,6 +4731,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" version = "1.8.0" diff --git a/Cargo.toml b/Cargo.toml index 54c5710..2f95caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,8 @@ hmac = "0.12.1" reqwest = { version = "0.12.3", features = ["json", "stream"] } base64 = "0.22.0" sha2 = "0.10.7" -itertools = "0.12.0" +itertools = "0.13.0" +urlencoding = "2.1.3" # Models openai-api-rs = "2.1.4" diff --git a/migrations/20240809_add_prompt_template_column.down.sql b/migrations/20240809_add_prompt_template_column.down.sql new file mode 100644 index 0000000..4f303c9 --- /dev/null +++ b/migrations/20240809_add_prompt_template_column.down.sql @@ -0,0 +1,3 @@ +-- Revert: 20240809_add_prompt_template_column.up.sql + +ALTER TABLE biomedgps_relation_metadata DROP COLUMN prompt_template; \ No newline at end of file diff --git a/migrations/20240809_add_prompt_template_column.up.sql b/migrations/20240809_add_prompt_template_column.up.sql new file mode 100644 index 0000000..fcdca79 --- /dev/null +++ b/migrations/20240809_add_prompt_template_column.up.sql @@ -0,0 +1,3 @@ +-- Add a prompt template column into the biomedgps_relation_metadata table for describing the prompt template of the relation type. +ALTER TABLE biomedgps_relation_metadata +ADD COLUMN prompt_template TEXT DEFAULT NULL; diff --git a/src/api/publication.rs b/src/api/publication.rs index 017e189..5610f32 100644 --- a/src/api/publication.rs +++ b/src/api/publication.rs @@ -1,7 +1,15 @@ use anyhow; +use log::info; use poem_openapi::Object; use reqwest; use serde::{Deserialize, Serialize}; +use urlencoding; + +const GUIDESCOPER_PUBLICATIONS_API: &str = "/api/paper_search/"; +const GUIDESCOPER_DETAILS_API: &str = "/api/papers/details/"; +const GUIDESCOPER_SUMMARY_API: &str = "/api/summary/?search_id="; +const GUIDESCOPER_CONSENSUS_API: &str = "/api/yes_no/?search_id="; +const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36"; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Object)] pub struct PublicationRecords { @@ -9,6 +17,7 @@ pub struct PublicationRecords { pub total: u64, pub page: u64, pub page_size: u64, + pub search_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Object)] @@ -20,24 +29,35 @@ pub struct Publication { pub title: String, pub year: Option, pub doc_id: String, + pub article_abstract: Option, + pub doi: Option, + pub provider_url: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Object)] -pub struct PublicationDetail { - pub authors: Vec, - pub citation_count: Option, +pub struct PublicationsSummary { pub summary: String, - pub journal: String, - pub title: String, - pub year: Option, - pub doc_id: String, - pub article_abstract: Option, - pub doi: Option, - pub provider_url: Option, + pub daily_limit_reached: bool, + pub is_disputed: bool, + pub is_incomplete: bool, + pub results_analyzed_count: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Object)] +pub struct ConsensusResult { + pub results_analyzed_count: u64, + pub yes_percent: f64, + pub no_percent: f64, + pub possibly_percent: f64, + pub yes_doc_ids: Vec, + pub no_doc_ids: Vec, + pub possibly_doc_ids: Vec, + pub is_incomplete: bool, + pub is_disputed: bool, } impl Publication { - pub async fn fetch_publication(id: &str) -> Result { + pub async fn fetch_publication(id: &str) -> Result { let api_token = match std::env::var("GUIDESCOPER_API_TOKEN") { Ok(token) => token, Err(_) => { @@ -45,17 +65,25 @@ impl Publication { } }; - let detail_api = match std::env::var("GUIDESCOPER_DETAIL_API") { + let guidescoper_server = match std::env::var("GUIDESCOPER_SERVER") { Ok(token) => token, Err(_) => { - return Err(anyhow::anyhow!("GUIDESCOPER_DETAIL_API not found")); + return Err(anyhow::anyhow!("GUIDESCOPER_SERVER not found")); } }; + let detail_api = format!("{}{}", guidescoper_server, GUIDESCOPER_DETAILS_API); + info!("detail_api: {}", detail_api); + let url = format!("{}{}", detail_api, id); let cookie = format!("_session={}", api_token); let client = reqwest::Client::new(); - let res = client.get(&url).header("Cookie", cookie).send().await?; + let res = client + .get(&url) + .header("Cookie", cookie) + .header("USER_AGENT", USER_AGENT) + .send() + .await?; if res.status().is_success() { let body = res.text().await?; @@ -76,7 +104,7 @@ impl Publication { let doi = json["doi"].as_str().map(|s| s.to_string()); let provider_url = json["provider_url"].as_str().map(|s| s.to_string()); - Ok(PublicationDetail { + Ok(Publication { authors: authors_vec, citation_count: citation_count, summary: summary, @@ -105,31 +133,48 @@ impl Publication { } }; - let guidescoper_api = match std::env::var("GUIDESCOPER_API") { + let guidescoper_server = match std::env::var("GUIDESCOPER_SERVER") { Ok(token) => token, Err(_) => { - return Err(anyhow::anyhow!("GUIDESCOPER_API not found")); + return Err(anyhow::anyhow!("GUIDESCOPER_SERVER not found")); } }; + let guidescoper_api = format!("{}{}", guidescoper_server, GUIDESCOPER_PUBLICATIONS_API); + info!("guidescoper_api: {}", guidescoper_api); + // We only need to fetch the top 10 results currently. - let total = 10; - let page = 0; - let page_size = 10; + let page = page.unwrap_or(1); + let page_size = page_size.unwrap_or(10); + let mut total = page_size; let mut records = Vec::new(); + let encoded_query_str = urlencoding::encode(query_str); let url = format!( "{}?query={}&page={}&size={}", - guidescoper_api, query_str, page, page_size + guidescoper_api, encoded_query_str, page, page_size ); + info!("Query url: {}", url); let cookie = format!("_session={}", api_token); let client = reqwest::Client::new(); - let res = client.get(&url).header("Cookie", cookie).send().await?; + let res = client + .get(&url) + .header("Cookie", cookie) + .header("USER_AGENT", USER_AGENT) + .send() + .await?; + + let mut search_id = String::new(); if res.status().is_success() { let body = res.text().await?; let json: serde_json::Value = serde_json::from_str(&body)?; + search_id = json["search_id"].as_str().unwrap().to_string(); + total = json["numTopResults"].as_u64().unwrap(); + // TODO: do we need to add the adjusted query into the response? It seems not necessary? + // let query_str = json["adjustedQuery"].as_str().unwrap().to_string(); let items = json["papers"].as_array().unwrap(); + for item in items { let authors = item["authors"].as_array().unwrap(); let mut authors_vec = Vec::new(); @@ -142,6 +187,8 @@ impl Publication { let title = item["title"].as_str().unwrap().to_string(); let year = item["year"].as_u64(); let doc_id = item["doc_id"].as_str().unwrap().to_string(); + let doi_id = item["doi"].as_str().unwrap().to_string(); + records.push(Publication { authors: authors_vec, citation_count: citation_count, @@ -150,8 +197,14 @@ impl Publication { title: title, year: year, doc_id: doc_id, + article_abstract: None, + doi: Some(doi_id), + provider_url: None, }); } + } else { + let err_msg = format!("Failed to fetch publications: {}", res.text().await?); + return Err(anyhow::anyhow!(err_msg)); } Ok(PublicationRecords { @@ -159,6 +212,129 @@ impl Publication { total: total, page: page, page_size: page_size, + search_id: Some(search_id), }) } + + pub async fn fetch_summary(search_id: &str) -> Result { + let api_token = match std::env::var("GUIDESCOPER_API_TOKEN") { + Ok(token) => token, + Err(_) => { + return Err(anyhow::anyhow!("GUIDESCOPER_API_TOKEN not found")); + } + }; + + let guidescoper_server = match std::env::var("GUIDESCOPER_SERVER") { + Ok(token) => token, + Err(_) => { + return Err(anyhow::anyhow!("GUIDESCOPER_SERVER not found")); + } + }; + + let summary_api = format!("{}{}", guidescoper_server, GUIDESCOPER_SUMMARY_API); + + let url = format!("{}{}", summary_api, search_id); + let cookie = format!("_session={}", api_token); + let client = reqwest::Client::new(); + let res = client + .get(&url) + .header("Cookie", cookie) + .header("USER_AGENT", USER_AGENT) + .send() + .await?; + + if res.status().is_success() { + let body = res.text().await?; + let json: serde_json::Value = serde_json::from_str(&body)?; + let summary = json["summary"].as_str().unwrap().to_string(); + let daily_limit_reached = json["dailyLimitReached"].as_bool().unwrap(); + let is_disputed = json["isDisputed"].as_bool().unwrap(); + let is_incomplete = json["isIncomplete"].as_bool().unwrap(); + let results_analyzed_count = json["resultsAnalyzedCount"].as_u64().unwrap(); + + Ok(PublicationsSummary { + summary: summary, + daily_limit_reached: daily_limit_reached, + is_disputed: is_disputed, + is_incomplete: is_incomplete, + results_analyzed_count: results_analyzed_count, + }) + } else { + let err_msg = format!("Failed to fetch summary: {}", res.text().await?); + Err(anyhow::anyhow!(err_msg)) + } + } + + pub async fn fetch_consensus(search_id: &str) -> Result { + let api_token = match std::env::var("GUIDESCOPER_API_TOKEN") { + Ok(token) => token, + Err(_) => { + return Err(anyhow::anyhow!("GUIDESCOPER_API_TOKEN not found")); + } + }; + + let guidescoper_server = match std::env::var("GUIDESCOPER_SERVER") { + Ok(token) => token, + Err(_) => { + return Err(anyhow::anyhow!("GUIDESCOPER_SERVER not found")); + } + }; + + let consensus_api = format!("{}{}", guidescoper_server, GUIDESCOPER_CONSENSUS_API); + + let url = format!("{}{}", consensus_api, search_id); + let cookie = format!("_session={}", api_token); + let client = reqwest::Client::new(); + let res = client + .get(&url) + .header("Cookie", cookie) + .header("USER_AGENT", USER_AGENT) + .send() + .await?; + + if res.status().is_success() { + let body = res.text().await?; + let json: serde_json::Value = serde_json::from_str(&body)?; + + let results_analyzed_count = json["resultsAnalyzedCount"].as_u64().unwrap(); + + let yes_no_answer_percents = &json["yesNoAnswerPercents"]; + let yes_percent = yes_no_answer_percents["YES"].as_f64().unwrap(); + let no_percent = yes_no_answer_percents["NO"].as_f64().unwrap(); + let possibly_percent = yes_no_answer_percents["POSSIBLY"].as_f64().unwrap(); + + let result_id_to_yes_no_answer = json["resultIdToYesNoAnswer"].as_object().unwrap(); + + let mut yes_doc_ids_vec = Vec::new(); + let mut no_doc_ids_vec = Vec::new(); + let mut possibly_doc_ids_vec = Vec::new(); + + for (doc_id, answer) in result_id_to_yes_no_answer { + match answer.as_str().unwrap() { + "YES" => yes_doc_ids_vec.push(doc_id.clone()), + "NO" => no_doc_ids_vec.push(doc_id.clone()), + "POSSIBLY" => possibly_doc_ids_vec.push(doc_id.clone()), + _ => {} + } + } + + let is_incomplete = json["isIncomplete"].as_bool().unwrap(); + let is_disputed = json["isDisputed"].as_bool().unwrap(); + + Ok(ConsensusResult { + results_analyzed_count: results_analyzed_count, + yes_percent: yes_percent, + no_percent: no_percent, + possibly_percent: possibly_percent, + yes_doc_ids: yes_doc_ids_vec, + no_doc_ids: no_doc_ids_vec, + possibly_doc_ids: possibly_doc_ids_vec, + is_incomplete: is_incomplete, + is_disputed: is_disputed, + }) + } else { + let err_msg = format!("Failed to fetch consensus: {}", res.text().await?); + Err(anyhow::anyhow!(err_msg)) + } + } } diff --git a/src/api/route.rs b/src/api/route.rs index 48a0d5e..4faf3b4 100644 --- a/src/api/route.rs +++ b/src/api/route.rs @@ -3,8 +3,9 @@ use crate::api::auth::{CustomSecurityScheme, USERNAME_PLACEHOLDER}; use crate::api::publication::Publication; use crate::api::schema::{ - ApiTags, DeleteResponse, GetEntityAttrResponse, GetEntityColorMapResponse, GetGraphResponse, - GetPromptResponse, GetPublicationsResponse, GetRecordsResponse, GetRelationCountResponse, + ApiTags, DeleteResponse, GetConsensusResultResponse, GetEntityAttrResponse, + GetEntityColorMapResponse, GetGraphResponse, GetPromptResponse, GetPublicationsResponse, + GetPublicationsSummaryResponse, GetRecordsResponse, GetRelationCountResponse, GetStatisticsResponse, GetWholeTableResponse, NodeIdsQuery, Pagination, PaginationQuery, PostResponse, PredictedNodeQuery, PromptList, SubgraphIdQuery, }; @@ -33,6 +34,52 @@ pub struct BiomedgpsApi; #[OpenApi(prefix_path = "/api/v1")] impl BiomedgpsApi { + /// Call `/api/v1/publications-summary` with query params to fetch publication summary. + #[oai( + path = "/publications-summary/:search_id", + method = "get", + tag = "ApiTags::KnowledgeGraph", + operation_id = "fetchPublicationsSummary" + )] + async fn fetch_publications_summary( + &self, + search_id: Path, + _token: CustomSecurityScheme, + ) -> GetPublicationsSummaryResponse { + let search_id = search_id.0; + match Publication::fetch_summary(&search_id).await { + Ok(result) => GetPublicationsSummaryResponse::ok(result), + Err(e) => { + let err = format!("Failed to fetch publications summary: {}", e); + warn!("{}", err); + return GetPublicationsSummaryResponse::bad_request(err); + } + } + } + + /// Call `/api/v1/publications-consensus` with query params to fetch publication consensus. + #[oai( + path = "/publications-consensus/:search_id", + method = "get", + tag = "ApiTags::KnowledgeGraph", + operation_id = "fetchPublicationsConsensus" + )] + async fn fetch_publications_consensus( + &self, + search_id: Path, + _token: CustomSecurityScheme, + ) -> GetConsensusResultResponse { + let search_id = search_id.0; + match Publication::fetch_consensus(&search_id).await { + Ok(result) => GetConsensusResultResponse::ok(result), + Err(e) => { + let err = format!("Failed to fetch publications consensus: {}", e); + warn!("{}", err); + return GetConsensusResultResponse::bad_request(err); + } + } + } + /// Call `/api/v1/publications/:id` to fetch a publication. #[oai( path = "/publications/:id", @@ -70,14 +117,6 @@ impl BiomedgpsApi { let page_size = page_size.0; info!("Fetch publications with query: {}", query_str); - let pairs = query_str.split("#").collect::>(); - let query_str = if pairs.len() != 2 { - let err = format!("Invalid query string: {}", query_str); - warn!("{}", err); - return GetPublicationsResponse::bad_request(err); - } else { - format!("what's the relation between {} and {}?", pairs[0], pairs[1]) - }; match Publication::fetch_publications(&query_str, page, page_size).await { Ok(records) => GetPublicationsResponse::ok(records), @@ -861,7 +900,7 @@ impl BiomedgpsApi { ) .await { - Ok(entities) => GetRecordsResponse::ok(entities), + Ok(records) => GetRecordsResponse::ok(records), Err(e) => { let err = format!("Failed to fetch relations: {}", e); warn!("{}", err); diff --git a/src/api/schema.rs b/src/api/schema.rs index 36a14d6..edab732 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; use validator::Validate; use validator::ValidationErrors; -use super::publication::{PublicationDetail, PublicationRecords}; +use super::publication::{Publication, PublicationRecords, PublicationsSummary, ConsensusResult}; #[derive(Tags)] pub enum ApiTags { @@ -214,10 +214,62 @@ impl< } } +#[derive(ApiResponse)] +pub enum GetPublicationsSummaryResponse { + #[oai(status = 200)] + Ok(Json), + + #[oai(status = 400)] + BadRequest(Json), + + #[oai(status = 404)] + NotFound(Json), +} + +impl GetPublicationsSummaryResponse { + pub fn ok(publications_summary: PublicationsSummary) -> Self { + Self::Ok(Json(publications_summary)) + } + + pub fn bad_request(msg: String) -> Self { + Self::BadRequest(Json(ErrorMessage { msg })) + } + + pub fn not_found(msg: String) -> Self { + Self::NotFound(Json(ErrorMessage { msg })) + } +} + +#[derive(ApiResponse)] +pub enum GetConsensusResultResponse { + #[oai(status = 200)] + Ok(Json), + + #[oai(status = 400)] + BadRequest(Json), + + #[oai(status = 404)] + NotFound(Json), +} + +impl GetConsensusResultResponse { + pub fn ok(consensus_result: ConsensusResult) -> Self { + Self::Ok(Json(consensus_result)) + } + + pub fn bad_request(msg: String) -> Self { + Self::BadRequest(Json(ErrorMessage { msg })) + } + + pub fn not_found(msg: String) -> Self { + Self::NotFound(Json(ErrorMessage { msg })) + } +} + #[derive(ApiResponse)] pub enum GetPublicationDetailResponse { #[oai(status = 200)] - Ok(Json), + Ok(Json), #[oai(status = 400)] BadRequest(Json), @@ -227,7 +279,7 @@ pub enum GetPublicationDetailResponse { } impl GetPublicationDetailResponse { - pub fn ok(publication_detail: PublicationDetail) -> Self { + pub fn ok(publication_detail: Publication) -> Self { Self::Ok(Json(publication_detail)) } diff --git a/src/model/core.rs b/src/model/core.rs index a3ea4ed..57207da 100644 --- a/src/model/core.rs +++ b/src/model/core.rs @@ -721,6 +721,10 @@ pub struct RelationMetadata { // To describe the relation type with a human-readable sentence. #[oai(skip_serializing_if_is_none)] pub description: Option, + + // Prompt Template + #[oai(skip_serializing_if_is_none)] + pub prompt_template: Option, } impl CheckData for RelationMetadata { @@ -749,6 +753,7 @@ impl CheckData for RelationMetadata { "start_entity_type".to_string(), "end_entity_type".to_string(), "description".to_string(), + "prompt_template".to_string(), ] } } diff --git a/src/model/mod.rs b/src/model/mod.rs index 5cf183a..82d3543 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -7,4 +7,4 @@ pub mod llm; pub mod kge; pub mod init_db; pub mod entity; -pub mod entity_attr; +pub mod entity_attr; \ No newline at end of file diff --git a/src/model/util.rs b/src/model/util.rs index bd6803d..9fdf5fc 100644 --- a/src/model/util.rs +++ b/src/model/util.rs @@ -339,6 +339,7 @@ pub async fn update_entity_metadata(pool: &sqlx::PgPool, drop: bool) -> Result<( struct RelationMetadata { relation_type: String, description: String, + prompt_template: String, } pub async fn update_relation_metadata( @@ -359,7 +360,7 @@ pub async fn update_relation_metadata( .from_path(metadata_filepath)?; let headers = reader.headers().unwrap(); - for col in ["relation_type", "description"].iter() { + for col in ["relation_type", "description", "prompt_template"].iter() { if !headers.into_iter().contains(col) { return Err(format!( "Column {} not found in the {} file. You should specify a file with the columns 'relation_type' and 'description' for annotating the relation types in the relation table.", @@ -396,11 +397,12 @@ pub async fn update_relation_metadata( sqlx::query( " UPDATE biomedgps_relation_metadata - SET description = $1 - WHERE relation_type = $2; + SET description = $1, prompt_template = $2 + WHERE relation_type = $3; ", ) .bind(record.description) + .bind(record.prompt_template) .bind(record.relation_type) .execute(&mut tx) .await?; diff --git a/studio/package.json b/studio/package.json index 682cede..e5456f8 100644 --- a/studio/package.json +++ b/studio/package.json @@ -38,7 +38,7 @@ "antd": "5.8.0", "antd-schema-form": "^4.5.1", "axios": "^1.1.2", - "biominer-components": "0.3.21", + "biominer-components": "0.3.22", "biomsa": "^0.3.3", "bootstrap": "^5.3.3", "classnames": "^2.3.0", diff --git a/studio/plugin.ts b/studio/plugin.ts index 80b331d..cae9ee0 100644 --- a/studio/plugin.ts +++ b/studio/plugin.ts @@ -23,8 +23,12 @@ export default (api: IApi) => { // For GTEx Components api.addHTMLHeadScripts(() => { return [ + // assets is the prefix for the static files in the production build "/assets/js/jquery-1.11.2.min.js", "/assets/js/popper-1.11.0.min.js", + // For development, we use the local version of jquery-ui + "/js/jquery-1.11.2.min.js", + "/js/popper-1.11.0.min.js", "https://gtexportal.org/external/jquery-ui-1.11.4.custom/jquery-ui.min.js", "https://gtexportal.org/external/bootstrap/3.3.7/bootstrap.min.js" ] diff --git a/studio/src/EdgeInfoPanel/PublicationPanel.tsx b/studio/src/EdgeInfoPanel/PublicationPanel.tsx index 2e7ab28..06eadce 100644 --- a/studio/src/EdgeInfoPanel/PublicationPanel.tsx +++ b/studio/src/EdgeInfoPanel/PublicationPanel.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { Button, List, message } from 'antd'; +import { Button, List, message, Row, Col, Tag } from 'antd'; import { FileProtectOutlined } from '@ant-design/icons'; import type { Publication, PublicationDetail } from 'biominer-components/dist/typings'; import PublicationDesc from './PublicationDesc'; -import { fetchPublication, fetchPublications } from '@/services/swagger/KnowledgeGraph'; +import { fetchPublication, fetchPublications, fetchPublicationsSummary } from '@/services/swagger/KnowledgeGraph'; import './index.less'; @@ -18,6 +18,8 @@ const PublicationPanel: React.FC = (props) => { const [pageSize, setPageSize] = useState(10); const [loading, setLoading] = useState(false); const [publicationMap, setPublicationMap] = useState>({}); + const [searchId, setSearchId] = useState(''); + const [publicationSummary, setPublicationSummary] = useState(''); const showAbstract = (doc_id: string): Promise => { console.log('Show Abstract: ', doc_id); @@ -47,11 +49,16 @@ const PublicationPanel: React.FC = (props) => { query_str: props.queryStr, page: 0, page_size: 10 - }).then((publications) => { - setPublications(publications.records); - setPage(publications.page); - setTotal(publications.total); - setPageSize(publications.page_size); + }).then((data) => { + setSearchId(data.search_id || ''); + if (data.search_id) { + fetchPublicationSummary(data.search_id); + } + + setPublications(data.records); + setPage(data.page); + setTotal(data.total); + setPageSize(data.page_size); }).catch((error) => { console.error('Error: ', error); message.error('Failed to fetch publications'); @@ -77,6 +84,17 @@ const PublicationPanel: React.FC = (props) => { } }; + const fetchPublicationSummary = async (searchId: string) => { + const response = await fetchPublicationsSummary({ + search_id: searchId + }) + + if (response && response.summary) { + const summary = response.summary; + setPublicationSummary(summary); + } + } + const onClickPublication = (item: Publication) => { if (publicationMap[item.doc_id]) { showPublication(publicationMap[item.doc_id]) @@ -90,45 +108,51 @@ const PublicationPanel: React.FC = (props) => { } return ( - <> -
-

- Top 10 Relevant Publications [Keywords: {props.queryStr.split('#').join(', ')}] -

-
- { - setPage(page); - setPageSize(pageSize); - } - }} - renderItem={(item, index) => ( - - } - title={ { onClickPublication(item); }}>{item.title}} - description={ - onClickPublication(publication)} - /> - } - /> - - )} - /> - + + Question + + Q: {props.queryStr} +

+ A: {publicationSummary.length > 0 ? publicationSummary : `No AI summary for the above question.`} +

+ + + References + + { + setPage(page); + setPageSize(pageSize); + } + }} + renderItem={(item, index) => ( + + } + title={ { onClickPublication(item); }}>{item.title}} + description={ + onClickPublication(publication)} + /> + } + /> + + )} + /> + +
); }; diff --git a/studio/src/EdgeInfoPanel/index.less b/studio/src/EdgeInfoPanel/index.less index 3e5a07e..e0539d7 100644 --- a/studio/src/EdgeInfoPanel/index.less +++ b/studio/src/EdgeInfoPanel/index.less @@ -18,19 +18,31 @@ height: 100%; } - .publication-panel-header { - position: absolute; - top: 5px; - left: 22px; - z-index: 100; - - h3 { - margin: 0; + .publication-panel { + display: flex; + flex-direction: column; + + .publication-tag { + font-size: 1rem; + margin-left: 10px; + padding: 10px; + } + + .publication-panel-header { + font-size: 1rem; + padding: 20px; + } + + .publication-panel-content { + .ant-list-item { + padding: 16px 10px; + } } } .ant-list { .ant-list-pagination { + display: none; margin-block-start: 0; } } @@ -38,4 +50,4 @@ .highlight { color: #1890ff; } -} +} \ No newline at end of file diff --git a/studio/src/EdgeInfoPanel/index.tsx b/studio/src/EdgeInfoPanel/index.tsx index b4ba0f9..d9635f8 100644 --- a/studio/src/EdgeInfoPanel/index.tsx +++ b/studio/src/EdgeInfoPanel/index.tsx @@ -1,12 +1,11 @@ import React, { useEffect, useState } from 'react'; import { Row } from 'antd'; -import type { EdgeInfoPanelProps } from './index.t'; import DrugGene from './DrugGenePanel'; import DrugDisease from './DrugDiseasePanel'; import GeneDisease from './DiseaseGenePanel'; import PublicationPanel from './PublicationPanel'; -import { SEPARATOR } from './PublicationDesc'; import CommonPanel from './CommonPanel'; +import type { EdgeInfoPanelProps } from './index.t'; import './index.less'; @@ -18,30 +17,46 @@ const EdgeInfoPanel: React.FC = (props) => { }; const [relationType, setRelationType] = useState('Unknown'); - const whichPanel = (relationType: string) => { - console.log('whichPanel: ', relationType); - let queryStr = ''; - if (startNode && endNode) { - queryStr = startNode.data.name + SEPARATOR + endNode.data.name; + const format_prompt = (prompt_template: string, source_type: string, + source_name: string, target_type: string, target_name: string) => { + if (source_type == target_type) { + return prompt_template.replace(`#${source_type}1#`, source_name).replace(`#${target_type}2#`, target_name) + } else { + return prompt_template.replace(`#${source_type}#`, source_name).replace(`#${target_type}#`, target_name) } + } - switch (relationType) { - case 'DrugDisease': - return - - ; - case 'DrugGene': - return - - ; - case 'GeneDisease': - return - - ; - default: - return - - ; + const whichPanel = () => { + if (relationType !== 'Unknown') { + console.log('whichPanel: ', relationType, edge, startNode, endNode); + let queryStr = ''; + if (startNode && endNode) { + queryStr = edge.prompt_template ? + format_prompt(edge.prompt_template, startNode.data.label, + startNode.data.name, endNode.data.label, endNode.data.name) : + `${edge.description}; ${edge.data.source_type}: ${startNode.data.name}, ${edge.data.target_type}: ${endNode.data.name}` + } + + if (queryStr) { + switch (relationType) { + case 'DrugDisease': + return + + ; + case 'DrugGene': + return + + ; + case 'GeneDisease': + return + + ; + default: + return + + ; + } + } } }; @@ -70,7 +85,7 @@ const EdgeInfoPanel: React.FC = (props) => { } }, [edge, startNode, endNode]); - return {whichPanel(relationType)}; + return {whichPanel()}; }; export default EdgeInfoPanel; diff --git a/studio/src/pages/KnowledgeTable/index.tsx b/studio/src/pages/KnowledgeTable/index.tsx index f245891..e8a59ee 100644 --- a/studio/src/pages/KnowledgeTable/index.tsx +++ b/studio/src/pages/KnowledgeTable/index.tsx @@ -123,6 +123,7 @@ const KnowledgeTable: React.FC = (props) => { const [relationTypeOptions, setRelationTypeOptions] = useState([]); const [selectedRelationTypes, setSelectedRelationTypes] = useState([]); const [relationTypeDescs, setRelationTypeDescs] = useState>({}); + const [relationTypePrompts, setRelationTypePrompts] = useState>({}); const [resources, setResources] = useState([]); const [selectedResources, setSelectedResources] = useState([]); @@ -168,6 +169,7 @@ const KnowledgeTable: React.FC = (props) => { }); setRelationTypeDescs(descs); + let prompts = {} as Record; let res = [] as OptionType[]; relationStat.forEach((item, index) => { res.push({ @@ -175,8 +177,11 @@ const KnowledgeTable: React.FC = (props) => { label: item.resource, value: item.resource, }); + + prompts[item.relation_type] = item.prompt_template || ''; }); + setRelationTypePrompts(prompts); setResources(uniqBy(res, 'value')); }); } @@ -593,6 +598,9 @@ const KnowledgeTable: React.FC = (props) => { newItem.source_node = response.nodes.find((node) => node.data.id === item.source_id); newItem.target_name = targetName; newItem.target_node = response.nodes.find((node) => node.data.id === item.target_id); + // Summarizing related publications need the prompt template and description. Prefer the prompt template, if not, use the description. + newItem.prompt_template = relationTypePrompts[item.relation_type] || ''; + newItem.description = relationTypeDescs[item.relation_type] || ''; return newItem; }) @@ -610,8 +618,10 @@ const KnowledgeTable: React.FC = (props) => { return; } - fetchTableData(nodeIds, page, pageSize, selectedRelationTypes, selectedResources); - }, [nodeIds, page, pageSize, refreshKey]); + if (Object.keys(relationTypePrompts).length > 0) { + fetchTableData(nodeIds, page, pageSize, selectedRelationTypes, selectedResources); + } + }, [nodeIds, page, pageSize, refreshKey, relationTypePrompts]); const getRowKey = (record: GraphEdge) => { return record.relid || `${JSON.stringify(record)}`; @@ -863,11 +873,11 @@ const KnowledgeTable: React.FC = (props) => { placement={'right'} onClose={() => { setDrawerVisible(false); - } - } + }} + destroyOnClose={true} open={drawerVisible} > - {edgeInfo ? + {(drawerVisible && edgeInfo) ? : } diff --git a/studio/src/services/swagger/KnowledgeGraph.ts b/studio/src/services/swagger/KnowledgeGraph.ts index b823164..77aeca8 100644 --- a/studio/src/services/swagger/KnowledgeGraph.ts +++ b/studio/src/services/swagger/KnowledgeGraph.ts @@ -274,6 +274,34 @@ export async function fetchPublications( }); } +/** Call `/api/v1/publications-consensus` with query params to fetch publication consensus. GET /api/v1/publications-consensus/${param0} */ +export async function fetchPublicationsConsensus( + // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) + params: swagger.fetchPublicationsConsensusParams, + options?: { [key: string]: any }, +) { + const { search_id: param0, ...queryParams } = params; + return request(`/api/v1/publications-consensus/${param0}`, { + method: 'GET', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Call `/api/v1/publications-summary` with query params to fetch publication summary. GET /api/v1/publications-summary/${param0} */ +export async function fetchPublicationsSummary( + // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) + params: swagger.fetchPublicationsSummaryParams, + options?: { [key: string]: any }, +) { + const { search_id: param0, ...queryParams } = params; + return request(`/api/v1/publications-summary/${param0}`, { + method: 'GET', + params: { ...queryParams }, + ...(options || {}), + }); +} + /** Call `/api/v1/publications/:id` to fetch a publication. GET /api/v1/publications/${param0} */ export async function fetchPublication( // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) @@ -281,7 +309,7 @@ export async function fetchPublication( options?: { [key: string]: any }, ) { const { id: param0, ...queryParams } = params; - return request(`/api/v1/publications/${param0}`, { + return request(`/api/v1/publications/${param0}`, { method: 'GET', params: { ...queryParams }, ...(options || {}), diff --git a/studio/src/services/swagger/index.ts b/studio/src/services/swagger/index.ts index d8d5061..ad314f1 100644 --- a/studio/src/services/swagger/index.ts +++ b/studio/src/services/swagger/index.ts @@ -2,7 +2,7 @@ /* eslint-disable */ // API 更新时间: // API 唯一标识: -import * as knowledgeGraph from './KnowledgeGraph'; +import * as knowledgeGraph from './knowledgeGraph'; export default { knowledgeGraph, }; diff --git a/studio/src/services/swagger/typings.d.ts b/studio/src/services/swagger/typings.d.ts index 30e4c72..c2398f3 100644 --- a/studio/src/services/swagger/typings.d.ts +++ b/studio/src/services/swagger/typings.d.ts @@ -1,4 +1,4 @@ -export declare namespace swagger { +declare namespace swagger { type Article = { ref_id: string; pubmed_id: string; @@ -33,7 +33,7 @@ export declare namespace swagger { subclass: string; }; - export type CompoundAttr = { + type CompoundAttr = { compound_type: string; created: string; updated: string; @@ -77,6 +77,18 @@ export declare namespace swagger { targets: Target[]; }; + type ConsensusResult = { + results_analyzed_count: number; + yes_percent: number; + no_percent: number; + possibly_percent: number; + yes_doc_ids: string[]; + no_doc_ids: string[]; + possibly_doc_ids: string[]; + is_incomplete: boolean; + is_disputed: boolean; + }; + type Context = { entity?: Entity; expanded_relation?: ExpandedRelation; @@ -284,12 +296,20 @@ export declare namespace swagger { id: string; }; + type fetchPublicationsConsensusParams = { + search_id: string; + }; + type fetchPublicationsParams = { query_str: string; page?: number; page_size?: number; }; + type fetchPublicationsSummaryParams = { + search_id: string; + }; + type fetchRelationCountsParams = { query_str?: string; }; @@ -519,16 +539,6 @@ export declare namespace swagger { title: string; year?: number; doc_id: string; - }; - - type PublicationDetail = { - authors: string[]; - citation_count?: number; - summary: string; - journal: string; - title: string; - year?: number; - doc_id: string; article_abstract?: string; doi?: string; provider_url?: string; @@ -539,6 +549,15 @@ export declare namespace swagger { total: number; page: number; page_size: number; + search_id?: string; + }; + + type PublicationsSummary = { + summary: string; + daily_limit_reached: boolean; + is_disputed: boolean; + is_incomplete: boolean; + results_analyzed_count: number; }; type putCuratedKnowledgeParams = { @@ -636,6 +655,7 @@ export declare namespace swagger { start_entity_type: string; end_entity_type: string; description?: string; + prompt_template?: string; }; type Sequence = { diff --git a/studio/yarn.lock b/studio/yarn.lock index 4bb7a30..e5e7c34 100644 --- a/studio/yarn.lock +++ b/studio/yarn.lock @@ -5518,22 +5518,22 @@ bio.io@^1.0.6: xhr "^2.2.0" xmldoc "^0.5.1" -biomedgps-graph@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/biomedgps-graph/-/biomedgps-graph-0.1.0.tgz#bb864f0f4154fb2ea2b3627e839afd9647bdcc67" - integrity sha512-wS1F738hXdSCFP8eqBrw0NPHtIKl24GPv9uPPmJ2OEWBYymj5XR5zgGwwJt40aEa59S7GpRURCP7XsbAmct8Sg== +biomedgps-graph@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/biomedgps-graph/-/biomedgps-graph-0.2.0.tgz#7b8100e86eb89235cb218c7ff5c7553ba2ca9c2a" + integrity sha512-NqcNRcLmWenzZFrWkfNhWqpF/XIqVnpa9n46znrYo9lj9AaAZ4p3rGr6h1l2gulbRi5kh5Spv4I6iNbb+zDuhQ== -biominer-components@0.3.21: - version "0.3.21" - resolved "https://registry.yarnpkg.com/biominer-components/-/biominer-components-0.3.21.tgz#392d66f0ae7629945a5625c92588e299cf93b00f" - integrity sha512-JwSGFNVuolHW5PfWsu6cyagwgGkZ2gxWOdvZoqwmJlDV4BKjqB6yzT9dnRRV7Gv0MztDHQ2GqZo+Qr2+eTEy7w== +biominer-components@0.3.22: + version "0.3.22" + resolved "https://registry.yarnpkg.com/biominer-components/-/biominer-components-0.3.22.tgz#cb2a8ba6df2dfb65ec3b53087626419325b706f2" + integrity sha512-nrxgHiO+4wTc1zjIlP9mZHN2XMLsJ+tzqIWxdX5R+ohT+HsZ/vWBZHXhg4585khU4IBcVddLNnpsPGmhz8cAOQ== dependencies: "@fingerprintjs/fingerprintjs" "^4.2.2" ag-grid-community "^31.0.1" ag-grid-enterprise "^31.0.1" ag-grid-react "^31.0.1" axios "^1.6.7" - biomedgps-graph "^0.1.0" + biomedgps-graph "^0.2.0" html-react-parser "^5.1.8" lodash "^4.17.21" moment "^2.30.1"