nawhgnuj commited on
Commit
67064aa
ยท
verified ยท
1 Parent(s): 795c3d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1799 -152
app.py CHANGED
@@ -1,152 +1,1799 @@
1
- def main():
2
- from dotenv import load_dotenv
3
- load_dotenv()
4
-
5
- # Step_1 : Setup UI with streamlit (model provider, model, system prompt, web search query)
6
- import requests
7
- import streamlit as st
8
- from streamlit_lottie import st_lottie
9
-
10
- # ---------- Page Configuration ----------
11
- st.set_page_config(
12
- page_title="LangGraph AI Chatbot",
13
- layout="centered",
14
- initial_sidebar_state="collapsed"
15
- )
16
-
17
- # ----------------- CUSTOM CSS ------------------
18
- st.markdown(
19
- """
20
- <style>
21
- body {
22
- background-color: #111827;
23
- color: #d1d5db;
24
- font-family: 'Segoe UI', sans-serif;
25
- }
26
- .sidebar-content {
27
- background-color: #1f2937;
28
- padding: 20px;
29
- }
30
- .chat-message {
31
- padding: 10px;
32
- border-radius: 10px;
33
- margin-bottom: 10px;
34
- font-size: 16px;
35
- }
36
- .user-message {
37
- background-color: #374151;
38
- }
39
- .bot-message {
40
- background-color: #4b5563;
41
- }
42
- .avatar {
43
- width: 30px;
44
- height: 30px;
45
- border-radius: 50%;
46
- margin-right: 10px;
47
- }
48
- button[kind="primary"] {
49
- background-color: #10b981;
50
- color: white;
51
- border-radius: 10px;
52
- border: none;
53
- }
54
- </style>
55
- """,
56
- unsafe_allow_html=True
57
- )
58
-
59
- # ---------- Lottie Animation ----------
60
- def load_lottie_url(url: str):
61
- r = requests.get(url)
62
- if r.status_code != 200:
63
- return None
64
- return r.json()
65
-
66
- lottie_ai = load_lottie_url("https://assets10.lottiefiles.com/packages/lf20_kkflmtur.json")
67
- st_lottie(lottie_ai, height=200)
68
-
69
-
70
- # ---------- Avatar Config ----------
71
- USER_AVATAR = "https://cdn-icons-png.flaticon.com/512/9131/9131529.png"
72
- AGENT_AVATAR = "https://cdn-icons-png.flaticon.com/512/4712/4712100.png"
73
-
74
-
75
- # ---------- Title and Prompt ----------
76
- st.title("AI Chatbot")
77
- st.caption("Ask your AI agent anything โ€” powered by Groq and LangGraph!")
78
-
79
- # ---------- Initialize Session State ----------
80
- if "chat_history" not in st.session_state:
81
- st.session_state.chat_history = []
82
-
83
- # ---------- Inputs ----------
84
- MODEL_NAMES_GROQ = ["llama-3.3-70b-versatile", "llama3-70b-8192"]
85
- system_prompt = st.text_area("Define your AI Agent : ", height=68, placeholder="Type your system prompt here...")
86
- select_model = st.selectbox("Select Model (Groq Only) : ", MODEL_NAMES_GROQ)
87
- allow_web_search = st.checkbox("Allow Web Search")
88
- user_query = st.text_area("Enter you query :", height=150, placeholder="Ask Anything!")
89
-
90
- API_URL = "https://ai-agent-backend-uzhn.onrender.com/chat"
91
-
92
- # ---------- Chat History Display ----------
93
- for entry in st.session_state.chat_history:
94
- with st.chat_message("user", avatar=USER_AVATAR):
95
- st.markdown(entry["user"])
96
- with st.chat_message("assistant", avatar=AGENT_AVATAR):
97
- st.markdown(entry["agent"])
98
-
99
- # ---------- Submit Button ----------
100
- if st.button("Ask Agent!"):
101
- if user_query.strip():
102
- # Step_2 : Connect with backend via URL
103
-
104
- payload = {
105
- "model_name" : select_model,
106
- "system_prompt" : system_prompt,
107
- "messages" : [user_query],
108
- "allow_search" : allow_web_search
109
- }
110
- with st.spinner("Thinking... ๐Ÿ’ญ"):
111
- try:
112
- response = requests.post(API_URL, json=payload)
113
-
114
- if response.status_code == 200:
115
- try:
116
- response_data = response.json()
117
- except ValueError:
118
- st.error("โš ๏ธ Unable to parse the server response.")
119
- response_data = {"response": response.text}
120
-
121
- if isinstance(response_data, dict) and "error" in response_data:
122
- st.error(response_data["error"])
123
- else:
124
- final_response = (
125
- response_data if isinstance(response_data, str)
126
- else response_data .get("response", str(response_data))
127
- )
128
-
129
- # Display new message
130
- with st.chat_message("user", avatar=USER_AVATAR):
131
- st.markdown(user_query)
132
- with st.chat_message("assistant", avatar=AGENT_AVATAR):
133
- st.markdown(final_response)
134
-
135
- # Feedback section
136
- feedback = st.radio("Was this helpful?", ["๐Ÿ‘ Yes", "๐Ÿ‘Ž No"], horizontal=True)
137
- # Export option
138
- st.download_button(
139
- label = "๐Ÿ“„ Download Response",
140
- data = final_response,
141
- file_name = "agent_response.txt",
142
- mime = "text/plain"
143
- )
144
- else:
145
- st.error(f"๐Ÿšซ Server error: {response.status_code}")
146
- except Exception as e:
147
- st.error(f"โŒ Backend connection error: {e}")
148
-
149
-
150
- # End of main
151
- if __name__ == "__main__":
152
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import glob
4
+ import tempfile
5
+ from typing import Dict, List, TypedDict, Optional, Tuple, Set, Any, Union
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ import numpy as np
9
+ import pandas as pd
10
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
11
+ from langchain_community.vectorstores import FAISS
12
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
13
+ from langchain.schema import Document
14
+ from langgraph.graph import StateGraph, END
15
+ import json
16
+ from datetime import datetime
17
+ import logging
18
+ import streamlit as st
19
+ from streamlit_lottie import st_lottie
20
+ import requests
21
+
22
+ # ๋กœ๊น… ์„ค์ •
23
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # ========== ๋ฐ์ดํ„ฐ ๋ชจ๋ธ ์ •์˜ ==========
27
+
28
+ class DiseaseStage(Enum):
29
+ """์‹ ์žฅ ์งˆํ™˜ ๋‹จ๊ณ„"""
30
+ CKD_1 = "CKD Stage 1"
31
+ CKD_2 = "CKD Stage 2"
32
+ CKD_3 = "CKD Stage 3"
33
+ CKD_4 = "CKD Stage 4"
34
+ CKD_5 = "CKD Stage 5"
35
+ DIALYSIS = "Dialysis"
36
+ TRANSPLANT = "Transplant"
37
+
38
+ class TaskType(Enum):
39
+ """์งˆ๋ฌธ ์œ ํ˜• ๋ถ„๋ฅ˜"""
40
+ DIET_RECOMMENDATION = "diet_recommendation" # ์‹๋‹จ ์ถ”์ฒœ
41
+ DIET_ANALYSIS = "diet_analysis" # ํŠน์ • ์‹ํ’ˆ ๋ถ„์„
42
+ MEDICATION = "medication" # ๋ณต์•ฝ ๊ด€๋ จ
43
+ LIFESTYLE = "lifestyle" # ์ƒํ™œ ๊ด€๋ฆฌ
44
+ DIAGNOSIS = "diagnosis" # ์ง„๋‹จ/๊ฒ€์‚ฌ
45
+ EXERCISE = "exercise" # ์šด๋™
46
+ GENERAL = "general" # ์ผ๋ฐ˜ ์ •๋ณด
47
+
48
+ @dataclass
49
+ class PatientConstraints:
50
+ """ํ™˜์ž ๊ฐœ๋ณ„ ์ œ์•ฝ์กฐ๊ฑด"""
51
+ egfr: float # ์‚ฌ๊ตฌ์ฒด์—ฌ๊ณผ์œจ
52
+ disease_stage: DiseaseStage
53
+ on_dialysis: bool
54
+ comorbidities: List[str] # ๋™๋ฐ˜์งˆํ™˜ ๋ชฉ๋ก
55
+ medications: List[str] # ๋ณต์šฉ ์•ฝ๋ฌผ ๋ชฉ๋ก
56
+ age: int
57
+ gender: str
58
+
59
+ # ์˜์–‘ ์ œํ•œ์‚ฌํ•ญ
60
+ protein_restriction: Optional[float] = None # g/day
61
+ sodium_restriction: Optional[float] = None # mg/day
62
+ potassium_restriction: Optional[float] = None # mg/day
63
+ phosphorus_restriction: Optional[float] = None # mg/day
64
+ fluid_restriction: Optional[float] = None # ml/day
65
+ calorie_target: Optional[float] = None # kcal/day
66
+
67
+ @dataclass
68
+ class RecommendationItem:
69
+ """์ถ”์ฒœ ํ•ญ๋ชฉ"""
70
+ name: str
71
+ category: str # ์‹์ด, ์šด๋™, ์•ฝ๋ฌผ ๋“ฑ
72
+ description: str
73
+ constraints_satisfied: bool
74
+ embedding: Optional[np.ndarray] = None
75
+
76
+ @dataclass
77
+ class FoodItem:
78
+ """์‹ํ’ˆ ์ •๋ณด (์‹ค์ œ CSV ๊ตฌ์กฐ ๋ฐ˜์˜)"""
79
+ food_code: str # ์‹ํ’ˆ์ฝ”๋“œ
80
+ name: str # ์‹ํ’ˆ๋ช…
81
+ food_category_major: str # ์‹ํ’ˆ๋Œ€๋ถ„๋ฅ˜๋ช…
82
+ food_category_minor: str # ์‹ํ’ˆ์ค‘๋ถ„๋ฅ˜๋ช…
83
+ serving_size: float # ์˜์–‘์„ฑ๋ถ„ํ•จ๋Ÿ‰๊ธฐ์ค€๋Ÿ‰ (๋ณดํ†ต 100g)
84
+ calories: float # ์—๋„ˆ์ง€(kcal)
85
+ water: float # ์ˆ˜๋ถ„(g)
86
+ protein: float # ๋‹จ๋ฐฑ์งˆ(g)
87
+ fat: float # ์ง€๋ฐฉ(g)
88
+ carbohydrate: float # ํƒ„์ˆ˜ํ™”๋ฌผ(g)
89
+ sugar: float # ๋‹น๋ฅ˜(g)
90
+ dietary_fiber: float # ์‹์ด์„ฌ์œ (g)
91
+ calcium: float # ์นผ์Š˜(mg)
92
+ iron: float # ์ฒ (mg)
93
+ phosphorus: float # ์ธ(mg)
94
+ potassium: float # ์นผ๋ฅจ(mg)
95
+ sodium: float # ๋‚˜ํŠธ๋ฅจ(mg)
96
+ cholesterol: float # ์ฝœ๋ ˆ์Šคํ…Œ๋กค(mg)
97
+ saturated_fat: float # ํฌํ™”์ง€๋ฐฉ์‚ฐ(g)
98
+
99
+ def get_nutrients_per_serving(self, serving_g: float = 100) -> Dict[str, float]:
100
+ """์ง€์ •๋œ ์–‘(g)์— ๋Œ€ํ•œ ์˜์–‘์†Œ ํ•จ๋Ÿ‰ ๊ณ„์‚ฐ"""
101
+ ratio = serving_g / self.serving_size
102
+ return {
103
+ 'calories': self.calories * ratio,
104
+ 'protein': self.protein * ratio,
105
+ 'fat': self.fat * ratio,
106
+ 'carbohydrate': self.carbohydrate * ratio,
107
+ 'sodium': self.sodium * ratio,
108
+ 'potassium': self.potassium * ratio,
109
+ 'phosphorus': self.phosphorus * ratio
110
+ }
111
+
112
+ def is_suitable_for_patient(self, constraints: PatientConstraints,
113
+ serving_g: float = 100) -> Tuple[bool, List[str]]:
114
+ """ํ™˜์ž ์ œ์•ฝ์กฐ๊ฑด์— ์ ํ•ฉํ•œ์ง€ ํ™•์ธ"""
115
+ issues = []
116
+ nutrients = self.get_nutrients_per_serving(serving_g)
117
+
118
+ # ์ผ์ผ ์ œํ•œ๋Ÿ‰์˜ 30%๋ฅผ ํ•œ ๋ผ ๊ธฐ์ค€์œผ๋กœ ์„ค์ •
119
+ meal_ratio = 0.3
120
+
121
+ # ๋‹จ๋ฐฑ์งˆ ์ฒดํฌ
122
+ if constraints.protein_restriction:
123
+ if nutrients['protein'] > constraints.protein_restriction * meal_ratio:
124
+ issues.append(f"๋‹จ๋ฐฑ์งˆ ํ•จ๋Ÿ‰์ด ๋†’์Œ ({nutrients['protein']:.1f}g)")
125
+
126
+ # ๋‚˜ํŠธ๋ฅจ ์ฒดํฌ
127
+ if constraints.sodium_restriction:
128
+ if nutrients['sodium'] > constraints.sodium_restriction * meal_ratio:
129
+ issues.append(f"๋‚˜ํŠธ๋ฅจ ํ•จ๋Ÿ‰์ด ๋†’์Œ ({nutrients['sodium']:.0f}mg)")
130
+
131
+ # ์นผ๋ฅจ ์ฒดํฌ
132
+ if constraints.potassium_restriction:
133
+ if nutrients['potassium'] > constraints.potassium_restriction * meal_ratio:
134
+ issues.append(f"์นผ๋ฅจ ํ•จ๋Ÿ‰์ด ๋†’์Œ ({nutrients['potassium']:.0f}mg)")
135
+
136
+ # ์ธ ์ฒดํฌ
137
+ if constraints.phosphorus_restriction:
138
+ if nutrients['phosphorus'] > constraints.phosphorus_restriction * meal_ratio:
139
+ issues.append(f"์ธ ํ•จ๋Ÿ‰์ด ๋†’์Œ ({nutrients['phosphorus']:.0f}mg)")
140
+
141
+ return len(issues) == 0, issues
142
+
143
+ # ========== State ์ •์˜ ==========
144
+
145
+ class GraphState(TypedDict):
146
+ """LangGraph State"""
147
+ user_query: str
148
+ patient_constraints: PatientConstraints
149
+ task_type: TaskType
150
+ draft_response: str
151
+ draft_items: List[RecommendationItem]
152
+ corrected_items: List[RecommendationItem]
153
+ final_response: str
154
+ catalog_results: List[Document]
155
+ iteration_count: int
156
+ error: Optional[str]
157
+ food_analysis_results: Optional[Dict[str, Any]]
158
+ recommended_foods: Optional[List[FoodItem]]
159
+ meal_plan: Optional[Dict[str, List[FoodItem]]]
160
+ current_node: str # ํ˜„์žฌ ์ฒ˜๋ฆฌ ์ค‘์ธ ๋…ธ๋“œ
161
+ processing_log: List[str] # ์ฒ˜๋ฆฌ ๋กœ๊ทธ
162
+
163
+ # ========== Catalog ๊ด€๋ฆฌ ==========
164
+
165
+ class KidneyDiseaseCatalog:
166
+ """์‹ ์žฅ์งˆํ™˜ ์ •๋ณด ์นดํƒˆ๋กœ๊ทธ - ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด ์ ์šฉ"""
167
+
168
+ _instance = None
169
+ _initialized = False
170
+
171
+ def __new__(cls, *args, **kwargs):
172
+ if cls._instance is None:
173
+ cls._instance = super(KidneyDiseaseCatalog, cls).__new__(cls)
174
+ return cls._instance
175
+
176
+ def __init__(self, documents_path: str = "./data"):
177
+ if KidneyDiseaseCatalog._initialized:
178
+ return
179
+
180
+ self.embeddings = OpenAIEmbeddings()
181
+ self.vectorstore = None
182
+ self.documents_path = documents_path
183
+ self.metadata_index = {} # ๋ฌธ์„œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ธ๋ฑ์Šค
184
+
185
+ # ํƒœ๊ทธ ๋งคํ•‘ ์ •์˜
186
+ self.field_mapping = {
187
+ "์‹์ด": "diet", "์šด๋™": "exercise", "์ง„๋‹จ": "diagnosis",
188
+ "๋ณต์•ฝ": "medication", "์น˜๋ฃŒ": "treatment", "๊ต์œก": "education",
189
+ "์ƒํ™œ": "lifestyle"
190
+ }
191
+
192
+ self.status_mapping = {
193
+ "CKD": "chronic_kidney_disease", "HD": "hemodialysis",
194
+ "PD": "peritoneal_dialysis", "DIA": "dialysis",
195
+ "TX": "transplant", "ALL": "all"
196
+ }
197
+
198
+ self.level_mapping = {
199
+ "COM": "common", "STD": "standard", "DM": "diabetes",
200
+ "HTN": "hypertension", "OLD": "elderly", "PREG": "pregnancy",
201
+ "OBES": "obesity", "SYM": "symptom"
202
+ }
203
+
204
+ self.priority_mapping = {
205
+ "S1": "emergency", "S2": "caution", "S3": "general", "S4": "reference"
206
+ }
207
+
208
+ # ์ดˆ๊ธฐํ™” ์‹œ ๋ฌธ์„œ ๋กœ๋“œ
209
+ self.load_documents()
210
+ KidneyDiseaseCatalog._initialized = True
211
+
212
+ def parse_filename_tags(self, filename: str) -> Dict[str, str]:
213
+ """ํŒŒ์ผ๋ช…์—์„œ ํƒœ๊ทธ ํŒŒ์‹ฑ"""
214
+ pattern = r'\[([^-]+)-([^-]+)-([^-]+)-([^\]]+)\]'
215
+ match = re.search(pattern, filename)
216
+
217
+ if match:
218
+ field, status, level, priority = match.groups()
219
+ return {
220
+ "field": self.field_mapping.get(field, field),
221
+ "patient_status": self.status_mapping.get(status, status),
222
+ "personalization_level": self.level_mapping.get(level, level),
223
+ "safety_priority": self.priority_mapping.get(priority, priority),
224
+ "raw_tags": f"{field}-{status}-{level}-{priority}"
225
+ }
226
+ return {}
227
+
228
+ def load_documents(self):
229
+ """๊ถŒ์œ„์žˆ๋Š” ๊ธฐ๊ด€์˜ ๋ฌธ์„œ๋“ค์„ ๋กœ๋“œ"""
230
+ if self.vectorstore is not None:
231
+ logger.info("Documents already loaded")
232
+ return
233
+
234
+ documents = []
235
+
236
+
237
+ # data ํด๋”์˜ ๋ชจ๋“  txt ํŒŒ์ผ ๋กœ๋“œ
238
+ file_pattern = os.path.join(self.documents_path, "*.txt")
239
+ file_paths = glob.glob(file_pattern)
240
+
241
+ if not file_paths:
242
+ logger.warning(f"No documents found in {self.documents_path}. Creating sample files...")
243
+ file_paths = self._create_comprehensive_sample_files()
244
+
245
+ for file_path in file_paths:
246
+ try:
247
+ filename = os.path.basename(file_path)
248
+ tags = self.parse_filename_tags(filename)
249
+
250
+ with open(file_path, 'r', encoding='utf-8') as f:
251
+ content = f.read()
252
+
253
+ title_pattern = r'^#\s*(.+)$'
254
+ title_match = re.search(title_pattern, content, re.MULTILINE)
255
+ if title_match:
256
+ title = title_match.group(1)
257
+ else:
258
+ title = filename.split(']')[-1].replace('.txt', '').strip()
259
+ if not title:
260
+ title = filename.replace('.txt', '')
261
+
262
+ source = self._extract_source(content, filename)
263
+
264
+ doc = Document(
265
+ page_content=content,
266
+ metadata={
267
+ "filename": filename,
268
+ "title": title,
269
+ "source": source,
270
+ "timestamp": datetime.now().isoformat(),
271
+ **tags
272
+ }
273
+ )
274
+ documents.append(doc)
275
+ self.metadata_index[filename] = doc.metadata
276
+ logger.info(f"Loaded document: {filename}")
277
+
278
+ except Exception as e:
279
+ logger.error(f"Error loading file {file_path}: {e}")
280
+ continue
281
+
282
+ # ํ…์ŠคํŠธ ๋ถ„ํ• 
283
+ text_splitter = RecursiveCharacterTextSplitter(
284
+ chunk_size=2000,
285
+ chunk_overlap=100,
286
+ separators=["\n\n", "\n", ".", "!", "?", ",", " ", ""]
287
+ )
288
+ split_docs = text_splitter.split_documents(documents)
289
+
290
+ for doc in split_docs:
291
+ doc.metadata["chunk_id"] = f"{doc.metadata['filename']}_{hash(doc.page_content)}"
292
+
293
+ self.vectorstore = FAISS.from_documents(split_docs, self.embeddings)
294
+ logger.info(f"Loaded {len(documents)} documents ({len(split_docs)} chunks) into vectorstore")
295
+
296
+ def _extract_source(self, content: str, filename: str) -> str:
297
+ """๋ฌธ์„œ ๋‚ด์šฉ์—์„œ ์ถœ์ฒ˜ ๊ธฐ๊ด€ ์ถ”์ถœ"""
298
+ source_patterns = [
299
+ "๋ณด๊ฑด๋ณต์ง€๋ถ€", "์งˆ๋ณ‘๊ด€๋ฆฌ์ฒญ", "๋Œ€ํ•œ์˜ํ•™ํšŒ", "๋Œ€ํ•œ์‹ ์žฅํ•™ํšŒ",
300
+ "๋Œ€ํ•œ๋‹น๋‡จ๋ณ‘ํ•™ํšŒ", "๋Œ€ํ•œ์˜๋ฃŒ์‚ฌํšŒ๋ณต์ง€์‚ฌํ˜‘ํšŒ"
301
+ ]
302
+
303
+ for pattern in source_patterns:
304
+ if pattern in content:
305
+ return pattern
306
+
307
+ return "๊ด€๋ จ ๊ธฐ๊ด€"
308
+
309
+ def _create_comprehensive_sample_files(self) -> List[str]:
310
+ """ํฌ๊ด„์ ์ธ ์ƒ˜ํ”Œ ํŒŒ์ผ ์ƒ์„ฑ"""
311
+ sample_files = []
312
+ samples = [
313
+ ("[์‹์ด-CKD-STD-S3] ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘ ํ™˜์ž์˜ ๋‹จ๋ฐฑ์งˆ ์„ญ์ทจ ๊ฐ€์ด๋“œ.txt",
314
+ """# ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘ ํ™˜์ž์˜ ๋‹จ๋ฐฑ์งˆ ์„ญ์ทจ ๊ฐ€์ด๋“œ
315
+
316
+ ## ๊ฐœ์š”
317
+ ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘(CKD) ํ™˜์ž์˜ ์ ์ ˆํ•œ ๋‹จ๋ฐฑ์งˆ ์„ญ์ทจ๋Š” ์งˆ๋ณ‘ ์ง„ํ–‰์„ ๋Šฆ์ถ”๊ณ  ์˜์–‘ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋Š” ๋ฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.
318
+
319
+ ## ๋‹จ๊ณ„๋ณ„ ๋‹จ๋ฐฑ์งˆ ์„ญ์ทจ ๊ถŒ์žฅ๋Ÿ‰
320
+ - CKD 1-2๋‹จ๊ณ„: ์ •์ƒ ์„ญ์ทจ (์ฒด์ค‘ kg๋‹น 0.8-1.0g)
321
+ - CKD 3-4๋‹จ๊ณ„: ์ œํ•œ ํ•„์š” (์ฒด์ค‘ kg๋‹น 0.6-0.8g)
322
+ - CKD 5๋‹จ๊ณ„(ํˆฌ์„ ์ „): ์—„๊ฒฉํ•œ ์ œํ•œ (์ฒด์ค‘ kg๋‹น 0.6g)
323
+ - ํ˜ˆ์•กํˆฌ์„ ํ™˜์ž: ์ฆ๊ฐ€ ํ•„์š” (์ฒด์ค‘ kg๋‹น 1.2g)
324
+ - ๋ณต๋ง‰ํˆฌ์„ ํ™˜์ž: ๋” ์ฆ๊ฐ€ ํ•„์š” (์ฒด์ค‘ kg๋‹น 1.2-1.3g)
325
+
326
+ ## ์–‘์งˆ์˜ ๋‹จ๋ฐฑ์งˆ ์„ ํƒ
327
+ 1. ๋™๋ฌผ์„ฑ ๋‹จ๋ฐฑ์งˆ: ๋‹ฌ๊ฑ€, ์ƒ์„ , ๋‹ญ๊ฐ€์Šด์‚ด
328
+ 2. ์‹๋ฌผ์„ฑ ๋‹จ๋ฐฑ์งˆ: ๋‘๋ถ€, ์ฝฉ๋ฅ˜ (์ธ ํ•จ๋Ÿ‰ ์ฃผ์˜)
329
+
330
+ ## ์ฃผ์˜์‚ฌํ•ญ
331
+ - ๊ณผ๋„ํ•œ ๋‹จ๋ฐฑ์งˆ ์„ญ์ทจ๋Š” ์‹ ์žฅ์— ๋ถ€๋‹ด์„ ์ค๋‹ˆ๋‹ค
332
+ - ๊ฐœ์ธ๋ณ„ ์ƒํƒœ์— ๋”ฐ๋ผ ์„ญ์ทจ๋Ÿ‰ ์กฐ์ ˆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค
333
+ - ์ •๊ธฐ์ ์ธ ์˜์–‘ ์ƒ๋‹ด์„ ๋ฐ›์œผ์„ธ์š”
334
+
335
+ ์ถœ์ฒ˜: ๋Œ€ํ•œ์‹ ์žฅํ•™ํšŒ"""),
336
+
337
+ ("[๋ณต์•ฝ-HD-HTN-S2] ํ˜ˆ์•กํˆฌ์„ ํ™˜์ž์˜ ๊ณ ํ˜ˆ์•• ์•ฝ๋ฌผ ๊ด€๋ฆฌ.txt",
338
+ """# ํ˜ˆ์•กํˆฌ์„ ํ™˜์ž์˜ ๊ณ ํ˜ˆ์•• ์•ฝ๋ฌผ ๊ด€๋ฆฌ
339
+
340
+ ## ์ฃผ์š” ์›์น™
341
+ ํ˜ˆ์•กํˆฌ์„ ํ™˜์ž์˜ ์•ฝ 70-80%๊ฐ€ ๊ณ ํ˜ˆ์••์„ ๋™๋ฐ˜ํ•˜๋ฉฐ, ์ ์ ˆํ•œ ์•ฝ๋ฌผ ๊ด€๋ฆฌ๊ฐ€ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.
342
+
343
+ ## ๋ณต์•ฝ ์‹œ๊ฐ„ ์กฐ์ ˆ
344
+ 1. ํˆฌ์„ ํ›„ ๋ณต์šฉ ๊ถŒ์žฅ ์•ฝ๋ฌผ
345
+ - ACE ์–ต์ œ์ œ, ARB: ํˆฌ์„์œผ๋กœ ์ œ๊ฑฐ๋  ์ˆ˜ ์žˆ์Œ
346
+ - ๋ฒ ํƒ€์ฐจ๋‹จ์ œ: ํˆฌ์„ ์ค‘ ์ €ํ˜ˆ์•• ์œ„ํ—˜
347
+
348
+ 2. ํˆฌ์„๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ๋ณต์šฉ ๊ฐ€๋Šฅํ•œ ์•ฝ๋ฌผ
349
+ - ์นผ์Š˜์ฑ„๋„์ฐจ๋‹จ์ œ: ํˆฌ์„์œผ๋กœ ์ œ๊ฑฐ๋˜์ง€ ์•Š์Œ
350
+
351
+ ## ์•ฝ๋ฌผ ์ƒํ˜ธ์ž‘์šฉ ์ฃผ์˜
352
+ - ์ธ๊ฒฐํ•ฉ์ œ์™€ ๋‹ค๋ฅธ ์•ฝ๋ฌผ์€ ์ตœ์†Œ 2์‹œ๊ฐ„ ๊ฐ„๊ฒฉ
353
+ - ์ฒ ๋ถ„์ œ์™€ ์ผ๋ถ€ ํ•ญ์ƒ์ œ๋Š” ๋™์‹œ ๋ณต์šฉ ๊ธˆ์ง€
354
+
355
+ ## ํ˜ˆ์•• ๋ชฉํ‘œ
356
+ - ํˆฌ์„ ์ „: 140/90 mmHg ๋ฏธ๋งŒ
357
+ - ํˆฌ์„ ํ›„: 130/80 mmHg ๋ฏธ๋งŒ
358
+
359
+ ์ถœ์ฒ˜: ๋Œ€ํ•œ์‹ ์žฅํ•™ํšŒ"""),
360
+
361
+ ("[์‹์ด-HD-STD-S2] ํ˜ˆ์•กํˆฌ์„ ํ™˜์ž์˜ ์นผ๋ฅจ ์ œํ•œ ์‹์ด์š”๋ฒ•.txt",
362
+ """# ํ˜ˆ์•กํˆฌ์„ ํ™˜์ž์˜ ์นผ๋ฅจ ์ œํ•œ ์‹์ด์š”๋ฒ•
363
+
364
+ ## ์นผ๋ฅจ ์ œํ•œ์˜ ์ค‘์š”์„ฑ
365
+ ํ˜ˆ์•กํˆฌ์„ ํ™˜์ž๋Š” ์†Œ๋ณ€๋Ÿ‰ ๊ฐ์†Œ๋กœ ์นผ๋ฅจ ๋ฐฐ์„ค์ด ์–ด๋ ค์›Œ ๊ณ ์นผ๋ฅจํ˜ˆ์ฆ ์œ„ํ—˜์ด ๋†’์Šต๋‹ˆ๋‹ค.
366
+
367
+ ## ์ผ์ผ ์นผ๋ฅจ ์„ญ์ทจ ๊ถŒ์žฅ๋Ÿ‰
368
+ - ํ˜ˆ์•กํˆฌ์„ ํ™˜์ž: 2000-2500mg/์ผ
369
+ - ์ž”์—ฌ ์‹ ๊ธฐ๋Šฅ์— ๋”ฐ๋ผ ์กฐ์ ˆ ํ•„์š”
370
+
371
+ ## ๊ณ ์นผ๋ฅจ ์‹ํ’ˆ (์ œํ•œ ํ•„์š”)
372
+ - ๊ณผ์ผ: ๋ฐ”๋‚˜๋‚˜, ์ฐธ์™ธ, ํ† ๋งˆํ† , ์˜ค๋ Œ์ง€
373
+ - ์ฑ„์†Œ: ์‹œ๊ธˆ์น˜, ๊ฐ์ž, ๊ณ ๊ตฌ๋งˆ, ๋ฒ„์„ฏ
374
+ - ๊ธฐํƒ€: ์ดˆ์ฝœ๋ฆฟ, ๊ฒฌ๊ณผ๋ฅ˜, ์šฐ์œ 
375
+
376
+ ## ์นผ๋ฅจ ๊ฐ์†Œ ์กฐ๋ฆฌ๋ฒ•
377
+ 1. ์ฑ„์†Œ๋Š” ์ž˜๊ฒŒ ์ฐ์–ด ๋ฌผ์— 2์‹œ๊ฐ„ ๋‹ด๊ทผ ํ›„ ํ—น๊ตฌ๊ธฐ
378
+ 2. ๋“๋Š” ๋ฌผ์— ๋ฐ์นœ ํ›„ ๊ตญ๋ฌผ์€ ๋ฒ„๋ฆฌ๊ธฐ
379
+ 3. ๊ณผ์ผ์€ ํ†ต์กฐ๋ฆผ ์‚ฌ์šฉ (์‹œ๋Ÿฝ ์ œ๊ฑฐ)
380
+
381
+ ์ถœ์ฒ˜: ๋ณด๊ฑด๋ณต์ง€๋ถ€"""),
382
+
383
+ ("[์ƒํ™œ-CKD-STD-S3] ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘ ํ™˜์ž์˜ ์ˆ˜๋ถ„ ์„ญ์ทจ ๊ด€๋ฆฌ.txt",
384
+ """# ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘ ํ™˜์ž์˜ ์ˆ˜๋ถ„ ์„ญ์ทจ ๊ด€๋ฆฌ
385
+
386
+ ## ์ˆ˜๋ถ„ ์ œํ•œ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
387
+ - ์†Œ๋ณ€๋Ÿ‰์ด ํ•˜๋ฃจ 500ml ์ดํ•˜๋กœ ๊ฐ์†Œ
388
+ - ๋ถ€์ข…์ด ์žˆ๋Š” ๊ฒฝ์šฐ
389
+ - ์‹ฌ๋ถ€์ „์„ ๋™๋ฐ˜ํ•œ ๊ฒฝ์šฐ
390
+
391
+ ## ์ผ์ผ ์ˆ˜๋ถ„ ์„ญ์ทจ๋Ÿ‰ ๊ณ„์‚ฐ
392
+ - ๊ธฐ๋ณธ ๊ณต์‹: ์ „๋‚  ์†Œ๋ณ€๋Ÿ‰ + 500ml
393
+ - ํˆฌ์„ ํ™˜์ž: ํˆฌ์„ ๊ฐ„ ์ฒด์ค‘ ์ฆ๊ฐ€ 1kg ์ด๋‚ด
394
+
395
+ ## ์ˆ˜๋ถ„ ์„ญ์ทจ ๊ด€๋ฆฌ ์š”๋ น
396
+ 1. ๋ชจ๋“  ์•ก์ฒด๋ฅ˜ ํฌํ•จ (๊ตญ, ์šฐ์œ , ์•„์ด์Šคํฌ๋ฆผ ๋“ฑ)
397
+ 2. ์ž‘์€ ์ปต ์‚ฌ์šฉํ•˜๊ธฐ
398
+ 3. ์–ผ์Œ ์กฐ๊ฐ์œผ๋กœ ๊ฐˆ์ฆ ํ•ด์†Œ
399
+ 4. ๋ฌด์„คํƒ• ๊ปŒ์ด๋‚˜ ์‹  ์‚ฌํƒ• ํ™œ์šฉ
400
+
401
+ ## ์ฃผ์˜์‚ฌํ•ญ
402
+ - ๊ณผ๋„ํ•œ ์ˆ˜๋ถ„ ์ œํ•œ๋„ ์œ„ํ—˜
403
+ - ๊ฐœ์ธ๋ณ„ ์ƒํƒœ์— ๋”ฐ๋ผ ์กฐ์ ˆ
404
+ - ์ •๊ธฐ์ ์ธ ์ฒด์ค‘ ์ธก์ • ํ•„์š”
405
+
406
+ ์ถœ์ฒ˜: ๋Œ€ํ•œ์˜ํ•™ํšŒ"""),
407
+
408
+ ("[์šด๋™-CKD-STD-S3] ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘ ํ™˜์ž์˜ ์šด๋™ ๊ฐ€์ด๋“œ.txt",
409
+ """# ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘ ํ™˜์ž์˜ ์šด๋™ ๊ฐ€์ด๋“œ
410
+
411
+ ## ์šด๋™์˜ ์ด์ 
412
+ - ์‹ฌํ˜ˆ๊ด€ ๊ธฐ๋Šฅ ๊ฐœ์„ 
413
+ - ํ˜ˆ์•• ์กฐ์ ˆ
414
+ - ๊ทผ๋ ฅ ์œ ์ง€
415
+ - ์šฐ์šธ๊ฐ ๊ฐ์†Œ
416
+
417
+ ## ๊ถŒ์žฅ ์šด๋™
418
+ 1. ์œ ์‚ฐ์†Œ ์šด๋™
419
+ - ๊ฑท๊ธฐ: ์ฃผ 5ํšŒ, 30๋ถ„
420
+ - ์ž์ „๊ฑฐ: ์ €๊ฐ•๋„๋กœ ์‹œ์ž‘
421
+ - ์ˆ˜์˜: ๊ด€์ ˆ์— ๋ฌด๋ฆฌ ์—†์Œ
422
+
423
+ 2. ๊ทผ๋ ฅ ์šด๋™
424
+ - ๊ฐ€๋ฒผ์šด ๋ค๋ฒจ ์šด๋™
425
+ - ์ €ํ•ญ ๋ฐด๋“œ ์šด๋™
426
+ - ์ฃผ 2-3ํšŒ, 15-20๋ถ„
427
+
428
+ ## ์šด๋™ ์‹œ ์ฃผ์˜์‚ฌํ•ญ
429
+ - ํˆฌ์„ ์งํ›„๋Š” ํ”ผํ•˜๊ธฐ
430
+ - ํƒˆ์ˆ˜ ์ฃผ์˜
431
+ - ๊ฐ€์Šด ํ†ต์ฆ, ํ˜ธํก๊ณค๋ž€ ์‹œ ์ฆ‰์‹œ ์ค‘๋‹จ
432
+ - ์šด๋™ ์ „ํ›„ ํ˜ˆ์•• ์ฒดํฌ
433
+
434
+ ์ถœ์ฒ˜: ๋Œ€ํ•œ์˜๋ฃŒ์‚ฌํšŒ๋ณต์ง€์‚ฌํ˜‘ํšŒ"""),
435
+
436
+ ("[์ง„๋‹จ-CKD-STD-S3] ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘์˜ ์ง„๋‹จ๊ณผ ๊ฒ€์‚ฌ.txt",
437
+ """# ๋งŒ์„ฑ์ฝฉํŒฅ๋ณ‘์˜ ์ง„๋‹จ๊ณผ ๊ฒ€์‚ฌ
438
+
439
+ ## ์ง„๋‹จ ๊ธฐ์ค€
440
+ 3๊ฐœ์›” ์ด์ƒ ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜ ์ด์ƒ ์กด์žฌ ์‹œ:
441
+ - eGFR < 60 ml/min/1.73mยฒ
442
+ - ์•Œ๋ถ€๋ฏผ๋‡จ (ACR โ‰ฅ 30mg/g)
443
+ - ์‹ ์žฅ ์†์ƒ์˜ ์ฆ๊ฑฐ
444
+
445
+ ## ์ฃผ์š” ๊ฒ€์‚ฌ
446
+ 1. ํ˜ˆ์•ก๊ฒ€์‚ฌ
447
+ - ํฌ๋ ˆ์•„ํ‹ฐ๋‹Œ, eGFR
448
+ - ์ „ํ•ด์งˆ (Na, K, Ca, P)
449
+ - ๋นˆํ˜ˆ ์ง€ํ‘œ (Hb, ferritin)
450
+
451
+ 2. ์†Œ๋ณ€๊ฒ€์‚ฌ
452
+ - ๋‹จ๋ฐฑ๋‡จ/์•Œ๋ถ€๋ฏผ๋‡จ
453
+ - ํ˜„๋ฏธ๊ฒฝ ๊ฒ€์‚ฌ
454
+
455
+ 3. ์˜์ƒ๊ฒ€์‚ฌ
456
+ - ์‹ ์žฅ ์ดˆ์ŒํŒŒ
457
+ - ํ•„์š”์‹œ CT, MRI
458
+
459
+ ## ์ •๊ธฐ ๊ฒ€์ง„ ์ฃผ๊ธฐ
460
+ - CKD 1-2๋‹จ๊ณ„: ์—ฐ 1ํšŒ
461
+ - CKD 3๋‹จ๊ณ„: 6๊ฐœ์›”๋งˆ๋‹ค
462
+ - CKD 4-5๋‹จ๊ณ„: 3๊ฐœ์›”๋งˆ๋‹ค
463
+
464
+ ์ถœ์ฒ˜: ์งˆ๋ณ‘๊ด€๋ฆฌ์ฒญ"""),
465
+
466
+ ("[์‹์ด-CKD-DM-S2] ๋‹น๋‡จ๋ณ‘์„ฑ ์‹ ์ฆ ํ™˜์ž์˜ ์‹์‚ฌ ๊ด€๋ฆฌ.txt",
467
+ """# ๋‹น๋‡จ๋ณ‘์„ฑ ์‹ ์ฆ ํ™˜์ž์˜ ์‹์‚ฌ ๊ด€๋ฆฌ
468
+
469
+ ## ํŠน๋ณ„ ๊ณ ๋ ค์‚ฌํ•ญ
470
+ ๋‹น๋‡จ๋ณ‘๊ณผ ์‹ ์žฅ๋ณ‘์„ ํ•จ๊ป˜ ๊ด€๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๋ณต์žกํ•œ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.
471
+
472
+ ## ์˜์–‘ ๊ด€๋ฆฌ ์›์น™
473
+ 1. ํ˜ˆ๋‹น ์กฐ์ ˆ
474
+ - ๊ทœ์น™์ ์ธ ์‹์‚ฌ ์‹œ๊ฐ„
475
+ - ๋‹น์ง€์ˆ˜๊ฐ€ ๋‚ฎ์€ ์‹ํ’ˆ ์„ ํƒ
476
+ - ๋‹จ์ˆœ๋‹น ์ œํ•œ
477
+
478
+ 2. ๋‹จ๋ฐฑ์งˆ ์กฐ์ ˆ
479
+ - CKD 3-4๋‹จ๊ณ„: 0.6-0.8g/kg/์ผ
480
+ - ์–‘์งˆ์˜ ๋‹จ๋ฐฑ์งˆ ์œ„์ฃผ
481
+
482
+ 3. ๋‚˜ํŠธ๋ฅจ ์ œํ•œ
483
+ - 2000mg/์ผ ์ดํ•˜
484
+ - ๊ฐ€๊ณต์‹ํ’ˆ ํ”ผํ•˜๊ธฐ
485
+
486
+ ## ์ฃผ์˜ ์‹ํ’ˆ
487
+ - ๊ณผ์ผ: ๋‹น๋„ ๋†’์€ ๊ณผ์ผ ์ œํ•œ
488
+ - ๊ณก๋ฅ˜: ํ˜„๋ฏธ, ์žก๊ณก (์ธ ํ•จ๋Ÿ‰ ์ฃผ์˜)
489
+ - ์Œ๋ฃŒ: ๊ณผ์ผ์ฃผ์Šค, ์Šคํฌ์ธ ์Œ๋ฃŒ ๊ธˆ์ง€
490
+
491
+ ์ถœ์ฒ˜: ๋Œ€ํ•œ๋‹น๋‡จ๋ณ‘ํ•™ํšŒ""")
492
+ ]
493
+
494
+ for filename, content in samples:
495
+ filepath = os.path.join(self.documents_path, filename)
496
+ with open(filepath, 'w', encoding='utf-8') as f:
497
+ f.write(content)
498
+ sample_files.append(filepath)
499
+ logger.info(f"Created sample file: {filename}")
500
+
501
+ return sample_files
502
+
503
+ def search(self, query: str, k: int = 5,
504
+ filters: Optional[Dict[str, Any]] = None) -> List[Document]:
505
+ """๊ด€๋ จ ๋ฌธ์„œ ๊ฒ€์ƒ‰"""
506
+ if not self.vectorstore:
507
+ logger.warning("Vectorstore not loaded, loading now...")
508
+ self.load_documents()
509
+
510
+ logger.info(f"Searching for: '{query}' with k={k}, filters={filters}")
511
+ results = self.vectorstore.similarity_search(query, k=k*2)
512
+
513
+ if filters:
514
+ filtered_results = []
515
+ for doc in results:
516
+ match = True
517
+ for key, value in filters.items():
518
+ if key in doc.metadata and doc.metadata[key] != value:
519
+ match = False
520
+ break
521
+ if match:
522
+ filtered_results.append(doc)
523
+ results = filtered_results[:k]
524
+ else:
525
+ results = results[:k]
526
+
527
+ logger.info(f"Found {len(results)} documents")
528
+ return results
529
+
530
+ def search_by_patient_context(self, query: str,
531
+ constraints: PatientConstraints,
532
+ task_type: TaskType,
533
+ k: int = 5) -> List[Document]:
534
+ """ํ™˜์ž ์ƒํƒœ์™€ ์ž‘์—… ์œ ํ˜•์„ ๊ณ ๋ คํ•œ ๋งž์ถคํ˜• ๊ฒ€์ƒ‰"""
535
+ filters = {}
536
+
537
+ # ์ž‘์—… ์œ ํ˜•์— ๋”ฐ๋ฅธ ํ•„ํ„ฐ
538
+ task_field_mapping = {
539
+ TaskType.DIET_RECOMMENDATION: "diet",
540
+ TaskType.DIET_ANALYSIS: "diet",
541
+ TaskType.MEDICATION: "medication",
542
+ TaskType.LIFESTYLE: "lifestyle",
543
+ TaskType.DIAGNOSIS: "diagnosis",
544
+ TaskType.EXERCISE: "exercise"
545
+ }
546
+
547
+ if task_type in task_field_mapping:
548
+ filters["field"] = task_field_mapping[task_type]
549
+
550
+ # ํ™˜์ž ์ƒํƒœ์— ๋”ฐ๋ฅธ ํ•„ํ„ฐ
551
+ if constraints.on_dialysis:
552
+ filters["patient_status"] = "hemodialysis"
553
+ elif constraints.disease_stage in [DiseaseStage.CKD_3, DiseaseStage.CKD_4]:
554
+ filters["patient_status"] = "chronic_kidney_disease"
555
+
556
+ # ๋™๋ฐ˜์งˆํ™˜์— ๋”ฐ๋ฅธ ๊ฒ€์ƒ‰
557
+ additional_results = []
558
+ if "๋‹น๋‡จ" in constraints.comorbidities:
559
+ additional_results.extend(
560
+ self.search(query, k=k//3, filters={"personalization_level": "diabetes"})
561
+ )
562
+ if "๊ณ ํ˜ˆ์••" in constraints.comorbidities:
563
+ additional_results.extend(
564
+ self.search(query, k=k//3, filters={"personalization_level": "hypertension"})
565
+ )
566
+
567
+ main_results = self.search(query, k=k-len(additional_results), filters=filters)
568
+
569
+ all_results = main_results + additional_results
570
+ logger.info(f"Patient context search found {len(all_results)} total documents")
571
+
572
+ return all_results
573
+
574
+ # ========== ์‹ํ’ˆ ์˜์–‘ ๋ถ„์„ ==========
575
+
576
+ class FoodNutritionDatabase:
577
+ """์‹ํ’ˆ ์˜์–‘ ์„ฑ๋ถ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค - ์‹ฑ๊ธ€ํ†ค ํŒจํ„ด ์ ์šฉ"""
578
+
579
+ _instance = None
580
+ _initialized = False
581
+
582
+ def __new__(cls, *args, **kwargs):
583
+ if cls._instance is None:
584
+ cls._instance = super(FoodNutritionDatabase, cls).__new__(cls)
585
+ return cls._instance
586
+
587
+ def __init__(self, csv_path: str = "ํ†ตํ•ฉ์‹ํ’ˆ์˜์–‘์„ฑ๋ถ„์ •๋ณด(์Œ์‹)_20241224.csv"):
588
+ if FoodNutritionDatabase._initialized:
589
+ return
590
+
591
+ self.csv_path = csv_path
592
+ self.food_data = None
593
+ self.load_food_data()
594
+ FoodNutritionDatabase._initialized = True
595
+
596
+ def load_food_data(self):
597
+ """CSV ํŒŒ์ผ์—์„œ ์‹ํ’ˆ ๋ฐ์ดํ„ฐ ๋กœ๋“œ"""
598
+ try:
599
+ # CSV ํŒŒ์ผ ๋กœ๋“œ ์‹œ๋„
600
+ if os.path.exists(self.csv_path):
601
+ self.food_data = pd.read_csv(self.csv_path, encoding='utf-8')
602
+ logger.info(f"Loaded food data from {self.csv_path}")
603
+ else:
604
+ raise FileNotFoundError(f"CSV file not found: {self.csv_path}")
605
+
606
+ # ์ปฌ๋Ÿผ๋ช… ์ •๋ฆฌ (์‹ค์ œ CSV ๊ตฌ์กฐ์— ๋งž๊ฒŒ)
607
+ column_mapping = {
608
+ '์‹ํ’ˆ์ฝ”๋“œ': 'food_code',
609
+ '์‹ํ’ˆ๋ช…': 'name',
610
+ '์‹ํ’ˆ๋Œ€๋ถ„๋ฅ˜๋ช…': 'category_major',
611
+ '์‹ํ’ˆ์ค‘๋ถ„๋ฅ˜๋ช…': 'category_minor',
612
+ '์˜์–‘์„ฑ๋ถ„ํ•จ๋Ÿ‰๊ธฐ์ค€๋Ÿ‰': 'serving_size',
613
+ '์—๋„ˆ์ง€(kcal)': 'calories',
614
+ '์ˆ˜๋ถ„(g)': 'water',
615
+ '๋‹จ๋ฐฑ์งˆ(g)': 'protein',
616
+ '์ง€๋ฐฉ(g)': 'fat',
617
+ 'ํƒ„์ˆ˜ํ™”๋ฌผ(g)': 'carbohydrate',
618
+ '๋‹น๋ฅ˜(g)': 'sugar',
619
+ '์‹์ด์„ฌ์œ (g)': 'dietary_fiber',
620
+ '์นผ์Š˜(mg)': 'calcium',
621
+ '์ฒ (mg)': 'iron',
622
+ '์ธ(mg)': 'phosphorus',
623
+ '์นผ๋ฅจ(mg)': 'potassium',
624
+ '๋‚˜ํŠธ๋ฅจ(mg)': 'sodium',
625
+ '์ฝœ๋ ˆ์Šคํ…Œ๋กค(mg)': 'cholesterol',
626
+ 'ํฌํ™”์ง€๋ฐฉ์‚ฐ(g)': 'saturated_fat'
627
+ }
628
+
629
+ self.food_data = self.food_data.rename(columns=column_mapping)
630
+
631
+ # ์ˆซ์žํ˜• ์ปฌ๋Ÿผ ๋ณ€ํ™˜
632
+ numeric_columns = ['calories', 'protein', 'fat', 'carbohydrate',
633
+ 'sodium', 'potassium', 'phosphorus', 'calcium',
634
+ 'water', 'sugar', 'dietary_fiber', 'iron',
635
+ 'cholesterol', 'saturated_fat']
636
+ for col in numeric_columns:
637
+ if col in self.food_data.columns:
638
+ self.food_data[col] = pd.to_numeric(self.food_data[col], errors='coerce')
639
+
640
+ # serving_size๋ฅผ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ (์˜ˆ: "100g" -> 100)
641
+ if 'serving_size' in self.food_data.columns:
642
+ if self.food_data['serving_size'].dtype == 'object':
643
+ self.food_data['serving_size'] = self.food_data['serving_size'].str.extract('(\d+)').astype(float)
644
+ else:
645
+ self.food_data['serving_size'] = pd.to_numeric(self.food_data['serving_size'], errors='coerce')
646
+
647
+ # NaN ๊ฐ’์„ 0์œผ๋กœ ์ฑ„์šฐ๊ธฐ
648
+ self.food_data = self.food_data.fillna(0)
649
+
650
+ logger.info(f"Loaded {len(self.food_data)} food items from database")
651
+
652
+ except Exception as e:
653
+ logger.error(f"Error loading food database: {e}")
654
+ logger.info("Creating sample food data...")
655
+ self.food_data = self._create_sample_data()
656
+
657
+ def _create_sample_data(self):
658
+ """์ƒ˜ํ”Œ ์‹ํ’ˆ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ"""
659
+ sample_data = {
660
+ 'food_code': ['D101-001', 'D101-002', 'D101-003', 'D101-004', 'D101-005', 'D101-006',
661
+ 'D101-007', 'D101-008', 'D101-009', 'D101-010'],
662
+ 'name': ['์Œ€๋ฐฅ', '๋‹ญ๊ฐ€์Šด์‚ด', '๋ธŒ๋กœ์ฝœ๋ฆฌ', '์‚ฌ๊ณผ', '๋‘๋ถ€', '๋‹ฌ๊ฑ€', '๊ฐ์ž', '์šฐ์œ ', '์—ฐ์–ด', '์‹œ๊ธˆ์น˜'],
663
+ 'category_major': ['๊ณก๋ฅ˜', '์œก๋ฅ˜', '์ฑ„์†Œ๋ฅ˜', '๊ณผ์ผ๋ฅ˜', '์ฝฉ๋ฅ˜', '๋‚œ๋ฅ˜', '์„œ๋ฅ˜', '์œ ์ œํ’ˆ๋ฅ˜', '์–ดํŒจ๋ฅ˜', '์ฑ„์†Œ๋ฅ˜'],
664
+ 'category_minor': ['๋ฐฅ๋ฅ˜', '๊ฐ€๊ธˆ๋ฅ˜', '๋…นํ™ฉ์ƒ‰์ฑ„์†Œ', '๊ณผ์ผ', '๋‘๋ถ€', '๊ณ„๋ž€', '๊ฐ์ž๋ฅ˜', '์šฐ์œ ๋ฅ˜', '์ƒ์„ ๋ฅ˜', '์—ฝ์ฑ„๋ฅ˜'],
665
+ 'serving_size': [100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
666
+ 'calories': [130, 165, 34, 52, 76, 155, 77, 61, 208, 23],
667
+ 'water': [68.5, 65.3, 89.3, 85.6, 84.6, 76.2, 79.3, 87.7, 68.5, 91.4],
668
+ 'protein': [2.7, 31.0, 2.8, 0.3, 8.1, 13.0, 2.0, 3.3, 20.4, 2.9],
669
+ 'fat': [0.3, 3.6, 0.4, 0.2, 4.8, 11.0, 0.1, 3.3, 13.4, 0.4],
670
+ 'carbohydrate': [28.2, 0, 6.6, 13.8, 1.9, 1.1, 17.6, 4.8, 0, 3.6],
671
+ 'sugar': [0.1, 0, 1.7, 10.4, 0.7, 0.4, 0.8, 5.0, 0, 0.4],
672
+ 'dietary_fiber': [0.4, 0, 2.6, 2.4, 0.3, 0, 1.8, 0, 0, 2.2],
673
+ 'calcium': [10, 11, 47, 6, 350, 56, 10, 113, 12, 99],
674
+ 'iron': [0.5, 0.9, 0.7, 0.1, 5.4, 1.8, 0.8, 0.1, 0.8, 2.7],
675
+ 'phosphorus': [43, 210, 66, 11, 110, 198, 57, 93, 252, 49],
676
+ 'potassium': [35, 256, 316, 107, 121, 138, 421, 150, 490, 558],
677
+ 'sodium': [1, 74, 30, 1, 7, 142, 6, 50, 44, 79],
678
+ 'cholesterol': [0, 85, 0, 0, 0, 373, 0, 12, 55, 0],
679
+ 'saturated_fat': [0.1, 1.0, 0.1, 0, 0.7, 3.3, 0, 1.9, 3.1, 0.1]
680
+ }
681
+
682
+ return pd.DataFrame(sample_data)
683
+
684
+ def search_foods(self, query: str, limit: int = 10) -> List[FoodItem]:
685
+ """์‹ํ’ˆ ๊ฒ€์ƒ‰"""
686
+ logger.info(f"Searching for food: '{query}'")
687
+
688
+ # ๊ฒ€์ƒ‰์–ด๊ฐ€ ํฌํ•จ๋œ ์‹ํ’ˆ ์ฐพ๊ธฐ
689
+ mask = self.food_data['name'].str.contains(query, case=False, na=False)
690
+ results = self.food_data[mask].head(limit)
691
+
692
+ food_items = []
693
+ for _, row in results.iterrows():
694
+ food_item = FoodItem(
695
+ food_code=str(row.get('food_code', '')),
696
+ name=row['name'],
697
+ food_category_major=row.get('category_major', ''),
698
+ food_category_minor=row.get('category_minor', ''),
699
+ serving_size=float(row.get('serving_size', 100)),
700
+ calories=float(row['calories']),
701
+ water=float(row.get('water', 0)),
702
+ protein=float(row['protein']),
703
+ fat=float(row['fat']),
704
+ carbohydrate=float(row['carbohydrate']),
705
+ sugar=float(row.get('sugar', 0)),
706
+ dietary_fiber=float(row.get('dietary_fiber', 0)),
707
+ calcium=float(row.get('calcium', 0)),
708
+ iron=float(row.get('iron', 0)),
709
+ phosphorus=float(row['phosphorus']),
710
+ potassium=float(row['potassium']),
711
+ sodium=float(row['sodium']),
712
+ cholesterol=float(row.get('cholesterol', 0)),
713
+ saturated_fat=float(row.get('saturated_fat', 0))
714
+ )
715
+ food_items.append(food_item)
716
+
717
+ logger.info(f"Found {len(food_items)} food items for '{query}'")
718
+ return food_items
719
+
720
+ def recommend_foods_for_patient(self, constraints: PatientConstraints,
721
+ meal_type: str = "all",
722
+ limit: int = 20) -> List[FoodItem]:
723
+ """ํ™˜์ž ์ œ์•ฝ์กฐ๊ฑด์— ๋งž๋Š” ์‹ํ’ˆ ์ถ”์ฒœ"""
724
+ logger.info(f"Recommending foods for patient with constraints, meal_type={meal_type}")
725
+
726
+ # ํ•„ํ„ฐ๋ง ์กฐ๊ฑด ์„ค์ •
727
+ filtered_data = self.food_data.copy()
728
+
729
+ # ๋‹จ๋ฐฑ์งˆ ์ œํ•œ (ํ•œ ๋ผ ๊ธฐ์ค€ = ์ผ์ผ ์ œํ•œ๋Ÿ‰์˜ 30%)
730
+ if constraints.protein_restriction:
731
+ max_protein = constraints.protein_restriction * 0.3
732
+ filtered_data = filtered_data[filtered_data['protein'] <= max_protein]
733
+
734
+ # ๋‚˜ํŠธ๋ฅจ ์ œํ•œ
735
+ if constraints.sodium_restriction:
736
+ max_sodium = constraints.sodium_restriction * 0.3
737
+ filtered_data = filtered_data[filtered_data['sodium'] <= max_sodium]
738
+
739
+ # ์นผ๋ฅจ ์ œํ•œ
740
+ if constraints.potassium_restriction:
741
+ max_potassium = constraints.potassium_restriction * 0.3
742
+ filtered_data = filtered_data[filtered_data['potassium'] <= max_potassium]
743
+
744
+ # ์ธ ์ œํ•œ
745
+ if constraints.phosphorus_restriction:
746
+ max_phosphorus = constraints.phosphorus_restriction * 0.3
747
+ filtered_data = filtered_data[filtered_data['phosphorus'] <= max_phosphorus]
748
+
749
+ # ์‹์‚ฌ ์œ ํ˜•์— ๋”ฐ๋ฅธ ํ•„ํ„ฐ๋ง
750
+ if meal_type == "breakfast":
751
+ # ์•„์นจ์‹์‚ฌ์— ์ ํ•ฉํ•œ ์นดํ…Œ๊ณ ๋ฆฌ
752
+ breakfast_categories = ['๊ณก๋ฅ˜', '์œ ์ œํ’ˆ๋ฅ˜', '๊ณผ์ผ๋ฅ˜', '๋‚œ๋ฅ˜']
753
+ mask = filtered_data['category_major'].isin(breakfast_categories)
754
+ if mask.any():
755
+ filtered_data = filtered_data[mask]
756
+ elif meal_type == "lunch" or meal_type == "dinner":
757
+ # ์ ์‹ฌ/์ €๋…์— ์ ํ•ฉํ•œ ์นดํ…Œ๊ณ ๋ฆฌ
758
+ main_categories = ['๊ณก๋ฅ˜', '์œก๋ฅ˜', '์–ดํŒจ๋ฅ˜', '์ฑ„์†Œ๋ฅ˜', '์ฝฉ๋ฅ˜']
759
+ mask = filtered_data['category_major'].isin(main_categories)
760
+ if mask.any():
761
+ filtered_data = filtered_data[mask]
762
+
763
+ # ์นผ๋กœ๋ฆฌ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌ (์ ์ ˆํ•œ ์นผ๋กœ๋ฆฌ ๋ฒ”์œ„ ์šฐ์„ )
764
+ if constraints.calorie_target:
765
+ target_cal_per_meal = constraints.calorie_target / 3
766
+ filtered_data['cal_diff'] = abs(filtered_data['calories'] - target_cal_per_meal * 0.5)
767
+ filtered_data = filtered_data.sort_values('cal_diff')
768
+
769
+ # ์ƒ์œ„ N๊ฐœ ์„ ํƒ
770
+ top_foods = filtered_data.head(limit)
771
+
772
+ # FoodItem ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜
773
+ recommended_foods = []
774
+ for _, row in top_foods.iterrows():
775
+ food_item = FoodItem(
776
+ food_code=str(row.get('food_code', '')),
777
+ name=row['name'],
778
+ food_category_major=row.get('category_major', ''),
779
+ food_category_minor=row.get('category_minor', ''),
780
+ serving_size=float(row.get('serving_size', 100)),
781
+ calories=float(row['calories']),
782
+ water=float(row.get('water', 0)),
783
+ protein=float(row['protein']),
784
+ fat=float(row['fat']),
785
+ carbohydrate=float(row['carbohydrate']),
786
+ sugar=float(row.get('sugar', 0)),
787
+ dietary_fiber=float(row.get('dietary_fiber', 0)),
788
+ calcium=float(row.get('calcium', 0)),
789
+ iron=float(row.get('iron', 0)),
790
+ phosphorus=float(row['phosphorus']),
791
+ potassium=float(row['potassium']),
792
+ sodium=float(row['sodium']),
793
+ cholesterol=float(row.get('cholesterol', 0)),
794
+ saturated_fat=float(row.get('saturated_fat', 0))
795
+ )
796
+ recommended_foods.append(food_item)
797
+
798
+ logger.info(f"Recommended {len(recommended_foods)} foods for {meal_type}")
799
+ return recommended_foods
800
+
801
+ def create_daily_meal_plan(self, constraints: PatientConstraints) -> Dict[str, List[FoodItem]]:
802
+ """ํ•˜๋ฃจ ์‹๋‹จ ๊ณ„ํš ์ƒ์„ฑ"""
803
+ logger.info("Creating daily meal plan")
804
+
805
+ meal_plan = {
806
+ 'breakfast': [],
807
+ 'lunch': [],
808
+ 'dinner': [],
809
+ 'snack': []
810
+ }
811
+
812
+ # ๊ฐ ์‹์‚ฌ๋ณ„ ์ถ”์ฒœ ์‹ํ’ˆ
813
+ meal_plan['breakfast'] = self.recommend_foods_for_patient(
814
+ constraints, meal_type='breakfast', limit=5
815
+ )
816
+ meal_plan['lunch'] = self.recommend_foods_for_patient(
817
+ constraints, meal_type='lunch', limit=5
818
+ )
819
+ meal_plan['dinner'] = self.recommend_foods_for_patient(
820
+ constraints, meal_type='dinner', limit=5
821
+ )
822
+
823
+ # ๊ฐ„์‹ ์ถ”์ฒœ (์นผ๋กœ๋ฆฌ๊ฐ€ ๋‚ฎ์€ ์‹ํ’ˆ)
824
+ snack_data = self.food_data[self.food_data['calories'] < 100]
825
+ if constraints.protein_restriction:
826
+ snack_data = snack_data[snack_data['protein'] < constraints.protein_restriction * 0.1]
827
+
828
+ snack_foods = []
829
+ for _, row in snack_data.head(3).iterrows():
830
+ food_item = FoodItem(
831
+ food_code=str(row.get('food_code', '')),
832
+ name=row['name'],
833
+ food_category_major=row.get('category_major', ''),
834
+ food_category_minor=row.get('category_minor', ''),
835
+ serving_size=float(row.get('serving_size', 100)),
836
+ calories=float(row['calories']),
837
+ water=float(row.get('water', 0)),
838
+ protein=float(row['protein']),
839
+ fat=float(row['fat']),
840
+ carbohydrate=float(row['carbohydrate']),
841
+ sugar=float(row.get('sugar', 0)),
842
+ dietary_fiber=float(row.get('dietary_fiber', 0)),
843
+ calcium=float(row.get('calcium', 0)),
844
+ iron=float(row.get('iron', 0)),
845
+ phosphorus=float(row['phosphorus']),
846
+ potassium=float(row['potassium']),
847
+ sodium=float(row['sodium']),
848
+ cholesterol=float(row.get('cholesterol', 0)),
849
+ saturated_fat=float(row.get('saturated_fat', 0))
850
+ )
851
+ snack_foods.append(food_item)
852
+
853
+ meal_plan['snack'] = snack_foods
854
+
855
+ logger.info("Daily meal plan created successfully")
856
+ return meal_plan
857
+
858
+ # ========== LLM ์‘๋‹ต ์ƒ์„ฑ ==========
859
+
860
+ class DraftGenerator:
861
+ """์ดˆ์•ˆ ์‘๋‹ต ์ƒ์„ฑ๊ธฐ"""
862
+
863
+ def __init__(self):
864
+ self.llm = ChatOpenAI(temperature=0.7, model="gpt-4o")
865
+
866
+ def generate_draft(self, query: str, constraints: PatientConstraints,
867
+ context_docs: List[Document]) -> Tuple[str, List[RecommendationItem]]:
868
+ """์ œ์•ฝ์กฐ๊ฑด์„ ๊ณ ๋ คํ•œ ์ดˆ์•ˆ ์ƒ์„ฑ"""
869
+ logger.info("Generating draft response")
870
+
871
+ context = "\n".join([doc.page_content for doc in context_docs])
872
+
873
+ constraints_text = f"""
874
+ ํ™˜์ž ์ •๋ณด:
875
+ - eGFR: {constraints.egfr} ml/min
876
+ - ์งˆ๋ณ‘ ๋‹จ๊ณ„: {constraints.disease_stage.value}
877
+ - ํˆฌ์„ ์—ฌ๋ถ€: {'์˜ˆ' if constraints.on_dialysis else '์•„๋‹ˆ์˜ค'}
878
+ - ๋™๋ฐ˜์งˆํ™˜: {', '.join(constraints.comorbidities) if constraints.comorbidities else '์—†์Œ'}
879
+ - ๋ณต์šฉ ์•ฝ๋ฌผ: {', '.join(constraints.medications) if constraints.medications else '์—†์Œ'}
880
+ - ์—ฐ๋ น: {constraints.age}์„ธ
881
+ - ์„ฑ๋ณ„: {constraints.gender}
882
+
883
+ ์˜์–‘ ์ œํ•œ์‚ฌํ•ญ:
884
+ - ๋‹จ๋ฐฑ์งˆ: {constraints.protein_restriction}g/์ผ
885
+ - ๋‚˜ํŠธ๋ฅจ: {constraints.sodium_restriction}mg/์ผ
886
+ - ์นผ๋ฅจ: {constraints.potassium_restriction}mg/์ผ
887
+ - ์ธ: {constraints.phosphorus_restriction}mg/์ผ
888
+ - ์ˆ˜๋ถ„: {constraints.fluid_restriction}ml/์ผ
889
+ """
890
+
891
+ prompt = f"""
892
+ ๋‹ค์Œ ์‹ ์žฅ์งˆํ™˜ ํ™˜์ž์˜ ์งˆ๋ฌธ์— ๋Œ€ํ•ด ๋‹ต๋ณ€ํ•˜์„ธ์š”.
893
+
894
+ ์งˆ๋ฌธ: {query}
895
+
896
+ ์ฐธ๊ณ  ๋ฌธ์„œ:
897
+ {context}
898
+
899
+ {constraints_text}
900
+
901
+ ๋‹ต๋ณ€ ์‹œ ๋‹ค์Œ ์‚ฌํ•ญ์„ ์ค€์ˆ˜ํ•˜์„ธ์š”:
902
+ 1. ํ™˜์ž์˜ ๊ฐœ๋ณ„ ์ƒํƒœ๋ฅผ ๋ฐ˜์˜ํ•œ ๋งž์ถคํ˜• ๋‹ต๋ณ€ ์ œ๊ณต
903
+ 2. ๊ตฌ์ฒด์ ์ธ ๊ถŒ์žฅ์‚ฌํ•ญ์€ <item>ํƒœ๊ทธ</item>๋กœ ํ‘œ์‹œ
904
+ 3. ์˜ํ•™์ ์œผ๋กœ ์ •ํ™•ํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฌ์šด ์„ค๋ช… ์ œ๊ณต
905
+ 4. ์ฐธ๊ณ  ๋ฌธ์„œ์˜ ๋‚ด์šฉ์„ ํ™œ์šฉํ•˜์—ฌ ๊ทผ๊ฑฐ ์žˆ๋Š” ๋‹ต๋ณ€ ์ž‘์„ฑ
906
+ """
907
+
908
+ response = self.llm.predict(prompt)
909
+ items = self._extract_items(response)
910
+
911
+ logger.info(f"Generated draft with {len(items)} recommendation items")
912
+ return response, items
913
+
914
+ def _extract_items(self, response: str) -> List[RecommendationItem]:
915
+ """์‘๋‹ต์—์„œ ์ถ”์ฒœ ํ•ญ๋ชฉ ์ถ”์ถœ"""
916
+ items = []
917
+ pattern = r'<item>(.*?)</item>'
918
+ matches = re.findall(pattern, response, re.DOTALL)
919
+
920
+ for match in matches:
921
+ category = "์‹์ด" if any(word in match for word in ["์„ญ์ทจ", "์‹์‚ฌ", "์Œ์‹"]) else "๊ธฐํƒ€"
922
+
923
+ item = RecommendationItem(
924
+ name=match.strip(),
925
+ category=category,
926
+ description=match.strip(),
927
+ constraints_satisfied=False
928
+ )
929
+ items.append(item)
930
+
931
+ return items
932
+
933
+ # ========== Correction Algorithm ==========
934
+
935
+ class CorrectionAlgorithm:
936
+ """์ œ์•ฝ์กฐ๊ฑด ๋งŒ์กฑ์„ ์œ„ํ•œ ๋ณด์ • ์•Œ๊ณ ๋ฆฌ์ฆ˜"""
937
+
938
+ def __init__(self, catalog: KidneyDiseaseCatalog):
939
+ self.catalog = catalog
940
+ self.embeddings = OpenAIEmbeddings()
941
+
942
+ def correct_items(self, draft_items: List[RecommendationItem],
943
+ constraints: PatientConstraints) -> List[RecommendationItem]:
944
+ """์ดˆ์•ˆ ํ•ญ๋ชฉ๋“ค์„ ์ œ์•ฝ์กฐ๊ฑด์— ๋งž๊ฒŒ ๋ณด์ •"""
945
+ logger.info(f"Correcting {len(draft_items)} draft items")
946
+
947
+ corrected_items = []
948
+
949
+ for item in draft_items:
950
+ item.embedding = self._get_embedding(item.name)
951
+ similar_docs = self.catalog.search(item.name, k=10)
952
+
953
+ best_replacement = self._find_best_replacement(
954
+ item, similar_docs, constraints
955
+ )
956
+
957
+ if best_replacement:
958
+ corrected_items.append(best_replacement)
959
+ else:
960
+ item.constraints_satisfied = False
961
+ corrected_items.append(item)
962
+
963
+ logger.info(f"Corrected to {len(corrected_items)} items")
964
+ return corrected_items
965
+
966
+ def _get_embedding(self, text: str) -> np.ndarray:
967
+ """ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ"""
968
+ return np.array(self.embeddings.embed_query(text))
969
+
970
+ def _find_best_replacement(self, original_item: RecommendationItem,
971
+ candidates: List[Document],
972
+ constraints: PatientConstraints) -> Optional[RecommendationItem]:
973
+ """์ œ์•ฝ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ์ตœ์  ๋Œ€์ฒด ํ•ญ๋ชฉ ์ฐพ๊ธฐ"""
974
+
975
+ best_item = None
976
+ best_score = float('inf')
977
+
978
+ for doc in candidates:
979
+ if self._check_constraints(doc, constraints):
980
+ doc_embedding = self._get_embedding(doc.page_content)
981
+ distance = np.linalg.norm(original_item.embedding - doc_embedding)
982
+
983
+ if distance < best_score:
984
+ best_score = distance
985
+ best_item = RecommendationItem(
986
+ name=doc.metadata.get('title', doc.page_content[:50]),
987
+ category=doc.metadata.get('field', original_item.category),
988
+ description=doc.page_content,
989
+ constraints_satisfied=True,
990
+ embedding=doc_embedding
991
+ )
992
+
993
+ return best_item
994
+
995
+ def _check_constraints(self, doc: Document, constraints: PatientConstraints) -> bool:
996
+ """๋ฌธ์„œ๊ฐ€ ํ™˜์ž ์ œ์•ฝ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆ"""
997
+
998
+ content = doc.page_content.lower()
999
+
1000
+ if constraints.on_dialysis:
1001
+ if "ํˆฌ์„ ๊ธˆ์ง€" in content or "ํˆฌ์„ ํ™˜์ž ์ œ์™ธ" in content:
1002
+ return False
1003
+
1004
+ if constraints.disease_stage in [DiseaseStage.CKD_4, DiseaseStage.CKD_5]:
1005
+ if "์ง„ํ–‰์„ฑ ์‹ ๋ถ€์ „ ์ฃผ์˜" in content:
1006
+ return False
1007
+
1008
+ for comorbidity in constraints.comorbidities:
1009
+ if comorbidity == "๋‹น๋‡จ" and "๋‹น๋‡จ ๊ธˆ๊ธฐ" in content:
1010
+ return False
1011
+ if comorbidity == "๊ณ ํ˜ˆ์••" and "ํ˜ˆ์•• ์ƒ์Šน ์ฃผ์˜" in content:
1012
+ return False
1013
+
1014
+ return True
1015
+
1016
+ # ========== LangGraph Nodes ==========
1017
+
1018
+ def classify_task(state: GraphState) -> GraphState:
1019
+ """์งˆ๋ฌธ ์œ ํ˜• ๋ถ„๋ฅ˜ - LLM ์‚ฌ์šฉ"""
1020
+ logger.info("=== CLASSIFY TASK NODE ===")
1021
+ logger.info(f"User query: {state['user_query']}")
1022
+
1023
+ state["current_node"] = "๋ถ„๋ฅ˜"
1024
+ state["processing_log"].append("์งˆ๋ฌธ ์œ ํ˜• ๋ถ„์„ ์ค‘...")
1025
+
1026
+ llm = ChatOpenAI(temperature=0.3, model="gpt-4o")
1027
+
1028
+ prompt = f"""
1029
+ ๋‹ค์Œ ์งˆ๋ฌธ์„ ๋ถ„์„ํ•˜์—ฌ ์ ์ ˆํ•œ ์นดํ…Œ๊ณ ๋ฆฌ๋กœ ๋ถ„๋ฅ˜ํ•˜์„ธ์š”.
1030
+
1031
+ ์งˆ๋ฌธ: {state['user_query']}
1032
+
1033
+ ์นดํ…Œ๊ณ ๋ฆฌ:
1034
+ - diet_recommendation: ์‹๋‹จ ์ถ”์ฒœ, ํ•˜๋ฃจ ์‹์‚ฌ ๊ณ„ํš, ๋ฌด์—‡์„ ๋จน์–ด์•ผ ํ• ์ง€ ๋ฌป๋Š” ์งˆ๋ฌธ
1035
+ - diet_analysis: ํŠน์ • ์Œ์‹์˜ ์˜์–‘ ์„ฑ๋ถ„, ์„ญ์ทจ ๊ฐ€๋Šฅ ์—ฌ๋ถ€, ์˜์–‘์†Œ ํ•จ๋Ÿ‰ ๋ถ„์„
1036
+ - medication: ์•ฝ๋ฌผ ๋ณต์šฉ ๋ฐฉ๋ฒ•, ์‹œ๊ฐ„, ๋ถ€์ž‘์šฉ, ์ƒํ˜ธ์ž‘์šฉ
1037
+ - lifestyle: ์ผ์ƒ์ƒํ™œ ๊ด€๋ฆฌ, ์ˆ˜๋ฉด, ์ŠคํŠธ๋ ˆ์Šค, ์ˆ˜๋ถ„ ์„ญ์ทจ
1038
+ - diagnosis: ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ํ•ด์„, ์งˆ๋ณ‘ ๋‹จ๊ณ„, ์ˆ˜์น˜ ์˜๋ฏธ
1039
+ - exercise: ์šด๋™ ๋ฐฉ๋ฒ•, ์ข…๋ฅ˜, ๊ฐ•๋„, ์ฃผ์˜์‚ฌํ•ญ
1040
+ - general: ์œ„ ์นดํ…Œ๊ณ ๋ฆฌ์— ์†ํ•˜์ง€ ์•Š๋Š” ์ผ๋ฐ˜์ ์ธ ์งˆ๋ฌธ
1041
+
1042
+ ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„๋งŒ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”.
1043
+ """
1044
+
1045
+ response = llm.predict(prompt).strip().lower()
1046
+
1047
+ # ์นดํ…Œ๊ณ ๋ฆฌ ๋งคํ•‘
1048
+ category_mapping = {
1049
+ 'diet_recommendation': TaskType.DIET_RECOMMENDATION,
1050
+ 'diet_analysis': TaskType.DIET_ANALYSIS,
1051
+ 'medication': TaskType.MEDICATION,
1052
+ 'lifestyle': TaskType.LIFESTYLE,
1053
+ 'diagnosis': TaskType.DIAGNOSIS,
1054
+ 'exercise': TaskType.EXERCISE,
1055
+ 'general': TaskType.GENERAL
1056
+ }
1057
+
1058
+ selected_task = category_mapping.get(response, TaskType.GENERAL)
1059
+ state["task_type"] = selected_task
1060
+
1061
+ logger.info(f"Task classified as: {selected_task.value}")
1062
+ state["processing_log"].append(f"์งˆ๋ฌธ ์œ ํ˜•: {selected_task.value}")
1063
+ logger.info("=== END CLASSIFY TASK ===\n")
1064
+
1065
+ return state
1066
+
1067
+ def retrieve_context(state: GraphState) -> GraphState:
1068
+ """๊ด€๋ จ ๋ฌธ์„œ ๊ฒ€์ƒ‰"""
1069
+ logger.info("=== RETRIEVE CONTEXT NODE ===")
1070
+ logger.info(f"Query: {state['user_query']}")
1071
+ logger.info(f"Task type: {state['task_type'].value}")
1072
+
1073
+ state["current_node"] = "๊ฒ€์ƒ‰"
1074
+ state["processing_log"].append("๊ด€๋ จ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ์ค‘...")
1075
+
1076
+ catalog = KidneyDiseaseCatalog()
1077
+
1078
+ results = catalog.search_by_patient_context(
1079
+ state["user_query"],
1080
+ state["patient_constraints"],
1081
+ state["task_type"]
1082
+ )
1083
+
1084
+ state["catalog_results"] = results
1085
+
1086
+ for i, doc in enumerate(results[:3]):
1087
+ logger.info(f"Document {i+1}: {doc.metadata.get('title', 'Unknown')} "
1088
+ f"[{doc.metadata.get('raw_tags', 'No tags')}]")
1089
+
1090
+ state["processing_log"].append(f"{len(results)}๊ฐœ ๊ด€๋ จ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ์™„๋ฃŒ")
1091
+ logger.info("=== END RETRIEVE CONTEXT ===\n")
1092
+ return state
1093
+
1094
+ def analyze_diet_request(state: GraphState) -> GraphState:
1095
+ """์‹์ด ๊ด€๋ จ ์š”์ฒญ ๋ถ„์„ ๋ฐ ์‹ํ’ˆ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฒ€์ƒ‰"""
1096
+ logger.info("=== ANALYZE DIET REQUEST NODE ===")
1097
+
1098
+ state["current_node"] = "์‹ํ’ˆ ๋ถ„์„"
1099
+ state["processing_log"].append("์‹ํ’ˆ ์ •๋ณด ๋ถ„์„ ์ค‘...")
1100
+
1101
+ food_db = FoodNutritionDatabase()
1102
+ query = state["user_query"]
1103
+ constraints = state["patient_constraints"]
1104
+
1105
+ # LLM์„ ์‚ฌ์šฉํ•˜์—ฌ ์งˆ๋ฌธ์—์„œ ์–ธ๊ธ‰๋œ ์‹ํ’ˆ ์ถ”์ถœ
1106
+ llm = ChatOpenAI(temperature=0.3, model="gpt-4o")
1107
+
1108
+ prompt = f"""
1109
+ ๋‹ค์Œ ์งˆ๋ฌธ์—์„œ ์–ธ๊ธ‰๋œ ๋ชจ๋“  ์‹ํ’ˆ๋ช…์„ ์ถ”์ถœํ•˜์„ธ์š”.
1110
+
1111
+ ์งˆ๋ฌธ: {query}
1112
+
1113
+ ์‹ํ’ˆ๋ช…๋งŒ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ๋‚˜์—ดํ•˜์„ธ์š”. ์—†์œผ๋ฉด "์—†์Œ"์ด๋ผ๊ณ  ๋‹ตํ•˜์„ธ์š”.
1114
+ """
1115
+
1116
+ food_names_response = llm.predict(prompt).strip()
1117
+ logger.info(f"Extracted food names: {food_names_response}")
1118
+
1119
+ mentioned_foods = []
1120
+ if food_names_response != "์—†์Œ":
1121
+ food_names = [name.strip() for name in food_names_response.split(',')]
1122
+ for food_name in food_names:
1123
+ found_foods = food_db.search_foods(food_name, limit=3)
1124
+ mentioned_foods.extend(found_foods)
1125
+
1126
+ # ์‹ํ’ˆ ๋ถ„์„ ๊ฒฐ๊ณผ ์ƒ์„ฑ
1127
+ analysis_results = {
1128
+ 'mentioned_foods': [],
1129
+ 'suitable_foods': [],
1130
+ 'unsuitable_foods': [],
1131
+ 'nutritional_summary': {}
1132
+ }
1133
+
1134
+ # ์–ธ๊ธ‰๋œ ์‹ํ’ˆ ๋ถ„์„
1135
+ for food in mentioned_foods:
1136
+ is_suitable, issues = food.is_suitable_for_patient(constraints)
1137
+ food_info = {
1138
+ 'name': food.name,
1139
+ 'nutrients': food.get_nutrients_per_serving(100),
1140
+ 'suitable': is_suitable,
1141
+ 'issues': issues
1142
+ }
1143
+
1144
+ analysis_results['mentioned_foods'].append(food_info)
1145
+
1146
+ if is_suitable:
1147
+ analysis_results['suitable_foods'].append(food)
1148
+ else:
1149
+ analysis_results['unsuitable_foods'].append((food, issues))
1150
+
1151
+ state["food_analysis_results"] = analysis_results
1152
+
1153
+ logger.info(f"Analyzed {len(mentioned_foods)} foods")
1154
+ state["processing_log"].append(f"{len(mentioned_foods)}๊ฐœ ์‹ํ’ˆ ๋ถ„์„ ์™„๋ฃŒ")
1155
+ logger.info("=== END ANALYZE DIET REQUEST ===\n")
1156
+
1157
+ return state
1158
+
1159
+ def generate_meal_plan(state: GraphState) -> GraphState:
1160
+ """์ผ์ผ ์‹๋‹จ ๊ณ„ํš ์ƒ์„ฑ"""
1161
+ logger.info("=== GENERATE MEAL PLAN NODE ===")
1162
+
1163
+ state["current_node"] = "์‹๋‹จ ์ƒ์„ฑ"
1164
+ state["processing_log"].append("์ผ์ผ ์‹๋‹จ ๊ณ„ํš ์ƒ์„ฑ ์ค‘...")
1165
+
1166
+ food_db = FoodNutritionDatabase()
1167
+ constraints = state["patient_constraints"]
1168
+
1169
+ # ํ•˜๋ฃจ ์‹๋‹จ ์ƒ์„ฑ
1170
+ meal_plan = food_db.create_daily_meal_plan(constraints)
1171
+
1172
+ # ์˜์–‘์†Œ ์ด๋Ÿ‰ ๊ณ„์‚ฐ
1173
+ daily_totals = {
1174
+ 'calories': 0,
1175
+ 'protein': 0,
1176
+ 'sodium': 0,
1177
+ 'potassium': 0,
1178
+ 'phosphorus': 0
1179
+ }
1180
+
1181
+ for meal_type, foods in meal_plan.items():
1182
+ for food in foods:
1183
+ nutrients = food.get_nutrients_per_serving(100)
1184
+ for nutrient, value in nutrients.items():
1185
+ if nutrient in daily_totals:
1186
+ daily_totals[nutrient] += value
1187
+
1188
+ state["meal_plan"] = meal_plan
1189
+
1190
+ # ๊ธฐ์กด food_analysis_results๊ฐ€ ์žˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ, ์—†์œผ๋ฉด ์ƒ์„ฑ
1191
+ if state.get("food_analysis_results") is None:
1192
+ state["food_analysis_results"] = {}
1193
+
1194
+ state["food_analysis_results"].update({
1195
+ 'meal_plan': meal_plan,
1196
+ 'daily_totals': daily_totals,
1197
+ 'recommendations': []
1198
+ })
1199
+
1200
+ # ์ œ์•ฝ์กฐ๊ฑด ๋Œ€๋น„ ๊ฒ€์ฆ
1201
+ if daily_totals['protein'] > constraints.protein_restriction:
1202
+ state["food_analysis_results"]['recommendations'].append(
1203
+ f"์ฃผ์˜: ์ถ”์ฒœ ์‹๋‹จ์˜ ๋‹จ๋ฐฑ์งˆ ์ด๋Ÿ‰({daily_totals['protein']:.1f}g)์ด "
1204
+ f"์ผ์ผ ์ œํ•œ๋Ÿ‰({constraints.protein_restriction}g)์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค."
1205
+ )
1206
+
1207
+ logger.info("Meal plan generated successfully")
1208
+ state["processing_log"].append("์‹๋‹จ ๊ณ„ํš ์ƒ์„ฑ ์™„๋ฃŒ")
1209
+ logger.info("=== END GENERATE MEAL PLAN ===\n")
1210
+
1211
+ return state
1212
+
1213
+ def generate_diet_response(state: GraphState) -> GraphState:
1214
+ """์‹์ด ๊ด€๋ จ ์ตœ์ข… ์‘๋‹ต ์ƒ์„ฑ"""
1215
+ logger.info("=== GENERATE DIET RESPONSE NODE ===")
1216
+
1217
+ state["current_node"] = "์‘๋‹ต ์ƒ์„ฑ"
1218
+ state["processing_log"].append("์‹์ด ๊ด€๋ จ ๋‹ต๋ณ€ ์ƒ์„ฑ ์ค‘...")
1219
+
1220
+ llm = ChatOpenAI(temperature=0.5, model="gpt-4o")
1221
+
1222
+ task_type = state["task_type"]
1223
+ constraints = state["patient_constraints"]
1224
+ context_docs = state.get("catalog_results", [])
1225
+
1226
+ # ์ฐธ๊ณ  ๋ฌธ์„œ ๋‚ด์šฉ ์ถ”์ถœ
1227
+ context = "\n\n".join([
1228
+ f"[{doc.metadata.get('title', 'Document')}]\n{doc.page_content[:500]}..."
1229
+ for doc in context_docs[:3]
1230
+ ])
1231
+
1232
+ if task_type == TaskType.DIET_RECOMMENDATION and state.get("meal_plan"):
1233
+ # ์‹๋‹จ ์ถ”์ฒœ ์‘๋‹ต
1234
+ meal_plan = state["meal_plan"]
1235
+ daily_totals = state["food_analysis_results"]["daily_totals"]
1236
+
1237
+ prompt = f"""
1238
+ ์‹ ์žฅ์งˆํ™˜ ํ™˜์ž๋ฅผ ์œ„ํ•œ ์ผ์ผ ์‹๋‹จ์„ ์ถ”์ฒœํ•ฉ๋‹ˆ๋‹ค.
1239
+
1240
+ ํ™˜์ž ์ •๋ณด:
1241
+ - ์งˆ๋ณ‘ ๋‹จ๊ณ„: {constraints.disease_stage.value}
1242
+ - ๋‹จ๋ฐฑ์งˆ ์ œํ•œ: {constraints.protein_restriction}g/์ผ
1243
+ - ๋‚˜ํŠธ๋ฅจ ์ œํ•œ: {constraints.sodium_restriction}mg/์ผ
1244
+ - ์นผ๋ฅจ ์ œํ•œ: {constraints.potassium_restriction}mg/์ผ
1245
+ - ์ธ ์ œํ•œ: {constraints.phosphorus_restriction}mg/์ผ
1246
+
1247
+ ์ถ”์ฒœ ์‹๋‹จ:
1248
+ ์•„์นจ: {', '.join([f.name for f in meal_plan['breakfast'][:3]])}
1249
+ ์ ์‹ฌ: {', '.join([f.name for f in meal_plan['lunch'][:3]])}
1250
+ ์ €๋…: {', '.join([f.name for f in meal_plan['dinner'][:3]])}
1251
+ ๊ฐ„์‹: {', '.join([f.name for f in meal_plan['snack'][:2]])}
1252
+
1253
+ ์˜์–‘์†Œ ์ด๋Ÿ‰:
1254
+ - ์นผ๋กœ๋ฆฌ: {daily_totals['calories']:.0f} kcal
1255
+ - ๋‹จ๋ฐฑ์งˆ: {daily_totals['protein']:.1f} g
1256
+ - ๋‚˜ํŠธ๋ฅจ: {daily_totals['sodium']:.0f} mg
1257
+ - ์นผ๋ฅจ: {daily_totals['potassium']:.0f} mg
1258
+ - ์ธ: {daily_totals['phosphorus']:.0f} mg
1259
+
1260
+ ์ฐธ๊ณ  ์ž๋ฃŒ:
1261
+ {context}
1262
+
1263
+ ์œ„ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ™˜์ž๊ฐ€ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•˜๊ณ ,
1264
+ ๊ฐ ์‹์‚ฌ์˜ ์˜์–‘ํ•™์  ์žฅ์ ๊ณผ ์ฃผ์˜์‚ฌํ•ญ์„ ํฌํ•จํ•ด์ฃผ์„ธ์š”.
1265
+ ์˜๋ฃŒ์ง„๊ณผ์˜ ์ƒ๋‹ด ํ•„์š”์„ฑ๋„ ์–ธ๊ธ‰ํ•˜์„ธ์š”.
1266
+ """
1267
+
1268
+ elif task_type == TaskType.DIET_ANALYSIS and state.get("food_analysis_results"):
1269
+ # ํŠน์ • ์‹ํ’ˆ ๋ถ„์„ ์‘๋‹ต
1270
+ analysis = state["food_analysis_results"]
1271
+
1272
+ foods_summary = []
1273
+ for food_info in analysis.get('mentioned_foods', []):
1274
+ summary = f"{food_info['name']}: "
1275
+ if food_info['suitable']:
1276
+ summary += "์„ญ์ทจ ๊ฐ€๋Šฅ"
1277
+ else:
1278
+ summary += f"์ฃผ์˜ ํ•„์š” ({', '.join(food_info['issues'])})"
1279
+ foods_summary.append(summary)
1280
+
1281
+ prompt = f"""
1282
+ ํ™˜์ž๊ฐ€ ์งˆ๋ฌธํ•œ ์‹ํ’ˆ๋“ค์˜ ์˜์–‘ ๋ถ„์„ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค.
1283
+
1284
+ ์งˆ๋ฌธ: {state['user_query']}
1285
+
1286
+ ๋ถ„์„ ๊ฒฐ๊ณผ:
1287
+ {chr(10).join(foods_summary) if foods_summary else "๋ถ„์„๋œ ์‹ํ’ˆ์ด ์—†์Šต๋‹ˆ๋‹ค."}
1288
+
1289
+ ํ™˜์ž์˜ ์ œํ•œ์‚ฌํ•ญ:
1290
+ - ๋‹จ๋ฐฑ์งˆ: {constraints.protein_restriction}g/์ผ
1291
+ - ๋‚˜ํŠธ๋ฅจ: {constraints.sodium_restriction}mg/์ผ
1292
+ - ์นผ๋ฅจ: {constraints.potassium_restriction}mg/์ผ
1293
+ - ์ธ: {constraints.phosphorus_restriction}mg/์ผ
1294
+
1295
+ ์ฐธ๊ณ  ์ž๋ฃŒ:
1296
+ {context}
1297
+
1298
+ ์œ„ ๋ถ„์„์„ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐ ์‹ํ’ˆ์˜ ์„ญ์ทจ ๊ฐ€๋Šฅ ์—ฌ๋ถ€์™€
1299
+ ์ ์ ˆํ•œ ์„ญ์ทจ๋Ÿ‰์„ ๊ตฌ์ฒด์ ์œผ๋กœ ์„ค๋ช…ํ•ด์ฃผ์„ธ์š”.
1300
+ """
1301
+
1302
+ else:
1303
+ # ์ผ๋ฐ˜ ์‹์ด ๊ด€๋ จ ์‘๋‹ต
1304
+ prompt = f"""
1305
+ ์‹ ์žฅ์งˆํ™˜ ํ™˜์ž์˜ ์‹์ด ๊ด€๋ จ ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜์„ธ์š”.
1306
+
1307
+ ์งˆ๋ฌธ: {state['user_query']}
1308
+
1309
+ ํ™˜์ž ์ •๋ณด:
1310
+ - ์งˆ๋ณ‘ ๋‹จ๊ณ„: {constraints.disease_stage.value}
1311
+ - ์˜์–‘ ์ œํ•œ์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
1312
+
1313
+ ์ฐธ๊ณ  ์ž๋ฃŒ:
1314
+ {context}
1315
+
1316
+ ํ™˜์ž ์ƒํƒœ๋ฅผ ๊ณ ๋ คํ•œ ๊ตฌ์ฒด์ ์ด๊ณ  ์‹ค์šฉ์ ์ธ ๋‹ต๋ณ€์„ ์ œ๊ณตํ•˜์„ธ์š”.
1317
+ ์˜๋ฃŒ์ง„๊ณผ์˜ ์ƒ๋‹ด ํ•„์š”์„ฑ๋„ ์–ธ๊ธ‰ํ•˜์„ธ์š”.
1318
+ """
1319
+
1320
+ response = llm.predict(prompt)
1321
+ state["final_response"] = response
1322
+
1323
+ logger.info("Diet response generated")
1324
+ state["processing_log"].append("๋‹ต๋ณ€ ์ƒ์„ฑ ์™„๋ฃŒ")
1325
+ logger.info("=== END GENERATE DIET RESPONSE ===\n")
1326
+
1327
+ return state
1328
+
1329
+ def generate_general_response(state: GraphState) -> GraphState:
1330
+ """์ผ๋ฐ˜ ์งˆ๋ฌธ์— ๋Œ€ํ•œ ์‘๋‹ต ์ƒ์„ฑ"""
1331
+ logger.info("=== GENERATE GENERAL RESPONSE NODE ===")
1332
+
1333
+ state["current_node"] = "์‘๋‹ต ์ƒ์„ฑ"
1334
+ state["processing_log"].append("์ผ๋ฐ˜ ๋‹ต๋ณ€ ์ƒ์„ฑ ์ค‘...")
1335
+
1336
+ generator = DraftGenerator()
1337
+
1338
+ draft_response, draft_items = generator.generate_draft(
1339
+ state["user_query"],
1340
+ state["patient_constraints"],
1341
+ state.get("catalog_results", [])
1342
+ )
1343
+
1344
+ state["draft_response"] = draft_response
1345
+ state["draft_items"] = draft_items
1346
+
1347
+ # ๋ณด์ •์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
1348
+ if draft_items:
1349
+ catalog = KidneyDiseaseCatalog()
1350
+ corrector = CorrectionAlgorithm(catalog)
1351
+ corrected_items = corrector.correct_items(draft_items, state["patient_constraints"])
1352
+ state["corrected_items"] = corrected_items
1353
+
1354
+ # ์ตœ์ข… ์‘๋‹ต ์ƒ์„ฑ
1355
+ llm = ChatOpenAI(temperature=0.3, model="gpt-4o")
1356
+
1357
+ context_docs = state.get("catalog_results", [])
1358
+ context = "\n\n".join([
1359
+ f"[{doc.metadata.get('title', 'Document')}]\n{doc.page_content[:500]}..."
1360
+ for doc in context_docs[:3]
1361
+ ])
1362
+
1363
+ if state.get("corrected_items"):
1364
+ corrected_names = [item.name for item in state["corrected_items"]]
1365
+ prompt = f"""
1366
+ ๋‹ค์Œ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์—ฌ ํ™˜์ž ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•˜์„ธ์š”:
1367
+
1368
+ ์งˆ๋ฌธ: {state["user_query"]}
1369
+
1370
+ ํ™˜์ž ์ •๋ณด:
1371
+ - ์งˆ๋ณ‘ ๋‹จ๊ณ„: {state["patient_constraints"].disease_stage.value}
1372
+ - ํˆฌ์„ ์—ฌ๋ถ€: {'์˜ˆ' if state["patient_constraints"].on_dialysis else '์•„๋‹ˆ์˜ค'}
1373
+
1374
+ ์ฐธ๊ณ  ์ž๋ฃŒ:
1375
+ {context}
1376
+
1377
+ ์ดˆ์•ˆ ๋‹ต๋ณ€: {draft_response}
1378
+
1379
+ ๊ฒ€์ฆ๋œ ๊ถŒ์žฅ์‚ฌํ•ญ: {json.dumps(corrected_names, ensure_ascii=False)}
1380
+
1381
+ ์œ„ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ํ™˜์ž์—๊ฒŒ ๋„์›€์ด ๋˜๋Š” ๋‹ต๋ณ€์„ ์ž‘์„ฑํ•˜์„ธ์š”.
1382
+ ์˜๋ฃŒ์ง„๊ณผ์˜ ์ƒ๋‹ด ํ•„์š”์„ฑ์„ ๋ฐ˜๋“œ์‹œ ์–ธ๊ธ‰ํ•˜์„ธ์š”.
1383
+ """
1384
+ else:
1385
+ prompt = f"""
1386
+ ๋‹ค์Œ ์งˆ๋ฌธ์— ๋Œ€ํ•ด ์ •ํ™•ํ•˜๊ณ  ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๊ฒŒ ๋‹ต๋ณ€ํ•˜์„ธ์š”:
1387
+
1388
+ ์งˆ๋ฌธ: {state["user_query"]}
1389
+
1390
+ ํ™˜์ž ์ •๋ณด:
1391
+ - ์งˆ๋ณ‘ ๋‹จ๊ณ„: {state["patient_constraints"].disease_stage.value}
1392
+ - ํˆฌ์„ ์—ฌ๋ถ€: {'์˜ˆ' if state["patient_constraints"].on_dialysis else '์•„๋‹ˆ์˜ค'}
1393
+
1394
+ ์ฐธ๊ณ  ์ž๋ฃŒ:
1395
+ {context}
1396
+
1397
+ ์ดˆ์•ˆ: {draft_response}
1398
+
1399
+ ์œ„ ์ •๋ณด๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ™˜์ž์—๊ฒŒ ๋„์›€์ด ๋˜๋Š” ๋‹ต๋ณ€์„ ์ž‘์„ฑํ•˜์„ธ์š”.
1400
+ ์˜๋ฃŒ์ง„๊ณผ์˜ ์ƒ๋‹ด ํ•„์š”์„ฑ์„ ๋ฐ˜๋“œ์‹œ ์–ธ๊ธ‰ํ•˜์„ธ์š”.
1401
+ """
1402
+
1403
+ final_response = llm.predict(prompt)
1404
+ state["final_response"] = final_response
1405
+
1406
+ logger.info("General response generated")
1407
+ state["processing_log"].append("๋‹ต๋ณ€ ์ƒ์„ฑ ์™„๋ฃŒ")
1408
+ logger.info("=== END GENERATE GENERAL RESPONSE ===\n")
1409
+
1410
+ return state
1411
+
1412
+ def route_after_classification(state: GraphState) -> str:
1413
+ """ํƒœ์Šคํฌ ๋ถ„๋ฅ˜ ํ›„ ๋ผ์šฐํŒ…"""
1414
+ task_type = state["task_type"]
1415
+
1416
+ if task_type in [TaskType.DIET_RECOMMENDATION, TaskType.DIET_ANALYSIS]:
1417
+ logger.info(f"Routing to diet_path for task type: {task_type.value}")
1418
+ return "diet_path"
1419
+ else:
1420
+ logger.info(f"Routing to general_path for task type: {task_type.value}")
1421
+ return "general_path"
1422
+
1423
+ def route_diet_subtask(state: GraphState) -> str:
1424
+ """์‹์ด ๊ด€๋ จ ์„ธ๋ถ€ ํƒœ์Šคํฌ ๋ผ์šฐํŒ…"""
1425
+ if state["task_type"] == TaskType.DIET_RECOMMENDATION:
1426
+ logger.info("Routing to meal_plan for diet recommendation")
1427
+ return "meal_plan"
1428
+ else:
1429
+ logger.info("Routing to food_analysis for diet analysis")
1430
+ return "food_analysis"
1431
+
1432
+ def route_after_retrieve(state: GraphState) -> str:
1433
+ """๋ฌธ์„œ ๊ฒ€์ƒ‰ ํ›„ ๋ผ์šฐํŒ…"""
1434
+ if state["task_type"] in [TaskType.DIET_RECOMMENDATION, TaskType.DIET_ANALYSIS]:
1435
+ logger.info("Routing to diet_response")
1436
+ return "diet_response"
1437
+ else:
1438
+ logger.info("Routing to general_response")
1439
+ return "general_response"
1440
+
1441
+ # ========== Workflow ๊ตฌ์„ฑ ==========
1442
+
1443
+ def create_kidney_disease_rag_workflow():
1444
+ """์‹ ์žฅ์งˆํ™˜ RAG ์›Œํฌํ”Œ๋กœ์šฐ ์ƒ์„ฑ"""
1445
+ logger.info("Creating kidney disease RAG workflow")
1446
+
1447
+ workflow = StateGraph(GraphState)
1448
+
1449
+ # ๋…ธ๋“œ ์ถ”๊ฐ€
1450
+ workflow.add_node("classify", classify_task)
1451
+ workflow.add_node("retrieve", retrieve_context)
1452
+ workflow.add_node("analyze_diet", analyze_diet_request)
1453
+ workflow.add_node("generate_meal_plan", generate_meal_plan)
1454
+ workflow.add_node("generate_diet_response", generate_diet_response)
1455
+ workflow.add_node("generate_general_response", generate_general_response)
1456
+
1457
+ # ์‹œ์ž‘์ 
1458
+ workflow.set_entry_point("classify")
1459
+
1460
+ # ๋ถ„๋ฅ˜ ํ›„ ๋ผ์šฐํŒ…
1461
+ workflow.add_conditional_edges(
1462
+ "classify",
1463
+ route_after_classification,
1464
+ {
1465
+ "diet_path": "analyze_diet",
1466
+ "general_path": "retrieve"
1467
+ }
1468
+ )
1469
+
1470
+ # ์‹์ด ๊ฒฝ๋กœ - ์„ธ๋ถ€ ๋ถ„๊ธฐ
1471
+ workflow.add_conditional_edges(
1472
+ "analyze_diet",
1473
+ route_diet_subtask,
1474
+ {
1475
+ "meal_plan": "generate_meal_plan",
1476
+ "food_analysis": "retrieve"
1477
+ }
1478
+ )
1479
+
1480
+ # ์‹๋‹จ ์ƒ์„ฑ ํ›„ ๋ฌธ์„œ ๊ฒ€์ƒ‰
1481
+ workflow.add_edge("generate_meal_plan", "retrieve")
1482
+
1483
+ # ๋ฌธ์„œ ๊ฒ€์ƒ‰ ํ›„ ์‘๋‹ต ์ƒ์„ฑ์œผ๋กœ ๋ผ์šฐํŒ…
1484
+ workflow.add_conditional_edges(
1485
+ "retrieve",
1486
+ route_after_retrieve,
1487
+ {
1488
+ "diet_response": "generate_diet_response",
1489
+ "general_response": "generate_general_response"
1490
+ }
1491
+ )
1492
+
1493
+ # ์ตœ์ข… ๋…ธ๋“œ๋“ค์€ END๋กœ
1494
+ workflow.add_edge("generate_diet_response", END)
1495
+ workflow.add_edge("generate_general_response", END)
1496
+
1497
+ compiled_workflow = workflow.compile()
1498
+ logger.info("Workflow compiled successfully")
1499
+
1500
+ return compiled_workflow
1501
+
1502
+ # ========== Streamlit UI ==========
1503
+
1504
+ def main():
1505
+ # ํŽ˜์ด์ง€ ์„ค์ •
1506
+ st.set_page_config(
1507
+ page_title="์‹ ์žฅ์งˆํ™˜ AI ์ƒ๋‹ด ์‹œ์Šคํ…œ",
1508
+ page_icon="๐Ÿฅ",
1509
+ layout="wide",
1510
+ initial_sidebar_state="expanded"
1511
+ )
1512
+
1513
+ # ์‚ฌ์šฉ์ž ์ •์˜ CSS
1514
+ st.markdown("""
1515
+ <style>
1516
+ .main {
1517
+ padding: 2rem;
1518
+ }
1519
+ .stButton>button {
1520
+ background-color: #10b981;
1521
+ color: white;
1522
+ border-radius: 10px;
1523
+ border: none;
1524
+ padding: 0.5rem 1rem;
1525
+ font-weight: bold;
1526
+ transition: background-color 0.3s;
1527
+ }
1528
+ .stButton>button:hover {
1529
+ background-color: #059669;
1530
+ }
1531
+ .chat-message {
1532
+ padding: 1.5rem;
1533
+ border-radius: 1rem;
1534
+ margin-bottom: 1rem;
1535
+ background-color: #f3f4f6;
1536
+ }
1537
+ .user-message {
1538
+ background-color: #e0f2fe;
1539
+ }
1540
+ .assistant-message {
1541
+ background-color: #f0fdf4;
1542
+ }
1543
+ </style>
1544
+ """, unsafe_allow_html=True)
1545
+
1546
+ # Lottie ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋กœ๋“œ
1547
+ def load_lottie_url(url: str):
1548
+ try:
1549
+ r = requests.get(url)
1550
+ if r.status_code == 200:
1551
+ return r.json()
1552
+ except:
1553
+ pass
1554
+ return None
1555
+
1556
+ # ํ—ค๋”
1557
+ col1, col2, col3 = st.columns([1, 2, 1])
1558
+ with col2:
1559
+ st.title("๐Ÿฅ ์‹ ์žฅ์งˆํ™˜ AI ์ƒ๋‹ด ์‹œ์Šคํ…œ")
1560
+ st.caption("๋งž์ถคํ˜• ์˜๋ฃŒ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” AI ์‹œ์Šคํ…œ - OpenAI & LangGraph ๊ธฐ๋ฐ˜")
1561
+
1562
+ # ์‚ฌ์ด๋“œ๋ฐ” - ํ™˜์ž ์ •๋ณด ์ž…๋ ฅ
1563
+ with st.sidebar:
1564
+ st.header("โš™๏ธ ์„ค์ •")
1565
+
1566
+ # API ํ‚ค ์ž…๋ ฅ
1567
+ api_key = st.text_input(
1568
+ "OpenAI API Key",
1569
+ type="password",
1570
+ placeholder="sk-...",
1571
+ help="OpenAI API ํ‚ค๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”"
1572
+ )
1573
+
1574
+ if api_key:
1575
+ os.environ["OPENAI_API_KEY"] = api_key
1576
+
1577
+ st.divider()
1578
+
1579
+ st.header("๐Ÿ‘ค ํ™˜์ž ์ •๋ณด")
1580
+
1581
+ # ๊ธฐ๋ณธ ์ •๋ณด
1582
+ col1, col2 = st.columns(2)
1583
+ with col1:
1584
+ age = st.number_input("๋‚˜์ด", min_value=0, max_value=150, value=65)
1585
+ gender = st.selectbox("์„ฑ๋ณ„", ["๋‚จ์„ฑ", "์—ฌ์„ฑ"])
1586
+
1587
+ with col2:
1588
+ egfr = st.number_input("eGFR (ml/min)", min_value=0.0, max_value=150.0, value=25.0)
1589
+
1590
+ disease_stage = st.selectbox(
1591
+ "์‹ ์žฅ ์งˆํ™˜ ๋‹จ๊ณ„",
1592
+ options=[stage.value for stage in DiseaseStage],
1593
+ index=3 # CKD Stage 4
1594
+ )
1595
+
1596
+ on_dialysis = st.checkbox("ํˆฌ์„ ์ค‘", value=False)
1597
+
1598
+ # ๋™๋ฐ˜์งˆํ™˜ ๋ฐ ์•ฝ๋ฌผ
1599
+ st.subheader("๐Ÿฅ ๋™๋ฐ˜์งˆํ™˜")
1600
+ comorbidities = st.multiselect(
1601
+ "๋™๋ฐ˜์งˆํ™˜ ์„ ํƒ",
1602
+ ["๋‹น๋‡จ", "๊ณ ํ˜ˆ์••", "์‹ฌ๋ถ€์ „", "๊ฐ„์งˆํ™˜", "ํ†ตํ’"],
1603
+ default=["๋‹น๋‡จ", "๊ณ ํ˜ˆ์••"]
1604
+ )
1605
+
1606
+ st.subheader("๐Ÿ’Š ๋ณต์šฉ ์•ฝ๋ฌผ")
1607
+ medications = st.text_area(
1608
+ "๋ณต์šฉ ์ค‘์ธ ์•ฝ๋ฌผ (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„)",
1609
+ value="ARB, ์ธ๊ฒฐํ•ฉ์ œ",
1610
+ help="์˜ˆ: ARB, ์ธ๊ฒฐํ•ฉ์ œ, ๋ฒ ํƒ€์ฐจ๋‹จ์ œ"
1611
+ ).split(",")
1612
+ medications = [med.strip() for med in medications if med.strip()]
1613
+
1614
+ st.divider()
1615
+
1616
+ # ์˜์–‘ ์ œํ•œ์‚ฌํ•ญ
1617
+ st.header("๐Ÿฅ— ์˜์–‘ ์ œํ•œ์‚ฌํ•ญ (์ผ์ผ)")
1618
+
1619
+ protein = st.number_input("๋‹จ๋ฐฑ์งˆ (g)", min_value=0.0, value=40.0)
1620
+ sodium = st.number_input("๋‚˜ํŠธ๋ฅจ (mg)", min_value=0.0, value=2000.0)
1621
+ potassium = st.number_input("์นผ๋ฅจ (mg)", min_value=0.0, value=2000.0)
1622
+ phosphorus = st.number_input("์ธ (mg)", min_value=0.0, value=800.0)
1623
+ fluid = st.number_input("์ˆ˜๋ถ„ (ml)", min_value=0.0, value=1500.0)
1624
+ calorie = st.number_input("์นผ๋กœ๋ฆฌ (kcal)", min_value=0.0, value=1800.0)
1625
+
1626
+ # ๋ฉ”์ธ ์˜์—ญ
1627
+ # ์„ธ์…˜ ์ƒํƒœ ์ดˆ๊ธฐํ™”
1628
+ if "messages" not in st.session_state:
1629
+ st.session_state.messages = []
1630
+
1631
+ if "workflow" not in st.session_state:
1632
+ st.session_state.workflow = None
1633
+
1634
+ # ์›Œํฌํ”Œ๋กœ์šฐ ์ดˆ๊ธฐํ™”
1635
+ if api_key and st.session_state.workflow is None:
1636
+ with st.spinner("์‹œ์Šคํ…œ ์ดˆ๊ธฐํ™” ์ค‘..."):
1637
+ try:
1638
+ st.session_state.workflow = create_kidney_disease_rag_workflow()
1639
+ st.success("โœ… ์‹œ์Šคํ…œ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค!")
1640
+ except Exception as e:
1641
+ st.error(f"์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
1642
+
1643
+ # ์ฑ„ํŒ… ๊ธฐ๋ก ํ‘œ์‹œ
1644
+ for message in st.session_state.messages:
1645
+ with st.chat_message(message["role"]):
1646
+ st.markdown(message["content"])
1647
+
1648
+ # ์ฒ˜๋ฆฌ ๋กœ๊ทธ๊ฐ€ ์žˆ์œผ๋ฉด ํ‘œ์‹œ
1649
+ if "processing_log" in message:
1650
+ with st.expander("๐Ÿ” ์ฒ˜๋ฆฌ ๊ณผ์ • ๋ณด๊ธฐ"):
1651
+ for log in message["processing_log"]:
1652
+ st.caption(log)
1653
+
1654
+ # ์‚ฌ์šฉ์ž ์ž…๋ ฅ
1655
+ if prompt := st.chat_input("์‹ ์žฅ์งˆํ™˜์— ๋Œ€ํ•ด ๋ฌด์—‡์ด๋“  ๋ฌผ์–ด๋ณด์„ธ์š”..."):
1656
+ if not api_key:
1657
+ st.error("โš ๏ธ OpenAI API ํ‚ค๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”!")
1658
+ return
1659
+
1660
+ if not st.session_state.workflow:
1661
+ st.error("โš ๏ธ ์‹œ์Šคํ…œ์ด ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค!")
1662
+ return
1663
+
1664
+ # ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
1665
+ st.session_state.messages.append({"role": "user", "content": prompt})
1666
+
1667
+ with st.chat_message("user"):
1668
+ st.markdown(prompt)
1669
+
1670
+ # AI ์‘๋‹ต ์ƒ์„ฑ
1671
+ with st.chat_message("assistant"):
1672
+ with st.spinner("์ƒ๊ฐ ์ค‘..."):
1673
+ try:
1674
+ # ํ™˜์ž ์ œ์•ฝ์กฐ๊ฑด ์ƒ์„ฑ
1675
+ patient_constraints = PatientConstraints(
1676
+ egfr=egfr,
1677
+ disease_stage=next(s for s in DiseaseStage if s.value == disease_stage),
1678
+ on_dialysis=on_dialysis,
1679
+ comorbidities=comorbidities,
1680
+ medications=medications,
1681
+ age=age,
1682
+ gender=gender,
1683
+ protein_restriction=protein,
1684
+ sodium_restriction=sodium,
1685
+ potassium_restriction=potassium,
1686
+ phosphorus_restriction=phosphorus,
1687
+ fluid_restriction=fluid,
1688
+ calorie_target=calorie
1689
+ )
1690
+
1691
+ # ์ดˆ๊ธฐ ์ƒํƒœ ์ƒ์„ฑ
1692
+ initial_state = GraphState(
1693
+ user_query=prompt,
1694
+ patient_constraints=patient_constraints,
1695
+ task_type=TaskType.GENERAL,
1696
+ draft_response="",
1697
+ draft_items=[],
1698
+ corrected_items=[],
1699
+ final_response="",
1700
+ catalog_results=[],
1701
+ iteration_count=0,
1702
+ error=None,
1703
+ food_analysis_results=None,
1704
+ recommended_foods=None,
1705
+ meal_plan=None,
1706
+ current_node="",
1707
+ processing_log=[]
1708
+ )
1709
+
1710
+ # ์›Œํฌํ”Œ๋กœ์šฐ ์‹คํ–‰
1711
+ result = st.session_state.workflow.invoke(initial_state)
1712
+
1713
+ # ์‘๋‹ต ํ‘œ์‹œ
1714
+ response = result["final_response"]
1715
+ st.markdown(response)
1716
+
1717
+ # ์‹๋‹จ ๊ณ„ํš์ด ์žˆ์œผ๋ฉด ํ‘œ์‹œ
1718
+ if result.get("meal_plan"):
1719
+ st.divider()
1720
+ st.subheader("๐Ÿ“‹ ์ถ”์ฒœ ์‹๋‹จ")
1721
+
1722
+ meal_plan = result["meal_plan"]
1723
+ cols = st.columns(4)
1724
+
1725
+ for idx, (meal_type, foods) in enumerate(meal_plan.items()):
1726
+ with cols[idx % 4]:
1727
+ st.markdown(f"**{meal_type.upper()}**")
1728
+ for food in foods[:3]:
1729
+ nutrients = food.get_nutrients_per_serving(100)
1730
+ st.caption(f"โ€ข {food.name}")
1731
+ st.caption(f" ์นผ๋กœ๋ฆฌ: {nutrients['calories']:.0f}kcal")
1732
+ st.caption(f" ๋‹จ๋ฐฑ์งˆ: {nutrients['protein']:.1f}g")
1733
+
1734
+ # ์‹ํ’ˆ ๋ถ„์„ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ์œผ๋ฉด ํ‘œ์‹œ
1735
+ if result.get("food_analysis_results") and result["food_analysis_results"].get("mentioned_foods"):
1736
+ st.divider()
1737
+ st.subheader("๐Ÿ” ์‹ํ’ˆ ์˜์–‘ ๋ถ„์„")
1738
+
1739
+ for food_info in result["food_analysis_results"]["mentioned_foods"]:
1740
+ col1, col2 = st.columns([1, 3])
1741
+
1742
+ with col1:
1743
+ if food_info['suitable']:
1744
+ st.success("โœ… ์ ํ•ฉ")
1745
+ else:
1746
+ st.warning("โš ๏ธ ์ฃผ์˜")
1747
+
1748
+ with col2:
1749
+ st.markdown(f"**{food_info['name']}**")
1750
+ if not food_info['suitable']:
1751
+ for issue in food_info['issues']:
1752
+ st.caption(f"โ€ข {issue}")
1753
+
1754
+ nutrients = food_info['nutrients']
1755
+ st.caption(
1756
+ f"100g๋‹น: ๋‹จ๋ฐฑ์งˆ {nutrients['protein']:.1f}g, "
1757
+ f"๋‚˜ํŠธ๋ฅจ {nutrients['sodium']:.0f}mg, "
1758
+ f"์นผ๋ฅจ {nutrients['potassium']:.0f}mg"
1759
+ )
1760
+
1761
+ # ์‘๋‹ต ์ €์žฅ
1762
+ message_data = {
1763
+ "role": "assistant",
1764
+ "content": response,
1765
+ "processing_log": result.get("processing_log", [])
1766
+ }
1767
+ st.session_state.messages.append(message_data)
1768
+
1769
+ except Exception as e:
1770
+ st.error(f"์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}")
1771
+ logger.error(f"Error: {e}", exc_info=True)
1772
+
1773
+ # ํ•˜๋‹จ ์ •๋ณด
1774
+ st.divider()
1775
+ col1, col2, col3 = st.columns(3)
1776
+
1777
+ with col1:
1778
+ st.caption("โš ๏ธ ์ด ์‹œ์Šคํ…œ์€ ์˜๋ฃŒ ์ •๋ณด ์ œ๊ณต ๋ชฉ์ ์ด๋ฉฐ, ์‹ค์ œ ์ง„๋ฃŒ๋ฅผ ๋Œ€์ฒดํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
1779
+
1780
+ with col2:
1781
+ if st.button("๐Ÿ’ฌ ์ƒˆ ๋Œ€ํ™” ์‹œ์ž‘"):
1782
+ st.session_state.messages = []
1783
+ st.rerun()
1784
+
1785
+ with col3:
1786
+ if st.button("๐Ÿ“ฅ ๋Œ€ํ™” ๋‚ด์šฉ ๋‹ค์šด๋กœ๋“œ"):
1787
+ conversation = "\n\n".join([
1788
+ f"{'์‚ฌ์šฉ์ž' if msg['role'] == 'user' else 'AI'}: {msg['content']}"
1789
+ for msg in st.session_state.messages
1790
+ ])
1791
+ st.download_button(
1792
+ label="๋‹ค์šด๋กœ๋“œ",
1793
+ data=conversation,
1794
+ file_name=f"kidney_consultation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt",
1795
+ mime="text/plain"
1796
+ )
1797
+
1798
+ if __name__ == "__main__":
1799
+ main()