Skip to content

Commit 0677a89

Browse files
committed
Add proper callback message
Signed-off-by: M Q <[email protected]>
1 parent d1e136f commit 0677a89

File tree

8 files changed

+108
-98
lines changed

8 files changed

+108
-98
lines changed

platforms/aidoc/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Open another console window and change directory to the same as this file.
6666

6767
Set the environment vars so that the test script can get the input DCM and write the callback contents.
6868
Also, once the Restful app completes each processing, the Spleen Seg app's output will also be saved in
69-
the output folder speficied below (the script passes the output folder via the Rest API).
69+
the output folder specified below (the script passes the output folder via the Rest API).
7070
7171
```
7272
export HOLOSCAN_INPUT_PATH=dcm
@@ -79,7 +79,7 @@ Run the test script, and examine its console output.
7979
source test_endpoints.sh
8080
```
8181
82-
Once the script completes, examine the `output` folder, which should conatain the following (dcm file
82+
Once the script completes, examine the `output` folder, which should contain the following (dcm file
8383
name will be different)
8484
8585
```

platforms/aidoc/restful_app/ai_spleen_seg_app/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
import os
1313
import sys
1414

15+
from .app import AISpleenSegApp
16+
1517
_current_dir = os.path.abspath(os.path.dirname(__file__))
1618
if sys.path and os.path.abspath(sys.path[0]) != _current_dir:
1719
sys.path.insert(0, _current_dir)
1820
del _current_dir
21+
22+
__all__ = ["AISpleenSegApp"]

platforms/aidoc/restful_app/ai_spleen_seg_app/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
import logging
1313

14-
from app import AISpleenSegApp
14+
from .app import AISpleenSegApp
1515

1616
if __name__ == "__main__":
1717
logging.info(f"Begin {__name__}")

platforms/aidoc/restful_app/ai_spleen_seg_app/app.py

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12+
import json
1213
import logging
14+
import os
1315
from pathlib import Path
16+
from typing import List, Union
1417

1518
# Required for setting SegmentDescription attributes. Direct import as this is not part of App SDK package.
1619
from pydicom.sr.codedict import codes
17-
from reporter_operator import ExecutionStatusReporterOperator
1820

1921
from monai.deploy.conditions import CountCondition
2022
from monai.deploy.core import AppContext, Application
@@ -33,10 +35,15 @@
3335
)
3436
from monai.deploy.operators.stl_conversion_operator import STLConversionOperator
3537

38+
from .results_message import (
39+
AggregatedResults,
40+
AlgorithmClass,
41+
DetailedResult,
42+
MeasurementResult,
43+
Results,
44+
)
45+
3646

37-
# @resource(cpu=1, gpu=1, memory="7Gi")
38-
# pip_packages can be a string that is a path(str) to requirements.txt file or a list of packages.
39-
# The monai pkg is not required by this class, instead by the included operators.
4047
class AISpleenSegApp(Application):
4148
"""Demonstrates inference with built-in MONAI Bundle inference operator with DICOM files as input/output
4249
@@ -57,13 +64,68 @@ def __init__(self, *args, status_callback=None, **kwargs):
5764
"""Creates an application instance."""
5865
self._logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__))
5966
self._status_callback = status_callback
67+
self._app_input_path = None # to be set in compose
68+
self._app_output_path = None # to be set in compose
6069
super().__init__(*args, **kwargs)
6170

71+
def _get_files_in_folder(self, folder_path: Union[str, Path]) -> List[str]:
72+
"""Traverses a folder and returns a list of full paths of all files.
73+
74+
Args:
75+
folder_path (Union[str, Path]): The path to the folder to traverse.
76+
77+
Returns:
78+
List[str]: A list of absolute paths to the files in the folder.
79+
"""
80+
if not os.path.isdir(folder_path):
81+
self._logger.warning(f"Output folder '{folder_path}' not found, returning empty file list.")
82+
return []
83+
84+
file_paths = []
85+
for root, _, files in os.walk(folder_path):
86+
for file in files:
87+
full_path = os.path.abspath(os.path.join(root, file))
88+
file_paths.append(full_path)
89+
return file_paths
90+
6291
def run(self, *args, **kwargs):
6392
# This method calls the base class to run. Can be omitted if simply calling through.
6493
self._logger.info(f"Begin {self.run.__name__}")
65-
# The try...except block is removed as the reporter operator will handle status reporting.
66-
super().run(*args, **kwargs)
94+
try:
95+
super().run(*args, **kwargs)
96+
97+
if self._status_callback:
98+
# Create the results object using the Pydantic models
99+
ai_results = Results(
100+
aggregated_results=AggregatedResults(
101+
name="Spleen Segmentation",
102+
algorithm_class={AlgorithmClass.MEASUREMENT},
103+
),
104+
detailed_results={
105+
"Spleen Segmentation": DetailedResult(
106+
measurement=MeasurementResult(
107+
measurements_text="Spleen segmentation completed successfully.",
108+
)
109+
)
110+
},
111+
)
112+
113+
output_files = self._get_files_in_folder(self._app_output_path)
114+
115+
callback_msg_dict = {
116+
"run_success": True,
117+
"output_files": output_files,
118+
"error_message": None,
119+
"error_code": None,
120+
"result": ai_results.model_dump_json(),
121+
}
122+
self._status_callback(json.dumps(callback_msg_dict))
123+
124+
except Exception as e:
125+
self._logger.error(f"Error in {self.run.__name__}: {e}")
126+
# Let the caller to handle and report the error
127+
raise e
128+
67129
self._logger.info(f"End {self.run.__name__}")
68130

69131
def compose(self):
@@ -73,12 +135,12 @@ def compose(self):
73135

74136
# Use Commandline options over environment variables to init context.
75137
app_context: AppContext = Application.init_app_context(self.argv)
76-
app_input_path = Path(app_context.input_path)
77-
app_output_path = Path(app_context.output_path)
138+
self._app_input_path = Path(app_context.input_path)
139+
self._app_output_path = Path(app_context.output_path)
78140

79141
# Create the custom operator(s) as well as SDK built-in operator(s).
80142
study_loader_op = DICOMDataLoaderOperator(
81-
self, CountCondition(self, 1), input_folder=app_input_path, name="study_loader_op"
143+
self, CountCondition(self, 1), input_folder=self._app_input_path, name="study_loader_op"
82144
)
83145
series_selector_op = DICOMSeriesSelectorOperator(self, rules=Sample_Rules_Text, name="series_selector_op")
84146
series_to_vol_op = DICOMSeriesToVolumeOperator(self, name="series_to_vol_op")
@@ -122,11 +184,11 @@ def compose(self):
122184
self,
123185
segment_descriptions=segment_descriptions,
124186
custom_tags=custom_tags,
125-
output_folder=app_output_path,
187+
output_folder=self._app_output_path,
126188
name="dicom_seg_writer",
127189
)
128190

129-
reporter_op = ExecutionStatusReporterOperator(self, status_callback=self._status_callback)
191+
# reporter_op = ExecutionStatusReporterOperator(self, status_callback=self._status_callback)
130192

131193
# Create the processing pipeline, by specifying the source and destination operators, and
132194
# ensuring the output from the former matches the input of the latter, in both name and type.
@@ -143,13 +205,13 @@ def compose(self):
143205
# Create the surface mesh STL conversion operator and add it to the app execution flow, if needed, by
144206
# uncommenting the following couple lines.
145207
stl_conversion_op = STLConversionOperator(
146-
self, output_file=app_output_path.joinpath("stl/spleen.stl"), name="stl_conversion_op"
208+
self, output_file=self._app_output_path.joinpath("stl/spleen.stl"), name="stl_conversion_op"
147209
)
148210
self.add_flow(bundle_spleen_seg_op, stl_conversion_op, {("pred", "image")})
149211

150212
# Connect the reporter operator to the end of the pipeline.
151213
# It will be triggered after the DICOM SEG file is written.
152-
self.add_flow(stl_conversion_op, reporter_op, {("stl_bytes", "data")})
214+
# self.add_flow(stl_conversion_op, reporter_op, {("stl_bytes", "data")})
153215

154216
logging.info(f"End {self.compose.__name__}")
155217

platforms/aidoc/restful_app/ai_spleen_seg_app/reporter_operator.py

Lines changed: 0 additions & 65 deletions
This file was deleted.

platforms/aidoc/restful_app/app.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import argparse
1313
import importlib
14+
import json
1415
import logging
1516
import os
1617
import sys
@@ -23,11 +24,12 @@
2324
# The MONAI Deploy application to be wrapped.
2425
# This can be changed to any other application in the repository.
2526
# Provide the module path and the class name of the application.
26-
APP_MODULE_NAME = "ai_spleen_seg_app.app"
27+
28+
APP_MODULE_NAME = "ai_spleen_seg_app"
2729
APP_CLASS_NAME = "AISpleenSegApp"
2830

2931
# Flask application setup
30-
app = Flask(__name__)
32+
restful_app = Flask(__name__)
3133
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
3234

3335
# Global state to track processing status. A lock is used for thread safety.
@@ -54,15 +56,15 @@ def run_processing(input_folder, output_folder, callback_url):
5456
"""
5557

5658
# Define the callback function that the MONAI Deploy app will call.
57-
def app_status_callback(summary):
59+
def app_status_callback(summary: str):
5860
"""Callback function to handle the final status from the application."""
5961
logging.info(f"Received status from application: {summary}")
6062
if callback_url:
6163
try:
6264
logging.info(f"Sending final status callback to {callback_url}")
6365
# Here you could map the summary to the expected format of the callback.
6466
# For now, we'll just forward the summary.
65-
requests.post(callback_url, json=summary, timeout=5)
67+
requests.post(callback_url, data=summary, timeout=5)
6668
logging.info("Sent final status callback.")
6769
except Exception as e:
6870
logging.error(f"Failed to send callback to {callback_url}: {e}")
@@ -84,28 +86,33 @@ def app_status_callback(summary):
8486
app_class = getattr(module, APP_CLASS_NAME)
8587
monai_app = app_class(status_callback=app_status_callback)
8688

87-
# Run the MONAI Deploy application.
89+
# Run the MONAI Deploy application which calls the callback if successful.
8890
logging.info("Running the MONAI Deploy application.")
8991
monai_app.run()
9092
logging.info("Processing completed successfully.")
9193

9294
except Exception as e:
9395
logging.error(f"An error occurred during processing: {e}")
94-
# If the app fails to even start, we need to report a failure.
95-
app_status_callback({"status": "Failure", "message": f"Application failed to run: {e}"})
96+
# If the app fails, we need to handle it here and report a failure.
97+
callback_msg = {
98+
"run_success": False,
99+
"error_message": f"Error during processing: {e}",
100+
"error_code": 500,
101+
}
102+
app_status_callback(json.dumps(callback_msg))
96103

97104
finally:
98105
set_processing_status("IDLE")
99106
logging.info("Processor is now IDLE.")
100107

101108

102-
@app.route("/status", methods=["GET"])
109+
@restful_app.route("/status", methods=["GET"])
103110
def status():
104111
"""Endpoint to check the current processing status."""
105112
return jsonify({"status": get_processing_status()})
106113

107114

108-
@app.route("/process", methods=["POST"])
115+
@restful_app.route("/process", methods=["POST"])
109116
def process():
110117
"""Endpoint to start a new processing job."""
111118
if get_processing_status() == "BUSY":
@@ -144,4 +151,4 @@ def process():
144151
args = parser.parse_args()
145152
host = args.host or os.environ.get("FLASK_HOST", "0.0.0.0")
146153
port = args.port or int(os.environ.get("FLASK_PORT", 5000))
147-
app.run(host=host, port=port)
154+
restful_app.run(host=host, port=port)
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/bash
2-
export HOLOSCAN_INPUT_PATH="$(pwd)/../monai-deploy-app-sdk/inputs/spleen_ct_tcia"
3-
export HOLOSCAN_MODEL_PATH="$(pwd)/../monai-deploy-app-sdk/models/spleen_ct"
4-
export HOLOSCAN_OUTPUT_PATH="$(pwd)/output_spleen"
5-
export HOLOSCAN_LOG_LEVEL=INFO
2+
export HOLOSCAN_INPUT_PATH="/home/mqin/src//monai-deploy-app-sdk/inputs/spleen_ct_tcia"
3+
export HOLOSCAN_MODEL_PATH="/home/mqin/src//monai-deploy-app-sdk/models/spleen_ct"
4+
export HOLOSCAN_OUTPUT_PATH="./output_spleen"
5+
export HOLOSCAN_LOG_LEVEL=DEBUG

platforms/aidoc/restful_app/requirements.txt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ Pillow>=8.0.0
77
numpy-stl>=2.12.0
88
trimesh>=3.8.11
99
nibabel>=3.2.1
10-
torch>=1.12.0
11-
monai>=1.0.0
10+
torch>=2.4.1
11+
monai>=1.5.0
1212
Flask==2.2.2
1313
requests>=2.32
14-
Werkzeug==2.2.3
14+
types-requests>=2.32.0
15+
Werkzeug==2.2.3
16+
pydantic>=2.9.0

0 commit comments

Comments
 (0)