From 172a7a1da1f2d7c9a8a0e6ba38dee9219c8ac00d Mon Sep 17 00:00:00 2001 From: Varkha Sharma <112053040+varkha-d-sharma@users.noreply.github.com> Date: Wed, 17 Jul 2024 06:12:41 +0530 Subject: [PATCH] Model card creation (#181) * created popup component * adding popup component * added rendering code * updating popup component * Added close and download button * Deleted cmf file * updating model_card * Wrote an rest api for model card and axios interface * model card api update * json filename update * readded popup * adding more changes required for model card creation * added some json related changes to do some testing on popup side * made some changes * changes related to popup content * updated code for json filename * made some changes to display data properly in popup * Align items inside popup component to left * Made some changes in popup data fromat * Added table structure for artifact data * added style to table * adding some code for popup * made changes in table code * keeping old_index.jsx file * converted table to model card * Added the code to refine popup model card * Made changes regarding modelcard structure * mistakenly commited wrong user name * addressing review comments * Addressed review comments and commented on get_model_data function * Updating supported versions from <=3.10 to <3.11. So that, all the version of 3.10 are included * Made proper alignment of close and download button * Addressing review comments --------- Co-authored-by: first second Co-authored-by: Abhinav Chobey Co-authored-by: AyeshaSanadi --- docs/index.md | 2 +- pyproject.toml | 2 +- server/app/get_data.py | 72 ++++++++ server/app/main.py | 40 ++++- ui/src/client.js | 13 ++ ui/src/components/ArtifactTable/index.jsx | 41 ++++- ui/src/components/Popup/index.css | 128 ++++++++++++++ ui/src/components/Popup/index.jsx | 201 ++++++++++++++++++++++ ui/src/pages/artifacts/index.jsx | 2 +- 9 files changed, 489 insertions(+), 12 deletions(-) create mode 100644 ui/src/components/Popup/index.css create mode 100644 ui/src/components/Popup/index.jsx diff --git a/docs/index.md b/docs/index.md index 9249e645..206cc389 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ models and performance metrics) recorded by the framework are versioned and iden ## Installation #### 1. Pre-Requisites: -* 3.9>= Python <=3.10 +* 3.9>= Python <3.11 * Git latest version #### 2. Set up Python Virtual Environment: diff --git a/pyproject.toml b/pyproject.toml index 06132ae3..a8605cef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ authors = [ ] description = "Track metadata for AI pipeline" readme = "README.md" -requires-python = ">=3.9,<=3.10" +requires-python = ">=3.9,<3.11" classifiers = [ "Programming Language :: Python :: 3", "Operating System :: POSIX :: Linux", diff --git a/server/app/get_data.py b/server/app/get_data.py index f9ce87c0..6e7a4358 100644 --- a/server/app/get_data.py +++ b/server/app/get_data.py @@ -6,6 +6,78 @@ from server.app.query_visualization_execution import query_visualization_execution from fastapi.responses import FileResponse + +async def get_model_data(mlmdfilepath, modelId): + ''' + This function retrieves the necessary model data required for generating a model card. + + Arguments: + mlmdfilepath (str): The file path to the metadata. + modelId (int): The ID of the model for which data is required. + + Returns: + This function returns a tuple of DataFrames containing the following: + + model_data_df (DataFrame): Metadata related to the model itself. + model_exe_df (DataFrame): Metadata of the executions in which the specified modelId was an input or output. + model_input_df (DataFrame): Metadata of input artifacts that led to the creation of the model. + model_output_df (DataFrame): Metadata of artifacts that used the model as an input. + The returned DataFrames provide comprehensive metadata for the specified model, aiding in the creation of detailed and accurate model cards. + ''' + query = cmfquery.CmfQuery(mlmdfilepath) + pd.set_option('display.max_columns', None) + model_data_df = pd.DataFrame() + model_exe_df = pd.DataFrame() + model_input_df = pd.DataFrame() + model_output_df = pd.DataFrame() + + # get name from id + modelName = "" + model_data_df = query.get_all_artifacts_by_ids_list([modelId]) + # if above dataframe is not empty, we have the dataframe for given modelId with full model related details + if model_data_df.empty: + return model_data_df, model_exe_df, model_input_df, model_output_df + # However following check is done, in case, variable 'modelId' is not an ID for model artifact + modelType = model_data_df['type'].tolist()[0] + if not modelType == "Model": + # making model_data_df empty + model_data_df = pd.DataFrame() + return model_data_df, model_exe_df, model_input_df, model_output_df + + + # extracting modelName + modelName = model_data_df['name'].tolist()[0] + + # model's executions data with props and custom props + exe_df = query.get_all_executions_for_artifact(modelName) + exe_ids = [] + if not exe_df.empty: + exe_df.drop(columns=['execution_type_name', 'execution_name'], inplace=True) + exe_ids = exe_df['execution_id'].tolist() + + + if not exe_ids: + return model_data_df, model_exe_df, model_input_df, model_output_df + model_exe_df = query.get_all_executions_by_ids_list(exe_ids) + model_exe_df.drop(columns=['Python_Env', 'Git_Start_Commit', 'Git_End_Commit'], inplace=True) + + in_art_ids = [] + # input artifacts + # it is usually not a good practice to use functions starting with _ outside of the file they are defined .. should i change?? + in_art_ids.extend(query._get_input_artifacts(exe_ids)) + if modelId in in_art_ids: + in_art_ids.remove(modelId) + model_input_df = query.get_all_artifacts_by_ids_list(in_art_ids) + + out_art_ids = [] + # output artifacts + out_art_ids.extend(query._get_output_artifacts(exe_ids)) + if modelId in out_art_ids: + out_art_ids.remove(modelId) + model_output_df = query.get_all_artifacts_by_ids_list(out_art_ids) + + return model_data_df, model_exe_df, model_input_df, model_output_df + async def get_executions_by_ids(mlmdfilepath, pipeline_name, exe_ids): ''' Args: diff --git a/server/app/main.py b/server/app/main.py index aae8e47c..1dabe6c3 100644 --- a/server/app/main.py +++ b/server/app/main.py @@ -5,6 +5,7 @@ from fastapi.staticfiles import StaticFiles from contextlib import asynccontextmanager import pandas as pd +from typing import List, Dict, Any from cmflib import cmfquery, cmf_merger from server.app.get_data import ( @@ -15,7 +16,8 @@ get_artifact_types, get_all_artifact_ids, get_all_exe_ids, - get_executions_by_ids + get_executions_by_ids, + get_model_data ) from server.app.query_visualization import query_visualization from server.app.query_exec_lineage import query_exec_lineage @@ -146,8 +148,7 @@ async def display_exec( @app.get("/display_artifact_lineage/{pipeline_name}") async def display_artifact_lineage(request: Request, pipeline_name: str): ''' - This api's returns dictionary of nodes and links for given - pipeline. + This api returns dictionary of nodes and links for given pipeline. response = { nodes: [{id:"",name:""}], links: [{source:1,target:4},{}], @@ -170,8 +171,7 @@ async def display_artifact_lineage(request: Request, pipeline_name: str): @app.get("/get_execution_types/{pipeline_name}") async def get_execution_types(request: Request, pipeline_name: str): ''' - This api's returns - list of execution types. + This api's returns list of execution types. ''' # checks if mlmd file exists on server @@ -304,6 +304,34 @@ async def upload_file(request:Request, pipeline_name: str = Query(..., descripti except Exception as e: return {"error": f"Failed to up load file: {e}"} +@app.get("/model-card") +async def model_card(request:Request, modelId: int, response_model=List[Dict[str, Any]]): + json_payload_1 = "" + json_payload_2 = "" + json_payload_3 = "" + json_payload_4 = "" + model_data_df = pd.DataFrame() + model_exe_df = pd.DataFrame() + model_input_art_df = pd.DataFrame() + model_output_art_df = pd.DataFrame() + df = pd.DataFrame() + # checks if mlmd file exists on server + if os.path.exists(server_store_path): + model_data_df, model_exe_df, model_input_art_df, model_output_art_df = await get_model_data(server_store_path, modelId) + if not model_data_df.empty: + result_1 = model_data_df.to_json(orient="records") + json_payload_1 = json.loads(result_1) + if not model_exe_df.empty: + result_2 = model_exe_df.to_json(orient="records") + json_payload_2 = json.loads(result_2) + if not model_input_art_df.empty: + result_3 = model_input_art_df.to_json(orient="records") + json_payload_3 = json.loads(result_3) + if not model_output_art_df.empty: + result_4 = model_output_art_df.to_json(orient="records") + json_payload_4 = json.loads(result_4) + return [json_payload_1, json_payload_2, json_payload_3, json_payload_4] + async def update_global_art_dict(): global dict_of_art_ids output_dict = await get_all_artifact_ids(server_store_path) @@ -316,3 +344,5 @@ async def update_global_exe_dict(): output_dict = await get_all_exe_ids(server_store_path) dict_of_exe_ids = output_dict return + + diff --git a/ui/src/client.js b/ui/src/client.js index fc9d2702..1ce882ac 100644 --- a/ui/src/client.js +++ b/ui/src/client.js @@ -108,6 +108,19 @@ class FastAPIClient { console.error(error); } } + + async getModelCard(modelId) { + return this.apiClient.get(`/model-card`, { + params: { + modelId: modelId, + }, + }) + .then(({data}) => { + return data; + }); + } + } + export default FastAPIClient; diff --git a/ui/src/components/ArtifactTable/index.jsx b/ui/src/components/ArtifactTable/index.jsx index 58f4b852..f0aab04c 100644 --- a/ui/src/components/ArtifactTable/index.jsx +++ b/ui/src/components/ArtifactTable/index.jsx @@ -18,7 +18,13 @@ // ArtifactTable.jsx import React, { useState, useEffect } from "react"; import "./index.css"; -const ArtifactTable = ({ artifacts, onSort, onFilter }) => { +import Popup from "../../components/Popup"; +import FastAPIClient from "../../client"; +import config from "../../config"; + +const client = new FastAPIClient(config); + +const ArtifactTable = ({ artifacts, ArtifactType, onSort, onFilter }) => { // Default sorting order const [sortOrder, setSortOrder] = useState("Context_Type"); @@ -27,9 +33,12 @@ const ArtifactTable = ({ artifacts, onSort, onFilter }) => { const [filterValue, setFilterValue] = useState(""); const [expandedRow, setExpandedRow] = useState(null); + const [showPopup, setShowPopup] = useState(false); + const [popupData, setPopupData] = useState(''); const consistentColumns = []; + useEffect(() => { // Set initial sorting order when component mounts setSortOrder("asc"); @@ -55,6 +64,19 @@ const ArtifactTable = ({ artifacts, onSort, onFilter }) => { } }; + const handleLinkClick = (model_id) => { + client.getModelCard(model_id).then((data) => { + console.log(data); + setPopupData(data); + setShowPopup(true); + }); + }; + + const handleClosePopup = () => { + setShowPopup(false); + }; + + return (
@@ -83,7 +105,13 @@ const ArtifactTable = ({ artifacts, onSort, onFilter }) => { name {sortOrder === "asc" && } {sortOrder === "desc" && } - + {ArtifactType === "Model" && ( + + Model_Card + + )} + + execution_type_name @@ -105,14 +133,19 @@ const ArtifactTable = ({ artifacts, onSort, onFilter }) => { toggleRow(index)} className="text-sm font-medium text-gray-800" > - + toggleRow(index)}> {expandedRow === index ? "-" : "+"} {data.id} {data.name} + {ArtifactType === "Model" && ( + + { e.preventDefault(); handleLinkClick(data.id); }}>Open Model Card + + + )} {data.execution_type_name} {data.url} {data.uri} diff --git a/ui/src/components/Popup/index.css b/ui/src/components/Popup/index.css new file mode 100644 index 00000000..7cae1a8f --- /dev/null +++ b/ui/src/components/Popup/index.css @@ -0,0 +1,128 @@ +.popup-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; +} + +.popup-border { + position: sticky; + top: 0; + right: 0; + z-index: 10; /* Ensure it stays on top of the popup */ +} + +.popup { + background-color: white; + padding: 20px; + border-radius: 8px; + max-width: 1100px; + width: 100%; + position: relative; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + max-height: 90vh; /* Ensuring it doesn't exceed the viewport */ + overflow-y: auto; /* Ensure the popup can scroll if it still overflows */ +} + +.close-button { + background: gray; + color: white; + border: 2px solid black; + border-radius: 50%; + padding: 5px 10px; + cursor: pointer; + position: absolute; + top: -18px; + right: -19px; + z-index: 10; +} + +.download-button { + background: white; + color: dark gray; + border: none; + cursor: pointer; + width: 84px; + height: 55px; + float: right; +} + +.popup-content { + margin-top: 20px; +} + +.popup-row { + display: flex; + justify-content: space-between; + margin-bottom: 10px; + text-align: left; +} + +.popup-labels, .popup-data { + display: flex; + flex-direction: column; +} + +.popup-labels { + flex: 1; + font-weight: bold; + background-color: #f0f0f0; /* Light gray background for labels */ + padding: 10px; + border-radius: 4px 0 0 4px; /* Rounded corners for left section */ +} + +.popup-data { + flex: 2; + background-color: #e0f7fa; /* Light blue background for data */ + padding: 10px; + border-radius: 0 4px 4px 0; /* Rounded corners for right section */ + text-align: left; +} + +.table-container { + max-height: 400px; /* Adjust this height as needed */ + max-width: 100%; + overflow-x: auto; /* Horizontal scrollbar */ + overflow-y: auto; /* Vertical scrollbar */ + margin-top: 20px; +} + +.table { + width: 100%; + border-collapse: collapse; + overflow: auto; + background-color: #e0f7fa; /* Light blue background for data */ +} + +.table th, .table td { + border: 1px solid #ddd; + padding: 8px; + text-align: left; /* Align text to the left */ +} + +.table th { + background-color: #f2f2f2; /* Light gray background for headers */ + font-weight: bold; +} + +.tbody tr:nth-child(even) { + background-color: #f9f9f9; /* Light gray background for even rows */ +} + +.tbody tr:hover { + background-color: #f1f1f1; /* Light gray background for hovered rows */ +} + +hr { + margin: 20px 0; +} + +p { + font-weight: bold; + margin: 10px 0; +} diff --git a/ui/src/components/Popup/index.jsx b/ui/src/components/Popup/index.jsx new file mode 100644 index 00000000..afc855bd --- /dev/null +++ b/ui/src/components/Popup/index.jsx @@ -0,0 +1,201 @@ +import React from "react"; +import "./index.css"; // Optional: For styling the popup + +const Popup = ({ show, model_data, onClose }) => { + if (!show) { + return null; + } + + // find the uri value from artifacts + const findUri = () => { + const item = model_data[0].find((entry) => entry.uri); + return item ? item.uri : "default"; + }; + + // create filename based on uri + const createFilename = (uri) => { + return `model_card_${uri}.json`; + }; + + const downloadJSON = () => { + const uri = findUri(); + const filename = createFilename(uri); + + const jsonString = JSON.stringify(model_data, null, 2); + const blob = new Blob([jsonString], { type: "application/json" }); + + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + const excludeColumns = ["create_time_since_epoch"]; + + const renameKey = (key) => { + const prefix = "custom_properties_"; + if (key.startsWith(prefix)) { + return key.slice(prefix.length); + } + return key; + }; + + const renderContent = (item, index) => { + switch (index) { + case 0: + return ( +
+

Model's Data

+
+ {item.length > 0 && + item.map((data, i) => ( +
+
+ {Object.keys(data) + .filter((key) => !excludeColumns.includes(key)) + .map((key, idx) => ( +

{renameKey(key)}:

+ ))} +
+
+ {Object.entries(data) + .filter(([key]) => !excludeColumns.includes(key)) + .map(([key, value], idx) => ( +

{value ? value : "Null"}

+ ))} +
+
+ ))} +
+ ); + case 1: + const exe_headers = item.length > 0 ? Object.keys(item[0]) : []; + return ( +
+
+

List of executions in which model has been used

+
+ + + + {exe_headers.map((header, index) => ( + + ))} + + + + {item.length > 0 && + item.map((data, i) => ( + + {exe_headers.map((header, index) => ( + + ))} + + ))} + +
+ {renameKey(header)} +
{data[header]}
+
+ ); + case 2: + return ( +
+
+

List of input artifacts for the model

+
+ {item.length > 0 && + item.map((data, i) => ( +
+
+ {Object.keys(data) + .filter((key) => !excludeColumns.includes(key)) + .map((key, idx) => ( +

{renameKey(key)}:

+ ))} +
+
+ {Object.entries(data) + .filter(([key]) => !excludeColumns.includes(key)) + .map(([key, value], idx) => ( +

{value ? value : "Null"}

+ ))} +
+
+ ))} +
+ ); + case 3: + return ( +
+
+

List of output artifacts for the model

+
+ {item.length > 0 && + item.map((data, i) => ( +
+
+ {Object.keys(data) + .filter((key) => !excludeColumns.includes(key)) + .map((key, idx) => ( +

{renameKey(key)}:

+ ))} +
+
+ {Object.entries(data) + .filter(([key]) => !excludeColumns.includes(key)) + .map(([key, value], idx) => ( +

{value ? value : "Null"}

+ ))} +
+
+ ))} +
+ ); + default: + return ( +
+

Unknown item

+
+ ); + } + }; + + return ( + <> + +
+
+
+ +
+ +
+
+ {model_data.length > 0 ? ( + model_data.map((item, index) => ( +
{renderContent(item, index)}
+ )) + ) : ( +

No items available

+ )} +
+
+
+
+ + ); +}; + +export default Popup; diff --git a/ui/src/pages/artifacts/index.jsx b/ui/src/pages/artifacts/index.jsx index 0c40172d..6e0ad702 100644 --- a/ui/src/pages/artifacts/index.jsx +++ b/ui/src/pages/artifacts/index.jsx @@ -150,7 +150,7 @@ const Artifacts = () => {
{selectedPipeline !== null && selectedArtifactType !== null && artifacts !== null && artifacts !== {} && ( - + )}
{artifacts !== null && totalItems > 0 && (