Update app.py
Browse files
app.py
CHANGED
|
@@ -135,6 +135,30 @@ footer, .footer, .gradio-container footer, .built-with, [class*="footer"], .grad
|
|
| 135 |
box-shadow: 2px 2px 0 #1F2937;
|
| 136 |
}
|
| 137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
.gr-panel, .gr-box, .gr-form, .block, .gr-group {
|
| 139 |
background: #FFF !important;
|
| 140 |
border: 3px solid #1F2937 !important;
|
|
@@ -225,6 +249,58 @@ textarea:focus, input[type="text"]:focus {
|
|
| 225 |
font-size: 1.5rem;
|
| 226 |
}
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
label, .gr-input-label, .gr-block-label {
|
| 229 |
color: #1F2937 !important;
|
| 230 |
font-family: 'Comic Neue', cursive !important;
|
|
@@ -799,14 +875,14 @@ def convert_hwp_to_markdown(input_path: str) -> tuple:
|
|
| 799 |
# ============== LLM API ==============
|
| 800 |
def call_groq_api_stream(messages: List[Dict], api_key: str) -> Generator[str, None, None]:
|
| 801 |
if not api_key:
|
| 802 |
-
yield "❌ Groq API 키가 설정되지 않았습니다."
|
| 803 |
return
|
| 804 |
try:
|
| 805 |
response = requests.post(
|
| 806 |
"https://api.groq.com/openai/v1/chat/completions",
|
| 807 |
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
| 808 |
json={
|
| 809 |
-
"model": "
|
| 810 |
"messages": messages,
|
| 811 |
"temperature": 0.7,
|
| 812 |
"max_tokens": 8192,
|
|
@@ -833,7 +909,7 @@ def call_groq_api_stream(messages: List[Dict], api_key: str) -> Generator[str, N
|
|
| 833 |
|
| 834 |
def call_fireworks_api_stream(messages: List[Dict], image_base64: str, mime_type: str, api_key: str) -> Generator[str, None, None]:
|
| 835 |
if not api_key:
|
| 836 |
-
yield "❌ Fireworks API 키가 설정되지 않았습니다."
|
| 837 |
return
|
| 838 |
try:
|
| 839 |
formatted_messages = [{"role": m["role"], "content": m["content"]} for m in messages[:-1]]
|
|
@@ -849,7 +925,7 @@ def call_fireworks_api_stream(messages: List[Dict], image_base64: str, mime_type
|
|
| 849 |
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
| 850 |
json={
|
| 851 |
"model": "accounts/fireworks/models/qwen3-vl-235b-a22b-thinking",
|
| 852 |
-
"max_tokens":
|
| 853 |
"temperature": 0.6,
|
| 854 |
"messages": formatted_messages,
|
| 855 |
"stream": True
|
|
@@ -885,25 +961,28 @@ def process_file(file_path: str) -> tuple:
|
|
| 885 |
if is_hwp_file(file_path) or is_hwpx_file(file_path):
|
| 886 |
text, error = extract_text_from_hwp_or_hwpx(file_path)
|
| 887 |
if text and len(text.strip()) > 20:
|
| 888 |
-
|
|
|
|
|
|
|
| 889 |
return "error", f"한글 문서 추출 실패: {error}", None
|
| 890 |
|
| 891 |
if is_pdf_file(file_path):
|
| 892 |
text = extract_text_from_pdf(file_path)
|
| 893 |
if text:
|
| 894 |
-
|
|
|
|
| 895 |
return "error", "PDF 추출 실패", None
|
| 896 |
|
| 897 |
if is_text_file(file_path):
|
| 898 |
text = extract_text_from_txt(file_path)
|
| 899 |
if text:
|
| 900 |
-
return "text",
|
| 901 |
return "error", "텍스트 읽기 실패", None
|
| 902 |
|
| 903 |
return "unsupported", f"지원하지 않는 형식: {filename}", None
|
| 904 |
|
| 905 |
def chat_response(message: str, history: List[Dict], file: Optional[str],
|
| 906 |
-
session_id: str
|
| 907 |
if history is None:
|
| 908 |
history = []
|
| 909 |
if not message.strip() and not file:
|
|
@@ -914,10 +993,12 @@ def chat_response(message: str, history: List[Dict], file: Optional[str],
|
|
| 914 |
|
| 915 |
file_type, file_content, file_mime = None, None, None
|
| 916 |
file_info = None
|
|
|
|
| 917 |
|
| 918 |
if file:
|
|
|
|
| 919 |
file_type, file_content, file_mime = process_file(file)
|
| 920 |
-
file_info = json.dumps({"type": file_type, "filename":
|
| 921 |
|
| 922 |
if file_type == "error":
|
| 923 |
history = history + [
|
|
@@ -934,41 +1015,104 @@ def chat_response(message: str, history: List[Dict], file: Optional[str],
|
|
| 934 |
yield history, session_id
|
| 935 |
return
|
| 936 |
|
|
|
|
| 937 |
user_msg = message
|
| 938 |
if file:
|
| 939 |
-
filename = os.path.basename(file)
|
| 940 |
user_msg = f"📎 {filename}\n\n{message}" if message else f"📎 {filename}"
|
| 941 |
|
| 942 |
history = history + [{"role": "user", "content": user_msg}, {"role": "assistant", "content": ""}]
|
| 943 |
yield history, session_id
|
| 944 |
|
|
|
|
| 945 |
db_messages = get_session_messages(session_id, limit=10)
|
| 946 |
-
api_messages = [{
|
| 947 |
-
"role": "system",
|
| 948 |
-
"content": "당신은 도움이 되는 AI 어시스턴트입니다. 한국어로 자연스럽게 대화하며, 파일이 첨부되면 내용을 상세히 분석하여 답변합니다. 문서의 핵심 내용을 파악하고, 사용자의 질문에 정확하게 답변하세요."
|
| 949 |
-
}]
|
| 950 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 951 |
for m in db_messages:
|
| 952 |
api_messages.append({"role": m["role"], "content": m["content"]})
|
| 953 |
|
| 954 |
-
|
| 955 |
if file_type == "text" and file_content:
|
| 956 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
|
| 958 |
api_messages.append({"role": "user", "content": current_content})
|
| 959 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 960 |
full_response = ""
|
| 961 |
if file_type == "image":
|
| 962 |
-
for chunk in call_fireworks_api_stream(api_messages, file_content, file_mime,
|
| 963 |
full_response += chunk
|
| 964 |
history[-1] = {"role": "assistant", "content": full_response}
|
| 965 |
yield history, session_id
|
| 966 |
else:
|
| 967 |
-
for chunk in call_groq_api_stream(api_messages,
|
| 968 |
full_response += chunk
|
| 969 |
history[-1] = {"role": "assistant", "content": full_response}
|
| 970 |
yield history, session_id
|
| 971 |
|
|
|
|
| 972 |
save_message(session_id, "user", current_content, file_info)
|
| 973 |
save_message(session_id, "assistant", full_response)
|
| 974 |
|
|
@@ -1056,7 +1200,7 @@ def convert_hwp(file, output_format, progress=gr.Progress()):
|
|
| 1056 |
f.write(text)
|
| 1057 |
ext = ".txt"
|
| 1058 |
|
| 1059 |
-
elif output_format == "
|
| 1060 |
text, error = convert_hwp_to_markdown(input_path)
|
| 1061 |
if text:
|
| 1062 |
output_path = os.path.join(tmp_dir, "output.md")
|
|
@@ -1146,6 +1290,14 @@ with gr.Blocks(title="HWP AI 어시스턴트", css=COMIC_CSS, delete_cache=(60,
|
|
| 1146 |
</div>
|
| 1147 |
""")
|
| 1148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1149 |
session_state = gr.State("")
|
| 1150 |
|
| 1151 |
with gr.Tabs():
|
|
@@ -1167,12 +1319,6 @@ with gr.Blocks(title="HWP AI 어시스턴트", css=COMIC_CSS, delete_cache=(60,
|
|
| 1167 |
|
| 1168 |
with gr.Row():
|
| 1169 |
with gr.Column(scale=1):
|
| 1170 |
-
gr.HTML('<div class="info-box">⚙️ <b>설정</b></div>')
|
| 1171 |
-
|
| 1172 |
-
with gr.Accordion("🔑 API 키 설정", open=True):
|
| 1173 |
-
groq_key = gr.Textbox(label="Groq API Key", type="password", value=GROQ_API_KEY, placeholder="gsk_...")
|
| 1174 |
-
fireworks_key = gr.Textbox(label="Fireworks API Key", type="password", value=FIREWORKS_API_KEY, placeholder="fw_...")
|
| 1175 |
-
|
| 1176 |
gr.HTML("""
|
| 1177 |
<div class="info-box">
|
| 1178 |
📁 <b>지원 파일 형식</b><br><br>
|
|
@@ -1220,6 +1366,45 @@ with gr.Blocks(title="HWP AI 어시스턴트", css=COMIC_CSS, delete_cache=(60,
|
|
| 1220 |
</div>
|
| 1221 |
""")
|
| 1222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1223 |
with gr.Row():
|
| 1224 |
with gr.Column():
|
| 1225 |
gr.HTML('<div class="info-box">📤 <b>파일 업로드</b></div>')
|
|
@@ -1229,8 +1414,8 @@ with gr.Blocks(title="HWP AI 어시스턴트", css=COMIC_CSS, delete_cache=(60,
|
|
| 1229 |
elem_classes=["upload-box"]
|
| 1230 |
)
|
| 1231 |
format_select = gr.Radio(
|
| 1232 |
-
["
|
| 1233 |
-
value="
|
| 1234 |
label="📋 변환 형식"
|
| 1235 |
)
|
| 1236 |
convert_btn = gr.Button("🔄 변환하기", variant="primary", size="lg")
|
|
@@ -1245,7 +1430,7 @@ with gr.Blocks(title="HWP AI 어시스턴트", css=COMIC_CSS, delete_cache=(60,
|
|
| 1245 |
|
| 1246 |
gr.HTML("""
|
| 1247 |
<div class="info-box">
|
| 1248 |
-
ℹ️ <b>안내</b>: HWPX 파일은
|
| 1249 |
</div>
|
| 1250 |
""")
|
| 1251 |
|
|
@@ -1255,20 +1440,21 @@ with gr.Blocks(title="HWP AI 어시스턴트", css=COMIC_CSS, delete_cache=(60,
|
|
| 1255 |
<p style="font-family:'Bangers',cursive;font-size:1.8rem;letter-spacing:2px">📄 HWP AI 어시스턴트 🤖</p>
|
| 1256 |
<p>AI가 HWP 파일을 읽고, 보고, 말하며, 생각하고 기억합니다!</p>
|
| 1257 |
<p>📖 READ • 👁️ SEE • 💬 SPEAK • 🧠 THINK • 💾 MEMORY</p>
|
|
|
|
| 1258 |
<p style="margin-top:10px"><a href="https://www.humangen.ai" target="_blank" style="color:#FACC15;text-decoration:none;font-weight:bold;">🏠 www.humangen.ai</a></p>
|
| 1259 |
</div>
|
| 1260 |
""")
|
| 1261 |
|
| 1262 |
# ============== 이벤트 핸들러 ==============
|
| 1263 |
-
def on_submit(msg, hist, f, sid
|
| 1264 |
if hist is None:
|
| 1265 |
hist = []
|
| 1266 |
-
for r in chat_response(msg, hist, f, sid
|
| 1267 |
yield r[0], r[1], "", None
|
| 1268 |
|
| 1269 |
-
submit_btn.click(on_submit, [msg_input, chatbot, file_upload, session_state
|
| 1270 |
[chatbot, session_state, msg_input, file_upload])
|
| 1271 |
-
msg_input.submit(on_submit, [msg_input, chatbot, file_upload, session_state
|
| 1272 |
[chatbot, session_state, msg_input, file_upload])
|
| 1273 |
|
| 1274 |
new_btn.click(lambda: ([], create_session(), None, ""), outputs=[chatbot, session_state, file_upload, msg_input])
|
|
|
|
| 135 |
box-shadow: 2px 2px 0 #1F2937;
|
| 136 |
}
|
| 137 |
|
| 138 |
+
/* 무료 서비스 안내 박스 */
|
| 139 |
+
.free-service-notice {
|
| 140 |
+
text-align: center;
|
| 141 |
+
padding: 10px 15px;
|
| 142 |
+
background: linear-gradient(135deg, #FEE2E2 0%, #FECACA 100%);
|
| 143 |
+
border: 3px solid #1F2937;
|
| 144 |
+
border-radius: 8px;
|
| 145 |
+
margin: 10px 0;
|
| 146 |
+
box-shadow: 4px 4px 0 #1F2937;
|
| 147 |
+
font-family: 'Comic Neue', cursive;
|
| 148 |
+
font-weight: 700;
|
| 149 |
+
color: #991B1B;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.free-service-notice a {
|
| 153 |
+
color: #1D4ED8;
|
| 154 |
+
text-decoration: none;
|
| 155 |
+
font-weight: 700;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.free-service-notice a:hover {
|
| 159 |
+
text-decoration: underline;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
.gr-panel, .gr-box, .gr-form, .block, .gr-group {
|
| 163 |
background: #FFF !important;
|
| 164 |
border: 3px solid #1F2937 !important;
|
|
|
|
| 249 |
font-size: 1.5rem;
|
| 250 |
}
|
| 251 |
|
| 252 |
+
/* Markdown 강조 박스 */
|
| 253 |
+
.markdown-highlight-box {
|
| 254 |
+
background: linear-gradient(135deg, #EC4899 0%, #F472B6 100%) !important;
|
| 255 |
+
border: 4px solid #1F2937 !important;
|
| 256 |
+
border-radius: 12px !important;
|
| 257 |
+
padding: 20px !important;
|
| 258 |
+
margin: 15px 0 !important;
|
| 259 |
+
box-shadow: 6px 6px 0 #1F2937 !important;
|
| 260 |
+
animation: pulse-glow 2s ease-in-out infinite;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
@keyframes pulse-glow {
|
| 264 |
+
0%, 100% { box-shadow: 6px 6px 0 #1F2937; }
|
| 265 |
+
50% { box-shadow: 8px 8px 0 #1F2937, 0 0 20px rgba(236, 72, 153, 0.5); }
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.markdown-title {
|
| 269 |
+
font-family: 'Bangers', cursive !important;
|
| 270 |
+
font-size: 2rem !important;
|
| 271 |
+
color: #FFF !important;
|
| 272 |
+
text-shadow: 3px 3px 0 #1F2937 !important;
|
| 273 |
+
letter-spacing: 2px !important;
|
| 274 |
+
margin-bottom: 15px !important;
|
| 275 |
+
text-align: center !important;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.markdown-benefits {
|
| 279 |
+
display: grid;
|
| 280 |
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 281 |
+
gap: 12px;
|
| 282 |
+
margin-top: 10px;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.markdown-benefit-item {
|
| 286 |
+
background: rgba(255,255,255,0.95) !important;
|
| 287 |
+
border: 3px solid #1F2937 !important;
|
| 288 |
+
border-radius: 8px !important;
|
| 289 |
+
padding: 12px !important;
|
| 290 |
+
box-shadow: 3px 3px 0 #1F2937 !important;
|
| 291 |
+
font-family: 'Comic Neue', cursive !important;
|
| 292 |
+
font-weight: 700 !important;
|
| 293 |
+
font-size: 0.95rem !important;
|
| 294 |
+
color: #1F2937 !important;
|
| 295 |
+
text-align: center !important;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.markdown-benefit-icon {
|
| 299 |
+
font-size: 1.8rem !important;
|
| 300 |
+
display: block !important;
|
| 301 |
+
margin-bottom: 5px !important;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
label, .gr-input-label, .gr-block-label {
|
| 305 |
color: #1F2937 !important;
|
| 306 |
font-family: 'Comic Neue', cursive !important;
|
|
|
|
| 875 |
# ============== LLM API ==============
|
| 876 |
def call_groq_api_stream(messages: List[Dict], api_key: str) -> Generator[str, None, None]:
|
| 877 |
if not api_key:
|
| 878 |
+
yield "❌ Groq API 키가 설정되지 않았습니다. 환경변수 GROQ_API_KEY를 설정해주세요."
|
| 879 |
return
|
| 880 |
try:
|
| 881 |
response = requests.post(
|
| 882 |
"https://api.groq.com/openai/v1/chat/completions",
|
| 883 |
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
| 884 |
json={
|
| 885 |
+
"model": "mopenai/gpt-oss-120b",
|
| 886 |
"messages": messages,
|
| 887 |
"temperature": 0.7,
|
| 888 |
"max_tokens": 8192,
|
|
|
|
| 909 |
|
| 910 |
def call_fireworks_api_stream(messages: List[Dict], image_base64: str, mime_type: str, api_key: str) -> Generator[str, None, None]:
|
| 911 |
if not api_key:
|
| 912 |
+
yield "❌ Fireworks API 키가 설정되지 않았습니다. 환경변수 FIREWORKS_API_KEY를 설정해주세요."
|
| 913 |
return
|
| 914 |
try:
|
| 915 |
formatted_messages = [{"role": m["role"], "content": m["content"]} for m in messages[:-1]]
|
|
|
|
| 925 |
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
| 926 |
json={
|
| 927 |
"model": "accounts/fireworks/models/qwen3-vl-235b-a22b-thinking",
|
| 928 |
+
"max_tokens": 8000,
|
| 929 |
"temperature": 0.6,
|
| 930 |
"messages": formatted_messages,
|
| 931 |
"stream": True
|
|
|
|
| 961 |
if is_hwp_file(file_path) or is_hwpx_file(file_path):
|
| 962 |
text, error = extract_text_from_hwp_or_hwpx(file_path)
|
| 963 |
if text and len(text.strip()) > 20:
|
| 964 |
+
print(f"📄 [문서 내용 추출 완료] {len(text)} 글자")
|
| 965 |
+
print(f"📄 [문서 미리보기] {text[:500]}...")
|
| 966 |
+
return "text", text, None
|
| 967 |
return "error", f"한글 문서 추출 실패: {error}", None
|
| 968 |
|
| 969 |
if is_pdf_file(file_path):
|
| 970 |
text = extract_text_from_pdf(file_path)
|
| 971 |
if text:
|
| 972 |
+
print(f"📄 [PDF 내용 추출 완료] {len(text)} 글자")
|
| 973 |
+
return "text", text, None
|
| 974 |
return "error", "PDF 추출 실패", None
|
| 975 |
|
| 976 |
if is_text_file(file_path):
|
| 977 |
text = extract_text_from_txt(file_path)
|
| 978 |
if text:
|
| 979 |
+
return "text", text, None
|
| 980 |
return "error", "텍스트 읽기 실패", None
|
| 981 |
|
| 982 |
return "unsupported", f"지원하지 않는 형식: {filename}", None
|
| 983 |
|
| 984 |
def chat_response(message: str, history: List[Dict], file: Optional[str],
|
| 985 |
+
session_id: str) -> Generator[tuple, None, None]:
|
| 986 |
if history is None:
|
| 987 |
history = []
|
| 988 |
if not message.strip() and not file:
|
|
|
|
| 993 |
|
| 994 |
file_type, file_content, file_mime = None, None, None
|
| 995 |
file_info = None
|
| 996 |
+
filename = None
|
| 997 |
|
| 998 |
if file:
|
| 999 |
+
filename = os.path.basename(file)
|
| 1000 |
file_type, file_content, file_mime = process_file(file)
|
| 1001 |
+
file_info = json.dumps({"type": file_type, "filename": filename})
|
| 1002 |
|
| 1003 |
if file_type == "error":
|
| 1004 |
history = history + [
|
|
|
|
| 1015 |
yield history, session_id
|
| 1016 |
return
|
| 1017 |
|
| 1018 |
+
# 사용자 메시지 표시
|
| 1019 |
user_msg = message
|
| 1020 |
if file:
|
|
|
|
| 1021 |
user_msg = f"📎 {filename}\n\n{message}" if message else f"📎 {filename}"
|
| 1022 |
|
| 1023 |
history = history + [{"role": "user", "content": user_msg}, {"role": "assistant", "content": ""}]
|
| 1024 |
yield history, session_id
|
| 1025 |
|
| 1026 |
+
# 이전 대화 불러오기
|
| 1027 |
db_messages = get_session_messages(session_id, limit=10)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1028 |
|
| 1029 |
+
# 시스템 프롬프트 - 문서 분석 강화
|
| 1030 |
+
system_prompt = """당신은 문서 분석 전문 AI 어시스턴트입니다.
|
| 1031 |
+
|
| 1032 |
+
## 핵심 역할
|
| 1033 |
+
- 사용자가 업로드한 문서의 내용을 **정확하게 분석**하고 **구체적으로 답변**합니다.
|
| 1034 |
+
- 문서에 있는 **실제 내용**을 기반으로만 답변합니다.
|
| 1035 |
+
- 문서에 없는 내용은 추측하지 않습니다.
|
| 1036 |
+
|
| 1037 |
+
## 문서 분석 방법
|
| 1038 |
+
1. **문서가 제공되면**: 문서 전체 내용을 꼼꼼히 읽고 핵심 정보를 파악합니다.
|
| 1039 |
+
2. **요약 요청 시**: 문서의 주제, 목적, 핵심 내용, 주요 항목을 구조화하여 요약합니다.
|
| 1040 |
+
3. **질문 응답 시**: 문서에서 관련 내용을 찾아 **직접 인용하거나 구체적으로 설명**합니다.
|
| 1041 |
+
|
| 1042 |
+
## 답변 형식
|
| 1043 |
+
- 한국어로 자연스럽고 명확하게 답변합니다.
|
| 1044 |
+
- 문서 내용을 인용할 때는 구체적으로 언급합니다.
|
| 1045 |
+
- 긴 문서는 섹션별로 나누어 정리합니다.
|
| 1046 |
+
|
| 1047 |
+
## 주의사항
|
| 1048 |
+
- 문서에 **실제로 있는 내용만** 답변에 포함합니다.
|
| 1049 |
+
- 불확실한 내용은 "문서에서 확인되지 않습니다"라고 명시합니다."""
|
| 1050 |
+
|
| 1051 |
+
api_messages = [{"role": "system", "content": system_prompt}]
|
| 1052 |
+
|
| 1053 |
+
# 이전 대화 추가
|
| 1054 |
for m in db_messages:
|
| 1055 |
api_messages.append({"role": m["role"], "content": m["content"]})
|
| 1056 |
|
| 1057 |
+
# 현재 메시지 구성 - 문서 내용을 명확하게 구분
|
| 1058 |
if file_type == "text" and file_content:
|
| 1059 |
+
if message:
|
| 1060 |
+
current_content = f"""## 📄 업로드된 문서 내용 ({filename})
|
| 1061 |
+
|
| 1062 |
+
다음은 사용자가 업로드한 문서의 전체 내용입니다:
|
| 1063 |
+
|
| 1064 |
+
---
|
| 1065 |
+
{file_content}
|
| 1066 |
+
---
|
| 1067 |
+
|
| 1068 |
+
## 💬 사용자 질문
|
| 1069 |
+
{message}
|
| 1070 |
+
|
| 1071 |
+
위 문서 내용을 바탕으로 사용자의 질문에 **구체적이고 정확하게** 답변해주세요."""
|
| 1072 |
+
else:
|
| 1073 |
+
current_content = f"""## 📄 업로드된 문서 내용 ({filename})
|
| 1074 |
+
|
| 1075 |
+
다음은 사용자가 업로드한 문서의 전체 내용입니다:
|
| 1076 |
+
|
| 1077 |
+
---
|
| 1078 |
+
{file_content}
|
| 1079 |
+
---
|
| 1080 |
+
|
| 1081 |
+
## 📋 요청사항
|
| 1082 |
+
위 문서의 내용을 다음 형식으로 **상세하게 요약**해주세요:
|
| 1083 |
+
|
| 1084 |
+
1. **문서 제목/주제**: 문서가 다루는 주요 주제
|
| 1085 |
+
2. **문서 목적**: 이 문서의 작성 목적
|
| 1086 |
+
3. **핵심 내용**: 가장 중요한 내용 3-5���지
|
| 1087 |
+
4. **세부 항목**: 문서에 포함된 주요 섹션이나 항목
|
| 1088 |
+
5. **결론/요약**: 문서의 핵심 메시지"""
|
| 1089 |
+
else:
|
| 1090 |
+
current_content = message or ""
|
| 1091 |
|
| 1092 |
api_messages.append({"role": "user", "content": current_content})
|
| 1093 |
|
| 1094 |
+
# 디버그 로그
|
| 1095 |
+
print(f"\n🤖 [API 요청]")
|
| 1096 |
+
print(f" - 메시지 수: {len(api_messages)}")
|
| 1097 |
+
print(f" - 파일 타입: {file_type}")
|
| 1098 |
+
print(f" - 문서 길이: {len(file_content) if file_content else 0} 글자")
|
| 1099 |
+
if file_content:
|
| 1100 |
+
print(f" - 문서 미리보기: {file_content[:200]}...")
|
| 1101 |
+
|
| 1102 |
+
# 응답 생성
|
| 1103 |
full_response = ""
|
| 1104 |
if file_type == "image":
|
| 1105 |
+
for chunk in call_fireworks_api_stream(api_messages, file_content, file_mime, FIREWORKS_API_KEY):
|
| 1106 |
full_response += chunk
|
| 1107 |
history[-1] = {"role": "assistant", "content": full_response}
|
| 1108 |
yield history, session_id
|
| 1109 |
else:
|
| 1110 |
+
for chunk in call_groq_api_stream(api_messages, GROQ_API_KEY):
|
| 1111 |
full_response += chunk
|
| 1112 |
history[-1] = {"role": "assistant", "content": full_response}
|
| 1113 |
yield history, session_id
|
| 1114 |
|
| 1115 |
+
# DB 저장
|
| 1116 |
save_message(session_id, "user", current_content, file_info)
|
| 1117 |
save_message(session_id, "assistant", full_response)
|
| 1118 |
|
|
|
|
| 1200 |
f.write(text)
|
| 1201 |
ext = ".txt"
|
| 1202 |
|
| 1203 |
+
elif output_format == "⭐ MARKDOWN (추천)":
|
| 1204 |
text, error = convert_hwp_to_markdown(input_path)
|
| 1205 |
if text:
|
| 1206 |
output_path = os.path.join(tmp_dir, "output.md")
|
|
|
|
| 1290 |
</div>
|
| 1291 |
""")
|
| 1292 |
|
| 1293 |
+
# 무료 서비스 안내
|
| 1294 |
+
gr.HTML("""
|
| 1295 |
+
<div class="free-service-notice">
|
| 1296 |
+
🆓 본 서비스는 <b>무료 버전</b>으로 일부 기능에 제약이 있습니다.<br>
|
| 1297 |
+
📧 문의: <a href="mailto:arxivgpt@gmail.com">arxivgpt@gmail.com</a>
|
| 1298 |
+
</div>
|
| 1299 |
+
""")
|
| 1300 |
+
|
| 1301 |
session_state = gr.State("")
|
| 1302 |
|
| 1303 |
with gr.Tabs():
|
|
|
|
| 1319 |
|
| 1320 |
with gr.Row():
|
| 1321 |
with gr.Column(scale=1):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1322 |
gr.HTML("""
|
| 1323 |
<div class="info-box">
|
| 1324 |
📁 <b>지원 파일 형식</b><br><br>
|
|
|
|
| 1366 |
</div>
|
| 1367 |
""")
|
| 1368 |
|
| 1369 |
+
# Markdown 강조 박스
|
| 1370 |
+
gr.HTML("""
|
| 1371 |
+
<div class="markdown-highlight-box">
|
| 1372 |
+
<div class="markdown-title">⭐ MARKDOWN 변환 추천! ⭐</div>
|
| 1373 |
+
<div class="markdown-benefits">
|
| 1374 |
+
<div class="markdown-benefit-item">
|
| 1375 |
+
<span class="markdown-benefit-icon">🤖</span>
|
| 1376 |
+
<b>AI/LLM 최적화</b><br>
|
| 1377 |
+
ChatGPT, Claude 등 AI에 바로 입력 가능
|
| 1378 |
+
</div>
|
| 1379 |
+
<div class="markdown-benefit-item">
|
| 1380 |
+
<span class="markdown-benefit-icon">📝</span>
|
| 1381 |
+
<b>범용 포맷</b><br>
|
| 1382 |
+
GitHub, Notion, 블로그 등 어디서나 사용
|
| 1383 |
+
</div>
|
| 1384 |
+
<div class="markdown-benefit-item">
|
| 1385 |
+
<span class="markdown-benefit-icon">🔍</span>
|
| 1386 |
+
<b>구조 유지</b><br>
|
| 1387 |
+
제목, 목록, 표 등 문서 구조 보존
|
| 1388 |
+
</div>
|
| 1389 |
+
<div class="markdown-benefit-item">
|
| 1390 |
+
<span class="markdown-benefit-icon">⚡</span>
|
| 1391 |
+
<b>가볍고 빠름</b><br>
|
| 1392 |
+
용량이 작고 처리 속도 빠름
|
| 1393 |
+
</div>
|
| 1394 |
+
<div class="markdown-benefit-item">
|
| 1395 |
+
<span class="markdown-benefit-icon">🔄</span>
|
| 1396 |
+
<b>변환 용이</b><br>
|
| 1397 |
+
HTML, PDF, Word 등으로 재변환 가능
|
| 1398 |
+
</div>
|
| 1399 |
+
<div class="markdown-benefit-item">
|
| 1400 |
+
<span class="markdown-benefit-icon">✏️</span>
|
| 1401 |
+
<b>편집 간편</b><br>
|
| 1402 |
+
메모장으로도 바로 수정 가능
|
| 1403 |
+
</div>
|
| 1404 |
+
</div>
|
| 1405 |
+
</div>
|
| 1406 |
+
""")
|
| 1407 |
+
|
| 1408 |
with gr.Row():
|
| 1409 |
with gr.Column():
|
| 1410 |
gr.HTML('<div class="info-box">📤 <b>파일 업로드</b></div>')
|
|
|
|
| 1414 |
elem_classes=["upload-box"]
|
| 1415 |
)
|
| 1416 |
format_select = gr.Radio(
|
| 1417 |
+
["⭐ MARKDOWN (추천)", "TXT (텍스트)", "HTML", "ODT (OpenDocument)", "XML"],
|
| 1418 |
+
value="⭐ MARKDOWN (추천)",
|
| 1419 |
label="📋 변환 형식"
|
| 1420 |
)
|
| 1421 |
convert_btn = gr.Button("🔄 변환하기", variant="primary", size="lg")
|
|
|
|
| 1430 |
|
| 1431 |
gr.HTML("""
|
| 1432 |
<div class="info-box">
|
| 1433 |
+
ℹ️ <b>안내</b>: HWPX 파일은 MARKDOWN, TXT, XML 변환만 지원됩니다.
|
| 1434 |
</div>
|
| 1435 |
""")
|
| 1436 |
|
|
|
|
| 1440 |
<p style="font-family:'Bangers',cursive;font-size:1.8rem;letter-spacing:2px">📄 HWP AI 어시스턴트 🤖</p>
|
| 1441 |
<p>AI가 HWP 파일을 읽고, 보고, 말하며, 생각하고 기억합니다!</p>
|
| 1442 |
<p>📖 READ • 👁️ SEE • 💬 SPEAK • 🧠 THINK • 💾 MEMORY</p>
|
| 1443 |
+
<p style="margin-top:8px;font-size:0.9rem;">🆓 무료 서비스 (일부 기능 제한) | 📧 arxivgpt@gmail.com</p>
|
| 1444 |
<p style="margin-top:10px"><a href="https://www.humangen.ai" target="_blank" style="color:#FACC15;text-decoration:none;font-weight:bold;">🏠 www.humangen.ai</a></p>
|
| 1445 |
</div>
|
| 1446 |
""")
|
| 1447 |
|
| 1448 |
# ============== 이벤트 핸들러 ==============
|
| 1449 |
+
def on_submit(msg, hist, f, sid):
|
| 1450 |
if hist is None:
|
| 1451 |
hist = []
|
| 1452 |
+
for r in chat_response(msg, hist, f, sid):
|
| 1453 |
yield r[0], r[1], "", None
|
| 1454 |
|
| 1455 |
+
submit_btn.click(on_submit, [msg_input, chatbot, file_upload, session_state],
|
| 1456 |
[chatbot, session_state, msg_input, file_upload])
|
| 1457 |
+
msg_input.submit(on_submit, [msg_input, chatbot, file_upload, session_state],
|
| 1458 |
[chatbot, session_state, msg_input, file_upload])
|
| 1459 |
|
| 1460 |
new_btn.click(lambda: ([], create_session(), None, ""), outputs=[chatbot, session_state, file_upload, msg_input])
|