seawolf2357 commited on
Commit
f7b2e65
Β·
verified Β·
1 Parent(s): 0e3a17b

Update routes.py

Browse files
Files changed (1) hide show
  1. routes.py +57 -57
routes.py CHANGED
@@ -1,5 +1,3 @@
1
-
2
-
3
  import logging
4
  from flask import Blueprint, render_template, request, jsonify, send_from_directory
5
  from pathlib import Path
@@ -23,7 +21,7 @@ main_bp = Blueprint('main', __name__)
23
  # --- Serve the cache directory as a zip file ---
24
  @main_bp.route('/download_cache')
25
  def download_cache_zip():
26
- """Zips the cache directory and serves it for download."""
27
  zip_filename = "radexplain-cache.zip"
28
  # Create the zip file in a temporary directory
29
  # Using /tmp is common in containerized environments
@@ -33,30 +31,31 @@ def download_cache_zip():
33
 
34
  # Ensure the cache directory exists before trying to zip it
35
  if not os.path.isdir(cache_directory):
36
- logger.error(f"Cache directory not found at {cache_directory}")
37
- return jsonify({"error": f"Cache directory not found on server: {cache_directory}"}), 500
38
 
39
  try:
40
- logger.info(f"Creating zip archive of cache directory: {cache_directory} to {zip_filepath}")
41
  shutil.make_archive(
42
  zip_base_path, # This is the base name, shutil adds the .zip extension
43
  "zip",
44
  cache_directory, # This is the root directory to archive
45
  )
46
- logger.info("Zip archive created successfully.")
47
  # Send the file and then clean it up
48
  return send_from_directory(temp_dir, zip_filename, as_attachment=True)
49
  except Exception as e:
50
- logger.error(f"Error creating or sending zip archive of cache directory: {e}", exc_info=True)
51
- return jsonify({"error": f"Error creating or sending zip archive: {e}"}), 500
 
52
  @main_bp.route('/')
53
  def index():
54
- """Serves the main HTML page."""
55
  # The backend now only provides the list of available reports.
56
  # The frontend will be responsible for selecting a report,
57
  # fetching its details (text, image path), and managing the current state.
58
  if not config.AVAILABLE_REPORTS:
59
- logger.warning("No reports found in config. AVAILABLE_REPORTS is empty.")
60
 
61
  return render_template(
62
  'index.html',
@@ -65,12 +64,12 @@ def index():
65
 
66
  @main_bp.route('/get_report_details/<report_name>')
67
  def get_report_details(report_name):
68
- """Fetches the text content and image path for a given report name."""
69
  selected_report_info = next((item for item in config.AVAILABLE_REPORTS if item['name'] == report_name), None)
70
 
71
  if not selected_report_info:
72
- logger.error(f"Report '{report_name}' not found when fetching details.")
73
- return jsonify({"error": f"Report '{report_name}' not found."}), 404
74
 
75
  report_file = selected_report_info.get('report_file')
76
  image_file = selected_report_info.get('image_file')
@@ -83,47 +82,45 @@ def get_report_details(report_name):
83
  try:
84
  report_text_content = actual_server_report_path.read_text(encoding='utf-8').strip()
85
  except Exception as e:
86
- logger.error(f"Error reading report file {actual_server_report_path} for report '{report_name}': {e}", exc_info=True)
87
- return jsonify({"error": "Error reading report file."}), 500
88
  # If report_file was empty, report_text_content remains "".
89
 
90
  image_type_from_config = selected_report_info.get('image_type')
91
- display_image_type = 'Chest X-Ray' if image_type_from_config == 'CXR' else ('CT' if image_type_from_config == 'CT' else 'Medical Image')
92
 
93
  return jsonify({"text": report_text_content, "image_file": image_file, "image_type": display_image_type})
94
 
95
-
96
-
97
  @main_bp.route('/explain', methods=['POST'])
98
  def explain_sentence():
99
- """Handles the explanation request using LLM API with base64 encoded image."""
100
  if not llm_is_initialized():
101
- logger.error("LLM client (REST API) not initialized. Cannot process request.")
102
- return jsonify({"error": "LLM client (REST API) not initialized. Check API key and base URL."}), 500
103
 
104
  data = request.get_json()
105
  if not data or 'sentence' not in data or 'report_name' not in data:
106
- logger.warning("Missing 'sentence' or 'report_name' in request payload.")
107
- return jsonify({"error": "Missing 'sentence' or 'report_name' in request"}), 400
108
 
109
  selected_sentence = data['sentence']
110
  report_name = data['report_name']
111
- logger.info(f"Received request to explain: '{selected_sentence}' for report: '{report_name}'")
112
 
113
  # --- Find the selected report info ---
114
  selected_report_info = next((item for item in config.AVAILABLE_REPORTS if item['name'] == report_name), None)
115
 
116
  if not selected_report_info:
117
- logger.error(f"Report '{report_name}' not found in available reports.")
118
- return jsonify({"error": f"Report '{report_name}' not found."}), 404
119
 
120
  image_file = selected_report_info.get('image_file')
121
  report_file = selected_report_info.get('report_file')
122
  image_type = selected_report_info.get('image_type')
123
 
124
  if not image_file:
125
- logger.error(f"Image or report file path (relative to static) missing in config for report '{report_name}'.")
126
- return jsonify({"error": f"File configuration missing for report '{report_name}'."}), 500
127
 
128
  # Construct absolute server paths using BASE_DIR as image_file and report_file include "static/"
129
  server_image_path = config.BASE_DIR / image_file
@@ -131,15 +128,15 @@ def explain_sentence():
131
 
132
  # --- Prepare Base64 Image for API ---
133
  if not server_image_path.is_file():
134
- logger.error(f"Image file not found at {server_image_path}")
135
- return jsonify({"error": f"Image file for report '{report_name}' not found on server."}), 500
136
 
137
  base64_image_data_url = utils.image_to_base64_data_url(str(server_image_path))
138
  if not base64_image_data_url:
139
- logger.error("Failed to encode image to base64.")
140
- return jsonify({"error": "Could not encode image for API request"}), 500
141
 
142
- logger.info("Image successfully encoded to base64 data URL for API.")
143
 
144
  full_report_text = ""
145
  if report_file: # Only attempt to read if a report file is configured
@@ -147,25 +144,28 @@ def explain_sentence():
147
  try:
148
  full_report_text = server_report_path.read_text(encoding='utf-8')
149
  except FileNotFoundError:
150
- logger.error(f"Report file not found at {server_report_path}")
151
- return jsonify({"error": f"Report file for '{report_name}' not found on server."}), 500
152
  except Exception as e:
153
- logger.error(f"Error reading report file {server_report_path}: {e}", exc_info=True)
154
- return jsonify({"error": "Error reading report file."}), 500
155
  else: # If report_file is not configured (e.g. empty string from selected_report_info)
156
- logger.info(f"No report file configured for report '{report_name}'. Proceeding without full report text for system prompt.")
157
 
 
 
 
158
  system_prompt = (
159
- "You are a public-facing clinician. "
160
- f"A learning user has provided a sentence from a radiology report and is viewing the accompanying {image_type} image. "
161
- "Your task is to explain the meaning of ONLY the provided sentence in simple, clear terms. Explain terminology and abbriviations. Keep it concise. "
162
- "Directly address the meaning of the sentence. Do not use introductory phrases like 'Okay' or refer to the sentence itself or the report itself (e.g., 'This sentence means...'). " # noqa: E501
163
- f"{f'Crucially, since the user is looking at their {image_type} image, provide guidance on where to look on the image to understand your explanation, if applicable. ' if image_type != 'CT' else ''}"
164
- "Do not discuss any other part of the report or any sentences not explicitly provided by the user. Stick to facts in the text. Do not infer anything. \n"
165
  "===\n"
166
- f"For context, the full REPORT is:\n{full_report_text}"
167
  )
168
- user_prompt_text = f"Explain this sentence from the radiology report: '{selected_sentence}'"
169
 
170
  messages_for_api = [
171
  {"role": "system", "content": system_prompt},
@@ -180,11 +180,11 @@ def explain_sentence():
180
  cache_key = f"explain::{report_name}::{selected_sentence}"
181
  cached_result = cache.get(cache_key)
182
  if cached_result:
183
- logger.info("Returning cached explanation.")
184
  return jsonify({"explanation": cached_result})
185
 
186
  try:
187
- logger.info("Sending request to LLM API (REST) with base64 image...")
188
  response = make_chat_completion_request(
189
  model="tgi",
190
  messages=messages_for_api,
@@ -197,7 +197,7 @@ def explain_sentence():
197
  frequency_penalty=None,
198
  presence_penalty=None
199
  )
200
- logger.info("Received response stream from LLM API (REST).")
201
 
202
  explanation_parts = []
203
  for line in response.iter_lines():
@@ -212,7 +212,7 @@ def explain_sentence():
212
  if chunk.get("choices") and chunk["choices"][0].get("delta") and chunk["choices"][0]["delta"].get("content"):
213
  explanation_parts.append(chunk["choices"][0]["delta"]["content"])
214
  except json.JSONDecodeError:
215
- logger.warning(f"Could not decode JSON from stream chunk: {json_data_str}")
216
  # Depending on API, might need to handle partial JSON or other errors
217
  elif decoded_line.strip() == "[DONE]": # Some APIs might send [DONE] without "data: "
218
  break
@@ -221,10 +221,10 @@ def explain_sentence():
221
  if explanation:
222
  cache.set(cache_key, explanation, expire=None)
223
 
224
- logger.info("Explanation generated successfully." if explanation else "Empty explanation from API.")
225
- return jsonify({"explanation": explanation or "No explanation content received from the API."})
226
  except requests.exceptions.RequestException as e:
227
- logger.error(f"Error during LLM API (REST) call: {e}", exc_info=True)
228
- user_error_message = ("Failed to generate explanation. The service might be temporarily unavailable "
229
- "and is now likely starting up. Please try again in a few moments.")
230
- return jsonify({"error": user_error_message}), 500
 
 
 
1
  import logging
2
  from flask import Blueprint, render_template, request, jsonify, send_from_directory
3
  from pathlib import Path
 
21
  # --- Serve the cache directory as a zip file ---
22
  @main_bp.route('/download_cache')
23
  def download_cache_zip():
24
+ """μΊμ‹œ 디렉토리λ₯Ό μ••μΆ•ν•˜μ—¬ λ‹€μš΄λ‘œλ“œλ₯Ό μœ„ν•΄ μ œκ³΅ν•©λ‹ˆλ‹€."""
25
  zip_filename = "radexplain-cache.zip"
26
  # Create the zip file in a temporary directory
27
  # Using /tmp is common in containerized environments
 
31
 
32
  # Ensure the cache directory exists before trying to zip it
33
  if not os.path.isdir(cache_directory):
34
+ logger.error(f"μΊμ‹œ 디렉토리λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {cache_directory}")
35
+ return jsonify({"error": f"μ„œλ²„μ—μ„œ μΊμ‹œ 디렉토리λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {cache_directory}"}), 500
36
 
37
  try:
38
+ logger.info(f"μΊμ‹œ λ””λ ‰ν† λ¦¬μ˜ μ••μΆ• 파일 생성 쀑: {cache_directory} -> {zip_filepath}")
39
  shutil.make_archive(
40
  zip_base_path, # This is the base name, shutil adds the .zip extension
41
  "zip",
42
  cache_directory, # This is the root directory to archive
43
  )
44
+ logger.info("μ••μΆ• 파일이 μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
45
  # Send the file and then clean it up
46
  return send_from_directory(temp_dir, zip_filename, as_attachment=True)
47
  except Exception as e:
48
+ logger.error(f"μΊμ‹œ 디렉토리 μ••μΆ• 파일 생성 λ˜λŠ” 전솑 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
49
+ return jsonify({"error": f"μ••μΆ• 파일 생성 λ˜λŠ” 전솑 쀑 였λ₯˜ λ°œμƒ: {e}"}), 500
50
+
51
  @main_bp.route('/')
52
  def index():
53
+ """메인 HTML νŽ˜μ΄μ§€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€."""
54
  # The backend now only provides the list of available reports.
55
  # The frontend will be responsible for selecting a report,
56
  # fetching its details (text, image path), and managing the current state.
57
  if not config.AVAILABLE_REPORTS:
58
+ logger.warning("μ„€μ •μ—μ„œ λ³΄κ³ μ„œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. AVAILABLE_REPORTSκ°€ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.")
59
 
60
  return render_template(
61
  'index.html',
 
64
 
65
  @main_bp.route('/get_report_details/<report_name>')
66
  def get_report_details(report_name):
67
+ """μ£Όμ–΄μ§„ λ³΄κ³ μ„œ 이름에 λŒ€ν•œ ν…μŠ€νŠΈ λ‚΄μš©κ³Ό 이미지 경둜λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€."""
68
  selected_report_info = next((item for item in config.AVAILABLE_REPORTS if item['name'] == report_name), None)
69
 
70
  if not selected_report_info:
71
+ logger.error(f"상세 정보λ₯Ό κ°€μ Έμ˜¬ λ•Œ λ³΄κ³ μ„œ '{report_name}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
72
+ return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}), 404
73
 
74
  report_file = selected_report_info.get('report_file')
75
  image_file = selected_report_info.get('image_file')
 
82
  try:
83
  report_text_content = actual_server_report_path.read_text(encoding='utf-8').strip()
84
  except Exception as e:
85
+ logger.error(f"λ³΄κ³ μ„œ '{report_name}'의 파일 {actual_server_report_path} 읽기 였λ₯˜: {e}", exc_info=True)
86
+ return jsonify({"error": "λ³΄κ³ μ„œ νŒŒμΌμ„ μ½λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."}), 500
87
  # If report_file was empty, report_text_content remains "".
88
 
89
  image_type_from_config = selected_report_info.get('image_type')
90
+ display_image_type = '흉뢀 X-레이' if image_type_from_config == 'CXR' else ('CT' if image_type_from_config == 'CT' else '의료 μ˜μƒ')
91
 
92
  return jsonify({"text": report_text_content, "image_file": image_file, "image_type": display_image_type})
93
 
 
 
94
  @main_bp.route('/explain', methods=['POST'])
95
  def explain_sentence():
96
+ """base64둜 μΈμ½”λ”©λœ 이미지와 ν•¨κ»˜ LLM APIλ₯Ό μ‚¬μš©ν•˜μ—¬ μ„€λͺ… μš”μ²­μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€."""
97
  if not llm_is_initialized():
98
+ logger.error("LLM ν΄λΌμ΄μ–ΈνŠΈ(REST API)κ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μš”μ²­μ„ μ²˜λ¦¬ν•  수 μ—†μŠ΅λ‹ˆλ‹€.")
99
+ return jsonify({"error": "LLM ν΄λΌμ΄μ–ΈνŠΈ(REST API)κ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. API 킀와 베이슀 URL을 ν™•μΈν•˜μ„Έμš”."}), 500
100
 
101
  data = request.get_json()
102
  if not data or 'sentence' not in data or 'report_name' not in data:
103
+ logger.warning("μš”μ²­ νŽ˜μ΄λ‘œλ“œμ— 'sentence' λ˜λŠ” 'report_name'이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
104
+ return jsonify({"error": "μš”μ²­μ— 'sentence' λ˜λŠ” 'report_name'이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€"}), 400
105
 
106
  selected_sentence = data['sentence']
107
  report_name = data['report_name']
108
+ logger.info(f"μ„€λͺ… μš”μ²­ μˆ˜μ‹ : '{selected_sentence}' (λ³΄κ³ μ„œ: '{report_name}')")
109
 
110
  # --- Find the selected report info ---
111
  selected_report_info = next((item for item in config.AVAILABLE_REPORTS if item['name'] == report_name), None)
112
 
113
  if not selected_report_info:
114
+ logger.error(f"μ‚¬μš© κ°€λŠ₯ν•œ λ³΄κ³ μ„œμ—μ„œ '{report_name}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
115
+ return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}), 404
116
 
117
  image_file = selected_report_info.get('image_file')
118
  report_file = selected_report_info.get('report_file')
119
  image_type = selected_report_info.get('image_type')
120
 
121
  if not image_file:
122
+ logger.error(f"λ³΄κ³ μ„œ '{report_name}'의 μ„€μ •μ—μ„œ 이미지 λ˜λŠ” λ³΄κ³ μ„œ 파일 경둜(static κΈ°μ€€ μƒλŒ€κ²½λ‘œ)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
123
+ return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'의 파일 섀정이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€."}), 500
124
 
125
  # Construct absolute server paths using BASE_DIR as image_file and report_file include "static/"
126
  server_image_path = config.BASE_DIR / image_file
 
128
 
129
  # --- Prepare Base64 Image for API ---
130
  if not server_image_path.is_file():
131
+ logger.error(f"이미지 νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {server_image_path}")
132
+ return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'의 이미지 νŒŒμΌμ„ μ„œλ²„μ—μ„œ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}), 500
133
 
134
  base64_image_data_url = utils.image_to_base64_data_url(str(server_image_path))
135
  if not base64_image_data_url:
136
+ logger.error("이미지λ₯Ό base64둜 μΈμ½”λ”©ν•˜λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.")
137
+ return jsonify({"error": "API μš”μ²­μ„ μœ„ν•œ 이미지 인코딩에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€"}), 500
138
 
139
+ logger.info("APIλ₯Ό μœ„ν•œ base64 데이터 URL둜 이미지가 μ„±κ³΅μ μœΌλ‘œ μΈμ½”λ”©λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
140
 
141
  full_report_text = ""
142
  if report_file: # Only attempt to read if a report file is configured
 
144
  try:
145
  full_report_text = server_report_path.read_text(encoding='utf-8')
146
  except FileNotFoundError:
147
+ logger.error(f"λ³΄κ³ μ„œ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {server_report_path}")
148
+ return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'의 νŒŒμΌμ„ μ„œλ²„μ—μ„œ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}), 500
149
  except Exception as e:
150
+ logger.error(f"λ³΄κ³ μ„œ 파일 {server_report_path} 읽기 였λ₯˜: {e}", exc_info=True)
151
+ return jsonify({"error": "λ³΄κ³ μ„œ νŒŒμΌμ„ μ½λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."}), 500
152
  else: # If report_file is not configured (e.g. empty string from selected_report_info)
153
+ logger.info(f"λ³΄κ³ μ„œ '{report_name}'에 λŒ€ν•œ λ³΄κ³ μ„œ 파일이 μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈμ— 전체 λ³΄κ³ μ„œ ν…μŠ€νŠΈ 없이 μ§„ν–‰ν•©λ‹ˆλ‹€.")
154
 
155
+ # 이미지 νƒ€μž…μ— λ”°λ₯Έ ν•œκΈ€ ν‘œμ‹œ
156
+ image_type_korean = '흉뢀 X-레이' if image_type == 'CXR' else ('CT' if image_type == 'CT' else '의료 μ˜μƒ')
157
+
158
  system_prompt = (
159
+ "당신은 μΌλ°˜μΈμ„ λŒ€μƒμœΌλ‘œ μ„€λͺ…ν•˜λŠ” μž„μƒμ˜μž…λ‹ˆλ‹€. "
160
+ f"ν•™μŠ΅ 쀑인 μ‚¬μš©μžκ°€ 방사선 λ³΄κ³ μ„œμ˜ ν•œ λ¬Έμž₯을 μ œκ³΅ν–ˆκ³  ν•¨κ»˜ 제곡된 {image_type_korean} μ˜μƒμ„ 보고 μžˆμŠ΅λ‹ˆλ‹€. "
161
+ "λ‹Ήμ‹ μ˜ μž„λ¬΄λŠ” 제곡된 λ¬Έμž₯만의 의미λ₯Ό κ°„λ‹¨ν•˜κ³  λͺ…ν™•ν•œ μš©μ–΄λ‘œ μ„€λͺ…ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. μ „λ¬Έ μš©μ–΄μ™€ μ•½μ–΄λ₯Ό μ„€λͺ…ν•˜μ„Έμš”. κ°„κ²°ν•˜κ²Œ μœ μ§€ν•˜μ„Έμš”. "
162
+ "λ¬Έμž₯의 의미λ₯Ό μ§μ ‘μ μœΌλ‘œ μ„€λͺ…ν•˜μ„Έμš”. 'λ„€' λ˜λŠ” 'μ•Œκ² μŠ΅λ‹ˆλ‹€'와 같은 λ„μž… 문ꡬλ₯Ό μ‚¬μš©ν•˜μ§€ 말고, λ¬Έμž₯ μžμ²΄λ‚˜ λ³΄κ³ μ„œ 자체λ₯Ό μ–ΈκΈ‰ν•˜μ§€ λ§ˆμ„Έμš”(예: '이 λ¬Έμž₯은...'). "
163
+ f"{f'μ€‘μš”ν•œ 점은, μ‚¬μš©μžκ°€ {image_type_korean} μ˜μƒμ„ 보고 μžˆμœΌλ―€λ‘œ, ν•΄λ‹Ήλ˜λŠ” 경우 μ„€λͺ…을 μ΄ν•΄ν•˜κΈ° μœ„ν•΄ μ˜μƒμ˜ μ–΄λŠ 뢀뢄을 봐야 ν•˜λŠ”μ§€ μ•ˆλ‚΄λ₯Ό μ œκ³΅ν•˜μ„Έμš”. ' if image_type != 'CT' else ''}"
164
+ "μ‚¬μš©μžκ°€ λͺ…μ‹œμ μœΌλ‘œ μ œκ³΅ν•˜μ§€ μ•Šμ€ λ³΄κ³ μ„œμ˜ λ‹€λ₯Έ λΆ€λΆ„μ΄λ‚˜ λ¬Έμž₯에 λŒ€ν•΄μ„œλŠ” λ…Όμ˜ν•˜μ§€ λ§ˆμ„Έμš”. ν…μŠ€νŠΈμ— μžˆλŠ” μ‚¬μ‹€λ§Œμ„ κ³ μˆ˜ν•˜μ„Έμš”. μΆ”λ‘ ν•˜μ§€ λ§ˆμ„Έμš”. \n"
165
  "===\n"
166
+ f"μ°Έκ³ λ₯Ό μœ„ν•œ 전체 λ³΄κ³ μ„œ:\n{full_report_text}"
167
  )
168
+ user_prompt_text = f"방사선 λ³΄κ³ μ„œμ˜ 이 λ¬Έμž₯을 μ„€λͺ…ν•΄μ£Όμ„Έμš”: '{selected_sentence}'"
169
 
170
  messages_for_api = [
171
  {"role": "system", "content": system_prompt},
 
180
  cache_key = f"explain::{report_name}::{selected_sentence}"
181
  cached_result = cache.get(cache_key)
182
  if cached_result:
183
+ logger.info("μΊμ‹œλœ μ„€λͺ…을 λ°˜ν™˜ν•©λ‹ˆλ‹€.")
184
  return jsonify({"explanation": cached_result})
185
 
186
  try:
187
+ logger.info("base64 이미지와 ν•¨κ»˜ LLM API(REST)둜 μš”μ²­μ„ 전솑 쀑...")
188
  response = make_chat_completion_request(
189
  model="tgi",
190
  messages=messages_for_api,
 
197
  frequency_penalty=None,
198
  presence_penalty=None
199
  )
200
+ logger.info("LLM API(REST)λ‘œλΆ€ν„° 응닡 μŠ€νŠΈλ¦Όμ„ μˆ˜μ‹ ν–ˆμŠ΅λ‹ˆλ‹€.")
201
 
202
  explanation_parts = []
203
  for line in response.iter_lines():
 
212
  if chunk.get("choices") and chunk["choices"][0].get("delta") and chunk["choices"][0]["delta"].get("content"):
213
  explanation_parts.append(chunk["choices"][0]["delta"]["content"])
214
  except json.JSONDecodeError:
215
+ logger.warning(f"슀트림 μ²­ν¬μ—μ„œ JSON을 λ””μ½”λ”©ν•  수 μ—†μŠ΅λ‹ˆλ‹€: {json_data_str}")
216
  # Depending on API, might need to handle partial JSON or other errors
217
  elif decoded_line.strip() == "[DONE]": # Some APIs might send [DONE] without "data: "
218
  break
 
221
  if explanation:
222
  cache.set(cache_key, explanation, expire=None)
223
 
224
+ logger.info("μ„€λͺ…이 μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€." if explanation else "APIλ‘œλΆ€ν„° 빈 μ„€λͺ…을 λ°›μ•˜μŠ΅λ‹ˆλ‹€.")
225
+ return jsonify({"explanation": explanation or "APIλ‘œλΆ€ν„° μ„€λͺ… λ‚΄μš©μ„ λ°›μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."})
226
  except requests.exceptions.RequestException as e:
227
+ logger.error(f"LLM API(REST) 호좜 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
228
+ user_error_message = ("μ„€λͺ… 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μ„œλΉ„μŠ€κ°€ μΌμ‹œμ μœΌλ‘œ μ‚¬μš©ν•  수 μ—†μœΌλ©° "
229
+ "ν˜„μž¬ μ‹œμž‘ 쀑일 수 μžˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.")
230
+ return jsonify({"error": user_error_message}), 500