From 44541a494065f9b26066f2d389c445ba8f67d996 Mon Sep 17 00:00:00 2001 From: Jingcheng Yang Date: Sun, 17 Mar 2024 17:22:25 -0400 Subject: [PATCH] Add a publication panel for each edge. --- src/api/mod.rs | 3 +- src/api/req.rs | 155 ++++++++++++++++++ src/api/route.rs | 61 ++++++- src/api/schema.rs | 54 ++++++ src/bin/biomedgps.rs | 7 +- studio/package.json | 2 +- studio/src/components/Header/index.tsx | 22 ++- studio/src/pages/KnowledgeGraph/index.tsx | 16 +- studio/src/services/swagger/KnowledgeGraph.ts | 29 ++++ studio/src/services/swagger/typings.d.ts | 42 ++++- studio/yarn.lock | 90 ++++++++-- 11 files changed, 449 insertions(+), 32 deletions(-) create mode 100644 src/api/req.rs diff --git a/src/api/mod.rs b/src/api/mod.rs index ad0bae2..b48d441 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,4 +2,5 @@ pub mod route; pub mod schema; -pub mod auth; \ No newline at end of file +pub mod auth; +pub mod req; diff --git a/src/api/req.rs b/src/api/req.rs new file mode 100644 index 0000000..e705140 --- /dev/null +++ b/src/api/req.rs @@ -0,0 +1,155 @@ +use anyhow; +use poem_openapi::{types::ToJSON, Object}; +use reqwest; +use serde::{Deserialize, Serialize}; + +const CONSENSUS_API: &str = "https://consensus.app/api/paper_search/"; +const CONSENSUS_DETAIL_API: &str = "https://consensus.app/api/papers/details/"; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Object)] +pub struct PublicationRecords { + pub records: Vec, + pub total: u64, + pub page: u64, + pub page_size: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Object)] +pub struct Publication { + pub authors: Vec, + pub citation_count: Option, + pub summary: String, + pub journal: String, + pub title: String, + pub year: Option, + pub doc_id: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Object)] +pub struct PublicationDetail { + pub authors: Vec, + pub citation_count: Option, + 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, +} + +impl Publication { + pub async fn fetch_publication( + id: &str, + ) -> Result { + let api_token = match std::env::var("CONSENSUS_API_TOKEN") { + Ok(token) => token, + Err(_) => { + return Err(anyhow::anyhow!("CONSENSUS_API_TOKEN not found")); + } + }; + + let url = format!("{}{}", CONSENSUS_DETAIL_API, id); + let cookie = format!("_session={}", api_token); + let client = reqwest::Client::new(); + let res = client.get(&url).header("Cookie", cookie).send().await?; + + if res.status().is_success() { + let body = res.text().await?; + let json: serde_json::Value = serde_json::from_str(&body)?; + let authors = json["authors"].as_array().unwrap(); + let mut authors_vec = Vec::new(); + for author in authors { + authors_vec.push(author.as_str().unwrap().to_string()); + } + let citation_count = json["citation_count"].as_u64(); + let summary = json["abstract_takeaway"].as_str().unwrap().to_string(); + // Such as { "journal": { "title": "The Journal of biological chemistry","scimago_quartile": 1 }} + let journal = json["journal"]["title"].as_str().unwrap().to_string(); + let title = json["title"].as_str().unwrap().to_string(); + let year = json["year"].as_u64(); + let doc_id = json["id"].as_str().unwrap().to_string(); + let article_abstract = json["abstract"].as_str().map(|s| s.to_string()); + 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 { + authors: authors_vec, + citation_count: citation_count, + summary: summary, + journal: journal, + title: title, + year: year, + doc_id: doc_id, + article_abstract: article_abstract, + doi: doi, + provider_url: provider_url, + }) + } else { + Err(anyhow::anyhow!("Failed to fetch publication")) + } + } + + pub async fn fetch_publications( + query_str: &str, + page: Option, + page_size: Option, + ) -> Result { + let api_token = match std::env::var("CONSENSUS_API_TOKEN") { + Ok(token) => token, + Err(_) => { + return Err(anyhow::anyhow!("CONSENSUS_API_TOKEN not found")); + } + }; + + // We only need to fetch the top 10 results currently. + let total = 10; + let page = 0; + let page_size = 10; + + let mut records = Vec::new(); + let url = format!( + "{}?query={}&page={}&size={}", + CONSENSUS_API, query_str, page, page_size + ); + let cookie = format!("_session={}", api_token); + let client = reqwest::Client::new(); + let res = client.get(&url).header("Cookie", cookie).send().await?; + + if res.status().is_success() { + let body = res.text().await?; + let json: serde_json::Value = serde_json::from_str(&body)?; + let items = json["papers"].as_array().unwrap(); + for item in items { + let authors = item["authors"].as_array().unwrap(); + let mut authors_vec = Vec::new(); + for author in authors { + authors_vec.push(author.as_str().unwrap().to_string()); + } + let citation_count = item["citation_count"].as_u64(); + let summary = item["display_text"].as_str().unwrap().to_string(); + let journal = item["journal"].as_str().unwrap().to_string(); + 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(); + records.push(Publication { + authors: authors_vec, + citation_count: citation_count, + summary: summary, + journal: journal, + title: title, + year: year, + doc_id: doc_id, + }); + } + } + + Ok(PublicationRecords { + records: records, + total: total, + page: page, + page_size: page_size, + }) + } +} diff --git a/src/api/route.rs b/src/api/route.rs index 0059fdf..d64da5a 100644 --- a/src/api/route.rs +++ b/src/api/route.rs @@ -1,11 +1,12 @@ //! This module defines the routes of the API. use crate::api::auth::{CustomSecurityScheme, USERNAME_PLACEHOLDER}; +use crate::api::req::Publication; use crate::api::schema::{ ApiTags, DeleteResponse, GetEntityColorMapResponse, GetGraphResponse, GetPromptResponse, - GetRecordsResponse, GetRelationCountResponse, GetStatisticsResponse, GetWholeTableResponse, - NodeIdsQuery, Pagination, PaginationQuery, PostResponse, PredictedNodeQuery, PromptList, - SubgraphIdQuery, + GetPublicationsResponse, GetRecordsResponse, GetRelationCountResponse, GetStatisticsResponse, + GetWholeTableResponse, NodeIdsQuery, Pagination, PaginationQuery, PostResponse, + PredictedNodeQuery, PromptList, SubgraphIdQuery, }; use crate::model::core::{ Entity, Entity2D, EntityMetadata, KnowledgeCuration, RecordResponse, Relation, RelationCount, @@ -24,10 +25,58 @@ use poem_openapi::{param::Path, param::Query, payload::Json, OpenApi}; use std::sync::Arc; use validator::Validate; +use super::schema::GetPublicationDetailResponse; + pub struct BiomedgpsApi; #[OpenApi(prefix_path = "/api/v1")] impl BiomedgpsApi { + /// Call `/api/v1/publications/:id` to fetch a publication. + #[oai( + path = "/publications/:id", + method = "get", + tag = "ApiTags::KnowledgeGraph", + operation_id = "fetchPublication" + )] + async fn fetch_publication(&self, id: Path) -> GetPublicationDetailResponse { + let id = id.0; + match Publication::fetch_publication(&id).await { + Ok(publication) => GetPublicationDetailResponse::ok(publication), + Err(e) => { + let err = format!("Failed to fetch publication: {}", e); + warn!("{}", err); + return GetPublicationDetailResponse::bad_request(err); + } + } + } + + /// Call `/api/v1/publications` with query params to fetch publications. + #[oai( + path = "/publications", + method = "get", + tag = "ApiTags::KnowledgeGraph", + operation_id = "fetchPublications" + )] + async fn fetch_publications( + &self, + query_str: Query, + page: Query>, + page_size: Query>, + ) -> GetPublicationsResponse { + let query_str = query_str.0; + let page = page.0; + let page_size = page_size.0; + + match Publication::fetch_publications(&query_str, page, page_size).await { + Ok(records) => GetPublicationsResponse::ok(records), + Err(e) => { + let err = format!("Failed to fetch publications: {}", e); + warn!("{}", err); + return GetPublicationsResponse::bad_request(err); + } + } + } + /// Call `/api/v1/statistics` with query params to fetch all entity & relation metadata. #[oai( path = "/statistics", @@ -522,7 +571,7 @@ impl BiomedgpsApi { page, page_size, Some("id ASC"), - None + None, ) .await { @@ -707,7 +756,7 @@ impl BiomedgpsApi { page, page_size, Some("score ASC"), - None + None, ) .await { @@ -827,7 +876,7 @@ impl BiomedgpsApi { page, page_size, Some("embedding_id ASC"), - None + None, ) .await { diff --git a/src/api/schema.rs b/src/api/schema.rs index 5b18b22..a61fdc7 100644 --- a/src/api/schema.rs +++ b/src/api/schema.rs @@ -11,6 +11,8 @@ use serde::{Deserialize, Serialize}; use validator::Validate; use validator::ValidationErrors; +use super::req::{PublicationDetail, PublicationRecords}; + #[derive(Tags)] pub enum ApiTags { KnowledgeGraph, @@ -211,6 +213,58 @@ impl< } } +#[derive(ApiResponse)] +pub enum GetPublicationDetailResponse { + #[oai(status = 200)] + Ok(Json), + + #[oai(status = 400)] + BadRequest(Json), + + #[oai(status = 404)] + NotFound(Json), +} + +impl GetPublicationDetailResponse { + pub fn ok(publication_detail: PublicationDetail) -> Self { + Self::Ok(Json(publication_detail)) + } + + 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 GetPublicationsResponse { + #[oai(status = 200)] + Ok(Json), + + #[oai(status = 400)] + BadRequest(Json), + + #[oai(status = 404)] + NotFound(Json), +} + +impl GetPublicationsResponse { + pub fn ok(publication_records: PublicationRecords) -> Self { + Self::Ok(Json(publication_records)) + } + + 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 GetRecordsResponse< S: Serialize diff --git a/src/bin/biomedgps.rs b/src/bin/biomedgps.rs index 45bf795..0d60394 100644 --- a/src/bin/biomedgps.rs +++ b/src/bin/biomedgps.rs @@ -65,6 +65,10 @@ struct Opt { #[structopt(name = "database-url", short = "d", long = "database-url")] database_url: Option, + /// Pool size for database connection. + #[structopt(name = "pool-size", short = "s", long = "pool-size")] + pool_size: Option, + /// Graph Database url, such as neo4j:://user:pass@host:port. We will always use the default database. /// You can also set it with env var: NEO4J_URL. #[structopt(name = "neo4j-url", short = "g", long = "neo4j-url")] @@ -239,7 +243,8 @@ async fn main() -> Result<(), std::io::Error> { database_url.unwrap() }; - let pool = connect_db(&database_url, 10).await; + let pool_size = args.pool_size.unwrap_or(10); + let pool = connect_db(&database_url, pool_size).await; let arc_pool = Arc::new(pool); let shared_rb = AddData::new(arc_pool.clone()); diff --git a/studio/package.json b/studio/package.json index d74e0f7..474c204 100644 --- a/studio/package.json +++ b/studio/package.json @@ -33,7 +33,7 @@ "antd": "5.7.0", "antd-schema-form": "^4.5.1", "axios": "^1.1.2", - "biominer-components": "0.3.13", + "biominer-components": "0.3.15", "classnames": "^2.3.0", "handlebars": "^4.7.7", "handsontable": "^12.1.3", diff --git a/studio/src/components/Header/index.tsx b/studio/src/components/Header/index.tsx index f8104a3..21f355b 100644 --- a/studio/src/components/Header/index.tsx +++ b/studio/src/components/Header/index.tsx @@ -1,5 +1,5 @@ import { QuestionCircleOutlined, InfoCircleOutlined, UserOutlined, FieldTimeOutlined } from '@ant-design/icons'; -import { Space, Menu, Button, message } from 'antd'; +import { Space, Menu, Button, message, Dropdown } from 'antd'; import React, { useEffect, useState } from 'react'; import { history } from 'umi'; import { useAuth0 } from "@auth0/auth0-react"; @@ -59,7 +59,7 @@ const GlobalHeaderRight: React.FC = (props) => { } }, [props.username, user]); - const items: MenuProps['items'] = [ + const directItems: MenuProps['items'] = [ { label: username, key: 'user', @@ -70,16 +70,19 @@ const GlobalHeaderRight: React.FC = (props) => { key: 'version', icon: }, - { - label: 'About', - key: 'about', - icon: , - }, + ] + + const items: MenuProps['items'] = [ { label: 'Help', key: 'help', icon: , }, + { + label: 'About Us', + key: 'about', + icon: , + }, { label: 'ChangeLog', key: 'changelog', @@ -113,7 +116,10 @@ const GlobalHeaderRight: React.FC = (props) => { return ( - + + + +