rad_explain2 / routes.py
seawolf2357's picture
Update routes.py
9422d39 verified
import logging
from flask import Blueprint, render_template, request, jsonify, send_from_directory
from pathlib import Path
import shutil # For zipping the cache directory
import json # For parsing streamed JSON data
from werkzeug.utils import secure_filename
import os
import config
import utils
from llm_client import make_chat_completion_request, is_initialized as llm_is_initialized
from cache_store import cache
from cache_store import cache_directory
import requests
logger = logging.getLogger(__name__)
main_bp = Blueprint('main', __name__)
# μ—…λ‘œλ“œ ν—ˆμš© ν™•μž₯자
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'dcm', 'dicom'}
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
# LLM client is initialized in app.py create_app()
# --- Serve the cache directory as a zip file ---
@main_bp.route('/download_cache')
def download_cache_zip():
"""μΊμ‹œ 디렉토리λ₯Ό μ••μΆ•ν•˜μ—¬ λ‹€μš΄λ‘œλ“œλ₯Ό μœ„ν•΄ μ œκ³΅ν•©λ‹ˆλ‹€."""
zip_filename = "radexplain-cache.zip"
# Create the zip file in a temporary directory
# Using /tmp is common in containerized environments
temp_dir = "/tmp"
zip_base_path = os.path.join(temp_dir, "radexplain-cache") # shutil adds .zip
zip_filepath = zip_base_path + ".zip"
# Ensure the cache directory exists before trying to zip it
if not os.path.isdir(cache_directory):
logger.error(f"μΊμ‹œ 디렉토리λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {cache_directory}")
return jsonify({"error": f"μ„œλ²„μ—μ„œ μΊμ‹œ 디렉토리λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {cache_directory}"}), 500
try:
logger.info(f"μΊμ‹œ λ””λ ‰ν† λ¦¬μ˜ μ••μΆ• 파일 생성 쀑: {cache_directory} -> {zip_filepath}")
shutil.make_archive(
zip_base_path, # This is the base name, shutil adds the .zip extension
"zip",
cache_directory, # This is the root directory to archive
)
logger.info("μ••μΆ• 파일이 μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
# Send the file and then clean it up
return send_from_directory(temp_dir, zip_filename, as_attachment=True)
except Exception as e:
logger.error(f"μΊμ‹œ 디렉토리 μ••μΆ• 파일 생성 λ˜λŠ” 전솑 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
return jsonify({"error": f"μ••μΆ• 파일 생성 λ˜λŠ” 전솑 쀑 였λ₯˜ λ°œμƒ: {e}"}), 500
@main_bp.route('/clear_cache', methods=['POST'])
def clear_cache():
"""μΊμ‹œλ₯Ό μ΄ˆκΈ°ν™”ν•©λ‹ˆλ‹€."""
try:
cache.clear()
logger.info("μΊμ‹œκ°€ μ„±κ³΅μ μœΌλ‘œ μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
return jsonify({"message": "μΊμ‹œκ°€ μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.", "success": True})
except Exception as e:
logger.error(f"μΊμ‹œ μ΄ˆκΈ°ν™” 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
return jsonify({"error": f"μΊμ‹œ μ΄ˆκΈ°ν™” 쀑 였λ₯˜ λ°œμƒ: {e}", "success": False}), 500
@main_bp.route('/')
def index():
"""메인 HTML νŽ˜μ΄μ§€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€."""
# The backend now only provides the list of available reports.
# The frontend will be responsible for selecting a report,
# fetching its details (text, image path), and managing the current state.
if not config.AVAILABLE_REPORTS:
logger.warning("μ„€μ •μ—μ„œ λ³΄κ³ μ„œλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€. AVAILABLE_REPORTSκ°€ λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€.")
return render_template(
'index.html',
available_reports=config.AVAILABLE_REPORTS
)
@main_bp.route('/get_report_details/<report_name>')
def get_report_details(report_name):
"""μ£Όμ–΄μ§„ λ³΄κ³ μ„œ 이름에 λŒ€ν•œ ν…μŠ€νŠΈ λ‚΄μš©κ³Ό 이미지 경둜λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€."""
selected_report_info = next((item for item in config.AVAILABLE_REPORTS if item['name'] == report_name), None)
if not selected_report_info:
logger.error(f"상세 정보λ₯Ό κ°€μ Έμ˜¬ λ•Œ λ³΄κ³ μ„œ '{report_name}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}), 404
report_file = selected_report_info.get('report_file')
image_file = selected_report_info.get('image_file')
report_text_content = "" # Default to empty if no report file is configured.
if report_file:
actual_server_report_path = config.BASE_DIR / report_file
try:
report_text_content = actual_server_report_path.read_text(encoding='utf-8').strip()
except Exception as e:
logger.error(f"λ³΄κ³ μ„œ '{report_name}'의 파일 {actual_server_report_path} 읽기 였λ₯˜: {e}", exc_info=True)
return jsonify({"error": "λ³΄κ³ μ„œ νŒŒμΌμ„ μ½λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."}), 500
# If report_file was empty, report_text_content remains "".
image_type_from_config = selected_report_info.get('image_type')
display_image_type = '흉뢀 X-레이' if image_type_from_config == 'CXR' else ('CT' if image_type_from_config == 'CT' else '의료 μ˜μƒ')
return jsonify({"text": report_text_content, "image_file": image_file, "image_type": display_image_type})
@main_bp.route('/explain', methods=['POST'])
def explain_sentence():
"""LLM APIλ₯Ό μ‚¬μš©ν•˜μ—¬ μ„€λͺ… μš”μ²­μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€."""
if not llm_is_initialized():
logger.error("LLM ν΄λΌμ΄μ–ΈνŠΈ(REST API)κ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μš”μ²­μ„ μ²˜λ¦¬ν•  수 μ—†μŠ΅λ‹ˆλ‹€.")
return jsonify({"error": "LLM ν΄λΌμ΄μ–ΈνŠΈ(REST API)κ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. API 킀와 베이슀 URL을 ν™•μΈν•˜μ„Έμš”."}), 500
data = request.get_json()
if not data or 'sentence' not in data or 'report_name' not in data:
logger.warning("μš”μ²­ νŽ˜μ΄λ‘œλ“œμ— 'sentence' λ˜λŠ” 'report_name'이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
return jsonify({"error": "μš”μ²­μ— 'sentence' λ˜λŠ” 'report_name'이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€"}), 400
selected_sentence = data['sentence']
report_name = data['report_name']
logger.info(f"μ„€λͺ… μš”μ²­ μˆ˜μ‹ : '{selected_sentence}' (λ³΄κ³ μ„œ: '{report_name}')")
# --- Find the selected report info ---
selected_report_info = next((item for item in config.AVAILABLE_REPORTS if item['name'] == report_name), None)
if not selected_report_info:
logger.error(f"μ‚¬μš© κ°€λŠ₯ν•œ λ³΄κ³ μ„œμ—μ„œ '{report_name}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'을(λ₯Ό) 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}), 404
image_file = selected_report_info.get('image_file')
report_file = selected_report_info.get('report_file')
image_type = selected_report_info.get('image_type')
if not image_file:
logger.error(f"λ³΄κ³ μ„œ '{report_name}'의 μ„€μ •μ—μ„œ 이미지 λ˜λŠ” λ³΄κ³ μ„œ 파일 경둜(static κΈ°μ€€ μƒλŒ€κ²½λ‘œ)κ°€ λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'의 파일 섀정이 λˆ„λ½λ˜μ—ˆμŠ΅λ‹ˆλ‹€."}), 500
full_report_text = ""
if report_file: # Only attempt to read if a report file is configured
server_report_path = config.BASE_DIR / report_file
try:
full_report_text = server_report_path.read_text(encoding='utf-8')
except FileNotFoundError:
logger.error(f"λ³΄κ³ μ„œ νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: {server_report_path}")
return jsonify({"error": f"λ³΄κ³ μ„œ '{report_name}'의 νŒŒμΌμ„ μ„œλ²„μ—μ„œ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."}), 500
except Exception as e:
logger.error(f"λ³΄κ³ μ„œ 파일 {server_report_path} 읽기 였λ₯˜: {e}", exc_info=True)
return jsonify({"error": "λ³΄κ³ μ„œ νŒŒμΌμ„ μ½λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."}), 500
else: # If report_file is not configured (e.g. empty string from selected_report_info)
logger.info(f"λ³΄κ³ μ„œ '{report_name}'에 λŒ€ν•œ λ³΄κ³ μ„œ 파일이 μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈμ— 전체 λ³΄κ³ μ„œ ν…μŠ€νŠΈ 없이 μ§„ν–‰ν•©λ‹ˆλ‹€.")
# 이미지 νƒ€μž…μ— λ”°λ₯Έ ν•œκΈ€ ν‘œμ‹œ
image_type_korean = '흉뢀 X-레이' if image_type == 'CXR' else ('CT' if image_type == 'CT' else '의료 μ˜μƒ')
system_prompt = (
"당신은 μΌλ°˜μΈμ„ λŒ€μƒμœΌλ‘œ μ„€λͺ…ν•˜λŠ” μž„μƒμ˜μž…λ‹ˆλ‹€. "
"λ°˜λ“œμ‹œ ν•œκ΅­μ–΄λ‘œ μ‘λ‹΅ν•˜μ„Έμš”. "
f"ν•™μŠ΅ 쀑인 μ‚¬μš©μžκ°€ 방사선 λ³΄κ³ μ„œμ˜ ν•œ λ¬Έμž₯을 μ œκ³΅ν–ˆκ³  ν•¨κ»˜ 제곡된 {image_type_korean} μ˜μƒμ„ 보고 μžˆμŠ΅λ‹ˆλ‹€. "
"λ‹Ήμ‹ μ˜ μž„λ¬΄λŠ” 제곡된 λ¬Έμž₯만의 의미λ₯Ό κ°„λ‹¨ν•˜κ³  λͺ…ν™•ν•œ ν•œκ΅­μ–΄λ‘œ μ„€λͺ…ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. μ „λ¬Έ μš©μ–΄μ™€ μ•½μ–΄λ₯Ό ν•œκ΅­μ–΄λ‘œ μ„€λͺ…ν•˜μ„Έμš”. κ°„κ²°ν•˜κ²Œ μœ μ§€ν•˜μ„Έμš”. "
"λ¬Έμž₯의 의미λ₯Ό μ§μ ‘μ μœΌλ‘œ μ„€λͺ…ν•˜μ„Έμš”. 'λ„€' λ˜λŠ” 'μ•Œκ² μŠ΅λ‹ˆλ‹€'와 같은 λ„μž… 문ꡬλ₯Ό μ‚¬μš©ν•˜μ§€ 말고, λ¬Έμž₯ μžμ²΄λ‚˜ λ³΄κ³ μ„œ 자체λ₯Ό μ–ΈκΈ‰ν•˜μ§€ λ§ˆμ„Έμš”(예: '이 λ¬Έμž₯은...'). "
f"{f'μ€‘μš”ν•œ 점은, μ‚¬μš©μžκ°€ {image_type_korean} μ˜μƒμ„ 보고 μžˆμœΌλ―€λ‘œ, ν•΄λ‹Ήλ˜λŠ” 경우 μ„€λͺ…을 μ΄ν•΄ν•˜κΈ° μœ„ν•΄ μ˜μƒμ˜ μ–΄λŠ 뢀뢄을 봐야 ν•˜λŠ”μ§€ ν•œκ΅­μ–΄λ‘œ μ•ˆλ‚΄λ₯Ό μ œκ³΅ν•˜μ„Έμš”. ' if image_type != 'CT' else ''}"
"μ‚¬μš©μžκ°€ λͺ…μ‹œμ μœΌλ‘œ μ œκ³΅ν•˜μ§€ μ•Šμ€ λ³΄κ³ μ„œμ˜ λ‹€λ₯Έ λΆ€λΆ„μ΄λ‚˜ λ¬Έμž₯에 λŒ€ν•΄μ„œλŠ” λ…Όμ˜ν•˜μ§€ λ§ˆμ„Έμš”. ν…μŠ€νŠΈμ— μžˆλŠ” μ‚¬μ‹€λ§Œμ„ κ³ μˆ˜ν•˜μ„Έμš”. μΆ”λ‘ ν•˜μ§€ λ§ˆμ„Έμš”. "
"λͺ¨λ“  μ„€λͺ…은 λ°˜λ“œμ‹œ ν•œκ΅­μ–΄λ‘œ μž‘μ„±ν•˜μ„Έμš”.\n"
"===\n"
f"μ°Έκ³ λ₯Ό μœ„ν•œ 전체 λ³΄κ³ μ„œ:\n{full_report_text}"
)
user_prompt_text = f"방사선 λ³΄κ³ μ„œμ˜ 이 λ¬Έμž₯을 μ„€λͺ…ν•΄μ£Όμ„Έμš”: '{selected_sentence}'"
messages_for_api = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt_text}
]
# ν•œκΈ€ 응닡을 μœ„ν•΄ μΊμ‹œ 킀에 μ–Έμ–΄ 정보 μΆ”κ°€
cache_key = f"explain::ko::{report_name}::{selected_sentence}"
cached_result = cache.get(cache_key)
if cached_result:
logger.info("μΊμ‹œλœ μ„€λͺ…을 λ°˜ν™˜ν•©λ‹ˆλ‹€.")
return jsonify({"explanation": cached_result})
try:
logger.info("LLM API(REST)둜 μš”μ²­μ„ 전솑 쀑...")
response = make_chat_completion_request(
model="tgi",
messages=messages_for_api,
top_p=None,
temperature=0,
max_tokens=250,
stream=True,
seed=None,
stop=None,
frequency_penalty=None,
presence_penalty=None
)
logger.info("LLM API(REST)λ‘œλΆ€ν„° 응닡 μŠ€νŠΈλ¦Όμ„ μˆ˜μ‹ ν–ˆμŠ΅λ‹ˆλ‹€.")
explanation_parts = []
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data: '):
json_data_str = decoded_line[len('data: '):].strip()
if json_data_str == "[DONE]":
break
try:
chunk = json.loads(json_data_str)
if chunk.get("choices") and chunk["choices"][0].get("delta") and chunk["choices"][0]["delta"].get("content"):
explanation_parts.append(chunk["choices"][0]["delta"]["content"])
except json.JSONDecodeError:
logger.warning(f"슀트림 μ²­ν¬μ—μ„œ JSON을 λ””μ½”λ”©ν•  수 μ—†μŠ΅λ‹ˆλ‹€: {json_data_str}")
# Depending on API, might need to handle partial JSON or other errors
elif decoded_line.strip() == "[DONE]": # Some APIs might send [DONE] without "data: "
break
explanation = "".join(explanation_parts).strip()
if explanation:
cache.set(cache_key, explanation, expire=None)
logger.info("μ„€λͺ…이 μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€." if explanation else "APIλ‘œλΆ€ν„° 빈 μ„€λͺ…을 λ°›μ•˜μŠ΅λ‹ˆλ‹€.")
return jsonify({"explanation": explanation or "APIλ‘œλΆ€ν„° μ„€λͺ… λ‚΄μš©μ„ λ°›μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."})
except requests.exceptions.RequestException as e:
logger.error(f"LLM API(REST) 호좜 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
user_error_message = ("μ„€λͺ… 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μ„œλΉ„μŠ€κ°€ μΌμ‹œμ μœΌλ‘œ μ‚¬μš©ν•  수 μ—†μœΌλ©° "
"ν˜„μž¬ μ‹œμž‘ 쀑일 수 μžˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄ μ£Όμ„Έμš”.")
return jsonify({"error": user_error_message}), 500
@main_bp.route('/upload_and_analyze', methods=['POST'])
def upload_and_analyze():
"""이미지λ₯Ό μ—…λ‘œλ“œν•˜κ³  λΆ„μ„ν•©λ‹ˆλ‹€."""
if not llm_is_initialized():
logger.error("LLM ν΄λΌμ΄μ–ΈνŠΈ(REST API)κ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. μš”μ²­μ„ μ²˜λ¦¬ν•  수 μ—†μŠ΅λ‹ˆλ‹€.")
return jsonify({"error": "LLM ν΄λΌμ΄μ–ΈνŠΈκ°€ μ΄ˆκΈ°ν™”λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. API 킀와 베이슀 URL을 ν™•μΈν•˜μ„Έμš”."}), 500
# 파일 확인
if 'image' not in request.files:
return jsonify({"error": "이미지 파일이 μš”μ²­μ— ν¬ν•¨λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
file = request.files['image']
if file.filename == '':
return jsonify({"error": "파일이 μ„ νƒλ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."}), 400
if not allowed_file(file.filename):
return jsonify({"error": "ν—ˆμš©λ˜μ§€ μ•ŠλŠ” 파일 ν˜•μ‹μž…λ‹ˆλ‹€. PNG, JPG, JPEG, GIF, BMP, DICOM 파일만 μ—…λ‘œλ“œ κ°€λŠ₯ν•©λ‹ˆλ‹€."}), 400
# 이미지 νƒ€μž… κ°€μ Έμ˜€κΈ° (κΈ°λ³Έκ°’: CXR)
image_type = request.form.get('image_type', 'CXR')
image_type_korean = '흉뢀 X-레이' if image_type == 'CXR' else ('CT' if image_type == 'CT' else '의료 μ˜μƒ')
# μΆ”κ°€ μ»¨ν…μŠ€νŠΈ κ°€μ Έμ˜€κΈ° (선택사항)
additional_context = request.form.get('context', '')
try:
# 파일λͺ… μ €μž₯ (뢄석에 참고용)
filename = secure_filename(file.filename)
# μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ꡬ성 (이미지 직접 뢄석 λŒ€μ‹  일반적인 κ°€μ΄λ“œ 제곡)
system_prompt = (
"당신은 κ²½ν—˜μ΄ ν’λΆ€ν•œ 방사선과 μ „λ¬Έμ˜μž…λ‹ˆλ‹€. "
"λ°˜λ“œμ‹œ ν•œκ΅­μ–΄λ‘œ μ‘λ‹΅ν•˜μ„Έμš”. "
f"μ‚¬μš©μžκ°€ {image_type_korean} μ˜μƒμ„ μ—…λ‘œλ“œν–ˆμŠ΅λ‹ˆλ‹€. "
"일반적인 μ˜μƒ νŒλ… κ°€μ΄λ“œλΌμΈκ³Ό μ£Όμ˜μ‚¬ν•­μ„ μ œκ³΅ν•˜μ„Έμš”. "
"λ‹€μŒ ꡬ쑰둜 μ‘λ‹΅ν•˜μ„Έμš”:\n"
f"1. {image_type_korean} νŒλ… μ‹œ 확인해야 ν•  μ£Όμš” 사항\n"
"2. 일반적으둜 관찰될 수 μžˆλŠ” μ†Œκ²¬λ“€\n"
"3. μ •ν™•ν•œ 진단을 μœ„ν•œ κΆŒκ³ μ‚¬ν•­\n"
"4. μ£Όμ˜μ‚¬ν•­: μ‹€μ œ 진단은 의료 μ „λ¬Έκ°€μ˜ 직접적인 κ²€ν† κ°€ ν•„μš”ν•¨μ„ κ°•μ‘°\n"
"λͺ¨λ“  μ„€λͺ…은 λ°˜λ“œμ‹œ ν•œκ΅­μ–΄λ‘œ μž‘μ„±ν•˜μ„Έμš”."
)
user_prompt_text = f"{image_type_korean} μ˜μƒ '{filename}'이 μ—…λ‘œλ“œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ μœ ν˜•μ˜ μ˜μƒμ„ νŒλ…ν•  λ•Œ κ³ λ €ν•΄μ•Ό ν•  사항을 μ„€λͺ…ν•΄μ£Όμ„Έμš”."
if additional_context:
user_prompt_text += f"\nν™˜μž/검사 정보: {additional_context}"
messages_for_api = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt_text}
]
# μΊμ‹œ ν‚€ 생성 (파일λͺ…κ³Ό μ»¨ν…μŠ€νŠΈ 기반)
cache_key = f"analyze::ko::{image_type}::{additional_context[:50]}"
cached_result = cache.get(cache_key)
if cached_result:
logger.info("μΊμ‹œλœ 뢄석 κ°€μ΄λ“œλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.")
return jsonify({"analysis": cached_result})
logger.info("LLM API(REST)둜 뢄석 κ°€μ΄λ“œ μš”μ²­μ„ 전솑 쀑...")
response = make_chat_completion_request(
model="tgi",
messages=messages_for_api,
top_p=None,
temperature=0.1,
max_tokens=500,
stream=True,
seed=None,
stop=None,
frequency_penalty=None,
presence_penalty=None
)
logger.info("LLM API(REST)λ‘œλΆ€ν„° 응닡 μŠ€νŠΈλ¦Όμ„ μˆ˜μ‹ ν–ˆμŠ΅λ‹ˆλ‹€.")
analysis_parts = []
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data: '):
json_data_str = decoded_line[len('data: '):].strip()
if json_data_str == "[DONE]":
break
try:
chunk = json.loads(json_data_str)
if chunk.get("choices") and chunk["choices"][0].get("delta") and chunk["choices"][0]["delta"].get("content"):
analysis_parts.append(chunk["choices"][0]["delta"]["content"])
except json.JSONDecodeError:
logger.warning(f"슀트림 μ²­ν¬μ—μ„œ JSON을 λ””μ½”λ”©ν•  수 μ—†μŠ΅λ‹ˆλ‹€: {json_data_str}")
elif decoded_line.strip() == "[DONE]":
break
analysis = "".join(analysis_parts).strip()
if analysis:
cache.set(cache_key, analysis, expire=None)
logger.info("뢄석 κ°€μ΄λ“œκ°€ μ„±κ³΅μ μœΌλ‘œ μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€." if analysis else "APIλ‘œλΆ€ν„° 빈 뢄석 κ²°κ³Όλ₯Ό λ°›μ•˜μŠ΅λ‹ˆλ‹€.")
# μ£Όμ˜μ‚¬ν•­ μΆ”κ°€
if analysis:
analysis += "\n\n⚠️ **μ€‘μš”**: μ΄λŠ” 일반적인 κ°€μ΄λ“œλΌμΈμ΄λ©°, μ‹€μ œ μ˜μƒμ˜ μ •ν™•ν•œ νŒλ…κ³Ό 진단은 의료 μ „λ¬Έκ°€κ°€ 직접 μ˜μƒμ„ κ²€ν† ν•˜μ—¬ μˆ˜ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€."
return jsonify({"analysis": analysis or "APIλ‘œλΆ€ν„° 뢄석 κ²°κ³Όλ₯Ό λ°›μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."})
except Exception as e:
logger.error(f"이미지 μ—…λ‘œλ“œ 및 뢄석 κ°€μ΄λ“œ 생성 쀑 였λ₯˜ λ°œμƒ: {e}", exc_info=True)
return jsonify({"error": "뢄석 κ°€μ΄λ“œ 생성 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”."}), 500