ginipick commited on
Commit
fe49aa3
Β·
verified Β·
1 Parent(s): 3f26784

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +227 -501
app.py CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  import os
2
  import streamlit as st
3
  import json
@@ -6,32 +15,29 @@ import requests
6
  import logging
7
  from gradio_client import Client
8
  import markdown
9
- import tempfile
10
- import base64
11
- from datetime import datetime
12
  import re
13
- from bs4 import BeautifulSoup # BeautifulSoupλŠ” 이제 μ‚¬μš©ν•˜μ§€ μ•Šμ§€λ§Œ, ν•„μš” μ‹œ μœ μ§€
14
- # (직접 ꡬ글 검색 λ‘œμ§μ€ μ‚­μ œν–ˆμœΌλ―€λ‘œ 사싀상 BeautifulSoupλŠ” ν•„μš” μ—†μŠ΅λ‹ˆλ‹€.)
 
15
 
16
- # λ‘œκΉ… μ„€μ •
17
  logging.basicConfig(
18
  level=logging.INFO,
19
- format='%(asctime)s - %(levelname)s - %(message)s')
20
-
21
- # API μ„€μ •
22
- api_key = os.environ.get("API_KEY")
23
- client = anthropic.Anthropic(api_key=api_key)
24
 
25
- # 이미지 생성 API URL
26
- IMAGE_API_URL = "http://211.233.58.201:7896"
 
 
 
 
27
 
28
- # μ΅œλŒ€ 토큰 수 μ„€μ • (Claude-3 Sonnet의 μ΅œλŒ€ 토큰 수)
29
- MAX_TOKENS = 7999
30
 
31
- # SerpHouse API Key μ„€μ •
32
- SERPHOUSE_API_KEY = os.environ.get("SERPHOUSE_API_KEY", "")
33
-
34
- def get_system_prompt():
35
  return """
36
  당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
37
 
@@ -82,382 +88,167 @@ def get_system_prompt():
82
  9.4. ν†€μ•€λ§€λ„ˆ: μΉœκ·Όν•˜κ³  전문적인 λŒ€ν™”μ²΄
83
  9.5. 데이터: λͺ¨λ“  μ •λ³΄μ˜ 좜처 λͺ…μ‹œ
84
  9.6. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©
85
-
86
- μ΄λŸ¬ν•œ ν”„λ ˆμž„μ›Œν¬λ₯Ό λ°”νƒ•μœΌλ‘œ, μš”μ²­λ°›μ€ μ£Όμ œμ— λŒ€ν•΄ 체계적이고 λ§€λ ₯적인 λΈ”λ‘œκ·Έ 포슀트λ₯Ό μž‘μ„±ν•˜κ² μŠ΅λ‹ˆλ‹€.
87
  """
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def test_image_api_connection():
90
- """이미지 API μ„œλ²„ μ—°κ²° ν…ŒμŠ€νŠΈ"""
91
  try:
92
- client = Client(IMAGE_API_URL)
93
- return "이미지 API μ—°κ²° 성곡: 정상 μž‘λ™ 쀑"
94
  except Exception as e:
95
- logging.error(f"이미지 API μ—°κ²° ν…ŒμŠ€νŠΈ μ‹€νŒ¨: {e}")
96
  return f"이미지 API μ—°κ²° μ‹€νŒ¨: {e}"
97
 
98
- def generate_image(prompt, width=768, height=768, guidance=3.5, inference_steps=30, seed=3):
99
- """이미지 생성 ν•¨μˆ˜"""
100
  if not prompt:
101
- return None, "였λ₯˜: ν”„λ‘¬ν”„νŠΈλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”"
102
-
103
  try:
104
- client = Client(IMAGE_API_URL)
105
- result = client.predict(
106
- prompt=prompt,
107
- width=int(width),
108
- height=int(height),
109
- guidance=float(guidance),
110
- inference_steps=int(inference_steps),
111
- seed=int(seed),
112
- do_img2img=False,
113
- init_image=None,
114
- image2image_strength=0.8,
115
- resize_img=True,
116
  api_name="/generate_image"
117
  )
118
- logging.info(f"이미지 생성 성곡: {result[1]}")
119
- return result[0], f"μ‚¬μš©λœ μ‹œλ“œ: {result[1]}"
120
  except Exception as e:
121
- logging.error(f"이미지 생성 μ‹€νŒ¨: {str(e)}")
122
- return None, f"였λ₯˜: {str(e)}"
123
 
124
  def extract_image_prompt(blog_content, blog_topic):
125
- """λΈ”λ‘œκ·Έ λ‚΄μš©μ—μ„œ 이미지 생성을 μœ„ν•œ ν”„λ‘¬ν”„νŠΈ μΆ”μΆœ"""
126
- image_prompt_system = f"""
127
- λ‹€μŒμ€ '{blog_topic}'에 κ΄€ν•œ λΈ”λ‘œκ·Έ κΈ€μž…λ‹ˆλ‹€. 이 λΈ”λ‘œκ·Έ κΈ€μ˜ λ‚΄μš©μ„ 기반으둜 μ μ ˆν•œ 이미지λ₯Ό μƒμ„±ν•˜κΈ° μœ„ν•œ
128
- ν”„λ‘¬ν”„νŠΈλ₯Ό μž‘μ„±ν•΄μ£Όμ„Έμš”. ν”„λ‘¬ν”„νŠΈλŠ” μ˜μ–΄λ‘œ μž‘μ„±ν•˜κ³ , ꡬ체적인 μ‹œκ°μ  μš”μ†Œλ₯Ό λ‹΄μ•„μ•Ό ν•©λ‹ˆλ‹€.
129
- ν”„λ‘¬ν”„νŠΈλ§Œ λ°˜ν™˜ν•˜μ„Έμš”(λ‹€λ₯Έ μ„€λͺ… 없이).
130
-
131
- μ˜ˆμ‹œ ν˜•μ‹:
132
- "A professional photo of [subject], [specific details], [atmosphere], [lighting], [perspective], high quality, detailed"
133
- """
134
-
135
  try:
136
- response = client.messages.create(
137
  model="claude-3-7-sonnet-20250219",
138
- max_tokens=150,
139
- system=image_prompt_system,
140
  messages=[{"role": "user", "content": blog_content}]
141
  )
142
-
143
- # μ‘λ‹΅μ—μ„œ ν”„λ‘¬ν”„νŠΈ μΆ”μΆœ
144
- image_prompt = response.content[0].text.strip()
145
- logging.info(f"μƒμ„±λœ 이미지 ν”„λ‘¬ν”„νŠΈ: {image_prompt}")
146
- return image_prompt
147
- except Exception as e:
148
- logging.error(f"이미지 ν”„λ‘¬ν”„νŠΈ 생성 였λ₯˜: {e}")
149
- return f"A professional photo related to {blog_topic}, detailed, high quality"
150
 
151
- # λ§ˆν¬λ‹€μš΄μ„ HTML둜 λ³€ν™˜ν•˜λŠ” ν•¨μˆ˜
152
  def convert_md_to_html(md_text, title="Ginigen Blog"):
153
- html_content = markdown.markdown(md_text)
154
- html_doc = f"""
155
- <!DOCTYPE html>
156
- <html>
157
- <head>
158
- <title>{title}</title>
159
- <meta charset="utf-8">
160
- <style>
161
- body {{ font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }}
162
- h1 {{ color: #2c3e50; font-size: 2.5em; margin-bottom: 20px; }}
163
- h2 {{ color: #3498db; margin-top: 25px; font-size: 1.8em; }}
164
- h3 {{ color: #2980b9; font-size: 1.5em; }}
165
- p {{ margin-bottom: 15px; font-size: 1.1em; }}
166
- blockquote {{ background: #f9f9f9; border-left: 10px solid #ccc; margin: 1.5em 10px; padding: 1em 10px; }}
167
- ul, ol {{ margin-bottom: 15px; }}
168
- li {{ margin-bottom: 5px; }}
169
- hr {{ border: 0; height: 1px; background: #ddd; margin: 20px 0; }}
170
- img {{ max-width: 100%; height: auto; display: block; margin: 20px auto; }}
171
- </style>
172
- </head>
173
- <body>
174
- {html_content}
175
- </body>
176
- </html>
177
- """
178
- return html_doc
179
-
180
- # μ›Ή 검색 ν‚€μ›Œλ“œ μΆ”μΆœ ν•¨μˆ˜
181
- def extract_keywords(text: str, top_k: int = 5) -> str:
182
- """
183
- 1) ν•œκΈ€(κ°€-힣), μ˜μ–΄(a-zA-Z), 숫자(0-9), 곡백만 남김
184
- 2) 곡백 κΈ°μ€€ 토큰 뢄리
185
- 3) μ΅œλŒ€ top_k개만
186
- """
187
- text = re.sub(r"[^a-zA-Z0-9κ°€-힣\s]", "", text)
188
- tokens = text.split()
189
- key_tokens = tokens[:top_k]
190
- return " ".join(key_tokens)
191
-
192
- # Mock 검색 κ²°κ³Ό 생성 ν•¨μˆ˜
193
- def generate_mock_search_results(query):
194
- """API 연결이 μ•ˆλ  λ•Œ μ‚¬μš©ν•  가상 검색 κ²°κ³Ό 생성"""
195
- current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
196
- mock_results = [
197
- {
198
- "title": f"{query}에 κ΄€ν•œ μ΅œμ‹  정보",
199
- "link": "https://example.com/article1",
200
- "snippet": f"{query}에 κ΄€ν•œ 가상 검색 κ²°κ³Όμž…λ‹ˆλ‹€. 이 κ²°κ³ΌλŠ” API μ—°κ²° 문제둜 인해 μƒμ„±λœ 가상 λ°μ΄ν„°μž…λ‹ˆλ‹€. μ‹€μ œ 검색 κ²°κ³Όκ°€ μ•„λ‹˜μ„ μ°Έκ³ ν•˜μ„Έμš”. 생성 μ‹œκ°„: {current_time}",
201
- "displayed_link": "example.com/article1"
202
- },
203
- {
204
- "title": f"{query} κ΄€λ ¨ 연ꡬ 동ν–₯",
205
- "link": "https://example.org/research",
206
- "snippet": "이것은 API μ—°κ²° 문제둜 μΈν•œ 가상 검색 κ²°κ³Όμž…λ‹ˆλ‹€. μ‹€μ œ 검색 κ²°κ³Όλ₯Ό λ³΄μ—¬λ“œλ¦¬μ§€ λͺ»ν•΄ μ£„μ†‘ν•©λ‹ˆλ‹€. λŒ€μ‹  AI의 κΈ°μ‘΄ 지식을 ν™œμš©ν•˜μ—¬ λ‹΅λ³€λ“œλ¦¬κ² μŠ΅λ‹ˆλ‹€.",
207
- "displayed_link": "example.org/research"
208
- },
209
- {
210
- "title": f"{query}의 역사적 λ°°κ²½",
211
- "link": "https://example.net/history",
212
- "snippet": "이 가상 검색 κ²°κ³ΌλŠ” API μ—°κ²° 문제둜 인해 μƒμ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 참고용으둜만 μ‚¬μš©ν•΄μ£Όμ„Έμš”.",
213
- "displayed_link": "example.net/history"
214
- }
215
- ]
216
-
217
- summary_lines = []
218
- for idx, item in enumerate(mock_results, start=1):
219
- title = item.get("title", "No title")
220
- link = item.get("link", "#")
221
- snippet = item.get("snippet", "No description")
222
- displayed_link = item.get("displayed_link", link)
223
-
224
- summary_lines.append(
225
- f"### Result {idx}: {title}\n\n"
226
- f"{snippet}\n\n"
227
- f"**좜처**: [{displayed_link}]({link})\n\n"
228
- f"---\n"
229
- )
230
-
231
- notice = """
232
- # 가상 검색 κ²°κ³Ό (API μ—°κ²° 문제둜 인해 생성됨)
233
- μ•„λž˜λŠ” API μ—°κ²° 문제둜 인해 μƒμ„±λœ 가상 검색 κ²°κ³Όμž…λ‹ˆλ‹€. μ‹€μ œ 검색 κ²°κ³Όκ°€ μ•„λ‹˜μ„ μ°Έκ³ ν•˜μ„Έμš”.
234
- λŒ€μ‹  AI의 κΈ°μ‘΄ 지식을 ν™œμš©ν•˜μ—¬ μ΅œλŒ€ν•œ μ •ν™•ν•œ 닡변을 λ“œλ¦¬κ² μŠ΅λ‹ˆλ‹€.
235
- """
236
-
237
- return notice + "\n".join(summary_lines)
238
-
239
-
240
- ###################################################
241
- # SerpHouse 만 μ‚¬μš©ν•˜λŠ” μ›Ή 검색 ν•¨μˆ˜
242
- ###################################################
243
- def do_web_search(query: str) -> str:
244
- """
245
- μ›Ή 검색을 **SerpHouse**둜만 μˆ˜ν–‰ν•˜λŠ” ν•¨μˆ˜.
246
- - SERPHOUSE_API_KEYκ°€ μ—†κ±°λ‚˜ mock일 κ²½μš°μ—λŠ” 가상 검색 κ²°κ³Όλ₯Ό λ°˜ν™˜
247
- - API 호좜이 μ„±κ³΅ν•˜λ©΄ κ·Έ κ²°κ³Όλ₯Ό νŒŒμ‹±ν•˜μ—¬ λ§ˆν¬λ‹€μš΄μœΌλ‘œ λ°˜ν™˜
248
- - μ‹€νŒ¨ν•˜λ©΄ 가상 검색 κ²°κ³Όλ₯Ό λ°˜ν™˜
249
- """
250
- # 1) API ν‚€ μœ νš¨μ„± 체크
251
- if not SERPHOUSE_API_KEY or "mock" in SERPHOUSE_API_KEY.lower():
252
- logging.warning("API ν‚€κ°€ μ—†κ±°λ‚˜ Mock λͺ¨λ“œμž…λ‹ˆλ‹€. => 가상 검색 κ²°κ³Ό λ°˜ν™˜")
253
- return generate_mock_search_results(query)
254
-
255
- try:
256
- # SerpHouse API
257
- url = "https://api.serphouse.com/serp/live"
258
- params = {
259
- "q": query,
260
- "domain": "google.com",
261
- "serp_type": "web", # μ›Ή 검색
262
- "device": "desktop",
263
- "lang": "ko", # ν•œκ΅­μ–΄
264
- "num": "5" # κ²°κ³Ό μ΅œλŒ€ 5개
265
- }
266
- headers = {
267
- "Authorization": f"Bearer {SERPHOUSE_API_KEY}"
268
- }
269
-
270
- logging.info(f"SerpHouse API 호좜 쀑... 검색어: {query}")
271
- response = requests.get(url, headers=headers, params=params, timeout=15)
272
- response.raise_for_status()
273
-
274
- data = response.json()
275
-
276
- # κ²°κ³Ό ꡬ쑰 νŒŒμ•…
277
- results = data.get("results", {})
278
- organic = None
279
-
280
- # κ°€λŠ₯ν•œ 응닡 ꡬ쑰 확인
281
- if isinstance(results, dict) and "organic" in results:
282
- organic = results["organic"]
283
- elif "organic" in data:
284
- organic = data["organic"]
285
-
286
- # organic κ²°κ³Όκ°€ μ—†λ‹€λ©΄ 가상 κ²°κ³Ό
287
- if not organic:
288
- logging.warning("SerpHouse μ‘λ‹΅μ—μ„œ organic ν•­λͺ©μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")
289
- return generate_mock_search_results(query)
290
-
291
- # μ΅œλŒ€ 5개만 μŠ¬λΌμ΄μ‹±
292
- organic = organic[:5]
293
-
294
- # κ²°κ³Ό μš”μ•½ λ¬Έμžμ—΄ λ§Œλ“€κΈ°
295
- summary_lines = []
296
- for idx, item in enumerate(organic, start=1):
297
- title = item.get("title", "No title")
298
- link = item.get("link", "#")
299
- snippet = item.get("snippet", "No description")
300
- displayed_link = item.get("displayed_link", link)
301
-
302
- summary_lines.append(
303
- f"### Result {idx}: {title}\n\n"
304
- f"{snippet}\n\n"
305
- f"**좜처**: [{displayed_link}]({link})\n\n"
306
- f"---\n"
307
- )
308
-
309
- instructions = """
310
- # μ›Ή 검색 κ²°κ³Ό
311
- μ•„λž˜λŠ” 검색 κ²°κ³Όμž…λ‹ˆλ‹€. μ§ˆλ¬Έμ— λ‹΅λ³€ν•  λ•Œ 이 정보λ₯Ό ν™œμš©ν•˜μ„Έμš”:
312
- 1. 각 결과의 제λͺ©, λ‚΄μš©, 좜처 링크λ₯Ό μ°Έκ³ ν•˜μ„Έμš”
313
- 2. 닡변에 κ΄€λ ¨ μ •λ³΄μ˜ 좜처λ₯Ό λͺ…μ‹œμ μœΌλ‘œ μΈμš©ν•˜μ„Έμš” (예: "X μΆœμ²˜μ— λ”°λ₯΄λ©΄...")
314
- 3. 응닡에 μ‹€μ œ 좜처 링크λ₯Ό ν¬ν•¨ν•˜μ„Έμš”
315
- 4. μ—¬λŸ¬ 좜처의 정보λ₯Ό μ’…ν•©ν•˜μ—¬ λ‹΅λ³€ν•˜μ„Έμš”
316
- """
317
-
318
- return instructions + "\n".join(summary_lines)
319
-
320
- except requests.exceptions.Timeout:
321
- logging.error("SerpHouse 검색 νƒ€μž„μ•„μ›ƒ. => 가상 검색 κ²°κ³Όλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.")
322
- return generate_mock_search_results(query)
323
- except Exception as e:
324
- logging.error(f"SerpHouse 검색 μ‹€νŒ¨: {e}")
325
- return generate_mock_search_results(query)
326
 
 
 
 
327
 
 
328
  def chatbot_interface():
329
  st.title("Ginigen Blog")
330
-
331
- # λͺ¨λΈ κ³ μ • μ„€μ •
332
- if "ai_model" not in st.session_state:
333
- st.session_state["ai_model"] = "claude-3-7-sonnet-20250219"
334
-
335
- # μ„Έμ…˜ μƒνƒœ μ΄ˆκΈ°ν™”
336
- if "messages" not in st.session_state:
337
- st.session_state.messages = []
338
-
339
- # μžλ™ μ €μž₯ κΈ°λŠ₯
340
- if "auto_save" not in st.session_state:
341
- st.session_state.auto_save = True
342
-
343
- # 이미지 생성 ν† κΈ€
344
- if "generate_image" not in st.session_state:
345
- st.session_state.generate_image = False
346
-
347
- # μ›Ή 검색 ν† κΈ€
348
- if "use_web_search" not in st.session_state:
349
- st.session_state.use_web_search = False
350
-
351
- # 이미지 API μƒνƒœ
352
- if "image_api_status" not in st.session_state:
353
- st.session_state.image_api_status = test_image_api_connection()
354
-
355
- # λŒ€ν™” 기둝 관리 (μ‚¬μ΄λ“œλ°”)
356
- st.sidebar.title("λŒ€ν™” 기둝 관리")
357
-
358
- # μžλ™ μ €μž₯ ν† κΈ€
359
- st.session_state.auto_save = st.sidebar.toggle("μžλ™ μ €μž₯", value=st.session_state.auto_save)
360
-
361
- # 이미지 생성 ν† κΈ€
362
- st.session_state.generate_image = st.sidebar.toggle("λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± ν›„ 이미지 μžλ™ 생성", value=st.session_state.generate_image)
363
-
364
- # μ›Ή 검색 ν† κΈ€
365
- st.session_state.use_web_search = st.sidebar.toggle("주제 μ›Ή 검색 및 뢄석", value=st.session_state.use_web_search)
366
-
367
- # 이미지 API μƒνƒœ ν‘œμ‹œ
368
- st.sidebar.text(st.session_state.image_api_status)
369
-
370
- # 이미지 생성 μ„€μ • (토글이 켜져 μžˆμ„ λ•Œλ§Œ ν‘œμ‹œ)
371
- if st.session_state.generate_image:
372
- st.sidebar.subheader("이미지 생성 μ„€μ •")
373
- width = st.sidebar.slider("λ„ˆλΉ„", 256, 1024, 768, 64)
374
- height = st.sidebar.slider("높이", 256, 1024, 768, 64)
375
- guidance = st.sidebar.slider("κ°€μ΄λ˜μŠ€ μŠ€μΌ€μΌ", 1.0, 20.0, 3.5, 0.1)
376
- inference_steps = st.sidebar.slider("인퍼런슀 μŠ€ν…", 1, 50, 30, 1)
377
- seed = st.sidebar.number_input("μ‹œλ“œ", value=3, min_value=0, step=1)
378
- else:
379
- # κΈ°λ³Έκ°’ μ„€μ •
380
- width, height, guidance, inference_steps, seed = 768, 768, 3.5, 30, 3
381
-
382
- # λΈ”λ‘œκ·Έ λ‚΄μš© λ‹€μš΄λ‘œλ“œ μ„Ήμ…˜
383
- st.sidebar.title("λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
384
-
385
- # μ΅œμ‹  λΈ”λ‘œκ·Έ λ‚΄μš© κ°€μ Έμ˜€κΈ°
386
- latest_blog = None
387
- latest_blog_title = "λΈ”λ‘œκ·Έ κΈ€"
388
-
389
- if len(st.session_state.messages) > 0:
390
- # κ°€μž₯ 졜근 assistant λ©”μ‹œμ§€ μ°ΎκΈ°
391
- for msg in reversed(st.session_state.messages):
392
- if msg["role"] == "assistant" and msg["content"].strip():
393
- latest_blog = msg["content"]
394
-
395
- # 타이틀 μΆ”μΆœ μ‹œλ„ (첫 번째 제λͺ© νƒœκ·Έ μ‚¬μš©)
396
- title_match = re.search(r'# (.*?)(\n|$)', latest_blog)
397
- if title_match:
398
- latest_blog_title = title_match.group(1).strip()
399
- # μ‚¬μš©μž μž…λ ₯을 νƒ€μ΄ν‹€λ‘œ μ‚¬μš©
400
- elif len(st.session_state.messages) >= 2:
401
- for i in range(len(st.session_state.messages)-1, -1, -1):
402
- if st.session_state.messages[i]["role"] == "user":
403
- latest_blog_title = st.session_state.messages[i]["content"][:30].strip()
404
- if len(st.session_state.messages[i]["content"]) > 30:
405
- latest_blog_title += "..."
406
- break
407
- break
408
-
409
- # λ‹€μš΄λ‘œλ“œ λ²„νŠΌ κ·Έλ£Ή
410
- if latest_blog:
411
- st.sidebar.subheader("졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
412
-
413
- col1, col2 = st.sidebar.columns(2)
414
-
415
- # λ§ˆν¬λ‹€μš΄μœΌλ‘œ λ‹€μš΄λ‘œλ“œ
416
- with col1:
417
- st.download_button(
418
- label="λ§ˆν¬λ‹€μš΄",
419
- data=latest_blog,
420
- file_name=f"{latest_blog_title}.md",
421
- mime="text/markdown"
422
- )
423
-
424
- # HTML둜 λ‹€μš΄λ‘œλ“œ
425
- with col2:
426
- html_content = convert_md_to_html(latest_blog, latest_blog_title)
427
- st.download_button(
428
- label="HTML",
429
- data=html_content,
430
- file_name=f"{latest_blog_title}.html",
431
- mime="text/html"
432
- )
433
-
434
- # λŒ€ν™” 기둝 뢈러였기
435
- uploaded_file = st.sidebar.file_uploader("λŒ€ν™” 기둝 뢈러였기", type=['json'])
436
- if uploaded_file is not None:
437
- try:
438
- content = uploaded_file.getvalue().decode()
439
- if content.strip():
440
- st.session_state.messages = json.loads(content)
441
- st.sidebar.success("λŒ€ν™” 기둝을 μ„±κ³΅μ μœΌλ‘œ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€!")
442
- else:
443
- st.sidebar.warning("μ—…λ‘œλ“œλœ 파일이 λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
444
- except json.JSONDecodeError:
445
- st.sidebar.error("μ˜¬λ°”λ₯Έ JSON ν˜•μ‹μ˜ 파일이 μ•„λ‹™λ‹ˆλ‹€.")
446
- except Exception as e:
447
- st.sidebar.error(f"파일 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}")
448
 
449
- # λŒ€ν™” 기둝 μ΄ˆκΈ°ν™” λ²„νŠΌ
450
- if st.sidebar.button("λŒ€ν™” 기둝 μ΄ˆκΈ°ν™”"):
451
- st.session_state.messages = []
452
- st.sidebar.success("λŒ€ν™” 기둝이 μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
453
-
454
- # λ©”μ‹œμ§€ ν‘œμ‹œ
455
- for message in st.session_state.messages:
456
- with st.chat_message(message["role"]):
457
- st.markdown(message["content"])
458
- # 이미지가 μžˆλŠ” 경우 ν‘œμ‹œ
459
- if "image" in message:
460
- st.image(message["image"], caption=message.get("image_caption", "μƒμ„±λœ 이미지"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
 
462
  # μ‚¬μš©μž μž…λ ₯
463
  if prompt := st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"):
@@ -465,151 +256,86 @@ def chatbot_interface():
465
  with st.chat_message("user"):
466
  st.markdown(prompt)
467
 
468
- # AI 응닡 생성
469
  with st.chat_message("assistant"):
470
- message_placeholder = st.empty()
471
- full_response = ""
472
-
473
- # μ›Ή 검색 μˆ˜ν–‰ (μ˜΅μ…˜μ΄ 켜져 μžˆμ„ 경우)
474
- system_prompt = get_system_prompt()
475
  if st.session_state.use_web_search:
476
- with st.spinner("μ›Ήμ—μ„œ κ΄€λ ¨ 정보λ₯Ό 검색 쀑..."):
477
- try:
478
- search_query = extract_keywords(prompt, top_k=5)
479
- st.info(f"검색어: {search_query}")
480
-
481
- # SerpHouse API둜 검색
482
- search_results = do_web_search(search_query)
483
-
484
- if "가상 검색 κ²°κ³Ό" in search_results:
485
- st.warning("μ‹€μ œ 검색 κ²°κ³Όλ₯Ό κ°€μ Έμ˜¬ 수 μ—†μ–΄ κΈ°μ‘΄ 지식을 ν™œμš©ν•©λ‹ˆλ‹€.")
486
- else:
487
- st.success(f"검색 μ™„λ£Œ: '{search_query}'에 λŒ€ν•œ 정보λ₯Ό μˆ˜μ§‘ν–ˆμŠ΅λ‹ˆλ‹€.")
488
-
489
- # μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈμ— 검색 κ²°κ³Ό μΆ”κ°€
490
- system_prompt += f"\n\n검색 κ²°κ³Ό:\n{search_results}\n"
491
- except Exception as e:
492
- st.error(f"μ›Ή 검색 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}")
493
- logging.error(f"μ›Ή 검색 였λ₯˜: {str(e)}")
494
- system_prompt += "\n\nμ›Ή 검색이 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. κΈ°μ‘΄ 지식을 λ°”νƒ•μœΌλ‘œ λ‹΅λ³€ν•˜μ„Έμš”."
495
-
496
- # API 호좜
497
  with client.messages.stream(
 
498
  max_tokens=MAX_TOKENS,
499
- system=system_prompt,
500
- messages=[{"role": m["role"], "content": m["content"]} for m in st.session_state.messages],
501
- model=st.session_state["ai_model"]
502
  ) as stream:
503
- for text in stream.text_stream:
504
- full_response += str(text) if text is not None else ""
505
- message_placeholder.markdown(full_response + "β–Œ")
506
-
507
- message_placeholder.markdown(full_response)
508
-
509
- # 이미지 생성 μ˜΅μ…˜μ΄ 켜져 μžˆλŠ” 경우
510
  if st.session_state.generate_image:
511
- with st.spinner("λΈ”λ‘œκ·Έμ— λ§žλŠ” 이미지 생성 쀑..."):
512
- # 이미지 ν”„λ‘¬ν”„νŠΈ 생성
513
- image_prompt = extract_image_prompt(full_response, prompt)
514
-
515
- # 이미지 생성
516
- image, image_caption = generate_image(
517
- image_prompt,
518
- width=width,
519
- height=height,
520
- guidance=guidance,
521
- inference_steps=inference_steps,
522
- seed=seed
523
- )
524
-
525
- if image:
526
- st.image(image, caption=image_caption)
527
- # 이미지 정보λ₯Ό 응닡에 포함
528
- st.session_state.messages.append({
529
- "role": "assistant",
530
- "content": full_response,
531
- "image": image,
532
- "image_caption": image_caption
533
- })
534
  else:
535
- st.error(f"이미지 생성 μ‹€νŒ¨: {image_caption}")
536
- st.session_state.messages.append({
537
- "role": "assistant",
538
- "content": full_response
539
- })
540
  else:
541
- # 이미지 생성 없이 μ‘λ‹΅λ§Œ μ €μž₯
542
- st.session_state.messages.append({
543
- "role": "assistant",
544
- "content": full_response
545
- })
546
-
547
- # λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ ν‘œμ‹œ (응닡 λ°”λ‘œ μ•„λž˜μ—)
548
- st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ:")
549
- col1, col2 = st.columns(2)
550
-
551
- with col1:
552
- st.download_button(
553
- label="λ§ˆν¬λ‹€μš΄μœΌλ‘œ μ €μž₯",
554
- data=full_response,
555
- file_name=f"{prompt[:30]}.md",
556
- mime="text/markdown"
557
- )
558
-
559
- with col2:
560
- html_content = convert_md_to_html(full_response, prompt[:30])
561
- st.download_button(
562
- label="HTML둜 μ €μž₯",
563
- data=html_content,
564
- file_name=f"{prompt[:30]}.html",
565
- mime="text/html"
566
  )
567
 
568
- # μžλ™ μ €μž₯ κΈ°λŠ₯
569
- if st.session_state.auto_save:
570
- try:
571
- # 이미지 μ •λ³΄λŠ” μ €μž₯ν•˜μ§€ μ•ŠμŒ (JSONμ—λŠ” λ°”μ΄λ„ˆλ¦¬ 데이터λ₯Ό 직접 μ €μž₯ν•  수 μ—†μŒ)
572
- save_messages = []
573
- for msg in st.session_state.messages:
574
- save_msg = {"role": msg["role"], "content": msg["content"]}
575
- save_messages.append(save_msg)
576
-
577
- # ν˜„μž¬ μ‹œκ°„μ„ ν¬ν•¨ν•œ 파일λͺ… 생성
578
- current_time = datetime.now().strftime("%Y%m%d_%H%M%S")
579
- filename = f'chat_history_auto_save_{current_time}.json'
580
-
581
- with open(filename, 'w', encoding='utf-8') as f:
582
- json.dump(save_messages, f, ensure_ascii=False, indent=4)
583
- except Exception as e:
584
- st.sidebar.error(f"μžλ™ μ €μž₯ 쀑 였λ₯˜ λ°œμƒ: {str(e)}")
585
-
586
- # λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ
587
- if st.sidebar.button("λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ"):
588
- # 이미지 μ •λ³΄λŠ” μ €μž₯ν•˜μ§€ μ•ŠμŒ
589
- save_messages = []
590
- for msg in st.session_state.messages:
591
- save_msg = {"role": msg["role"], "content": msg["content"]}
592
- save_messages.append(save_msg)
593
-
594
- json_history = json.dumps(save_messages, indent=4, ensure_ascii=False)
595
- st.sidebar.download_button(
596
- label="λŒ€ν™” 기둝 μ €μž₯ν•˜κΈ°",
597
- data=json_history,
598
- file_name="chat_history.json",
599
- mime="application/json"
600
- )
601
 
 
602
  def main():
603
  chatbot_interface()
604
 
605
  if __name__ == "__main__":
606
- # requirements.txt 파일 생성
607
  with open("requirements.txt", "w") as f:
608
- f.write("streamlit>=1.31.0\n")
609
- f.write("anthropic>=0.18.1\n")
610
- f.write("gradio-client>=1.8.0\n")
611
- f.write("requests>=2.32.3\n")
612
- f.write("markdown>=3.5.1\n")
613
- f.write("pillow>=10.1.0\n")
614
-
 
615
  main()
 
1
+ """
2
+ Ginigen Blog / Streamlit App
3
+ ────────────────────────────────────────────────────────────────────
4
+ - 2025-04-23 : Brave Search API 버전
5
+ - SerpHouse μ „λ©΄ 제거, Brave Search API 적용
6
+ - API Key : ν™˜κ²½λ³€μˆ˜ SERPHOUSE_API_KEY (μ΄λ¦„λ§Œ κ·ΈλŒ€λ‘œ μ‚¬μš©)
7
+ ────────────────────────────────────────────────────────────────────
8
+ """
9
+
10
  import os
11
  import streamlit as st
12
  import json
 
15
  import logging
16
  from gradio_client import Client
17
  import markdown
 
 
 
18
  import re
19
+ from datetime import datetime
20
+ # BeautifulSoupλŠ” 더 이상 μ‚¬μš©ν•˜μ§€ μ•Šμ§€λ§Œ, ν•„μš” μ‹œ μœ μ§€
21
+ # from bs4 import BeautifulSoup
22
 
23
+ # ───────────────────────────── 1) λ‘œκΉ… ─────────────────────────────────────────
24
  logging.basicConfig(
25
  level=logging.INFO,
26
+ format="%(asctime)s - %(levelname)s - %(message)s"
27
+ )
 
 
 
28
 
29
+ # ───────────────────────────── 2) μ „μ—­ μƒμˆ˜ / API ν‚€ ───────────────────────────
30
+ ANTHROPIC_KEY = os.getenv("API_KEY", "")
31
+ BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Brave Search API ν‚€
32
+ BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
33
+ IMAGE_API_URL = "http://211.233.58.201:7896"
34
+ MAX_TOKENS = 7_999
35
 
36
+ # ───────────────────────────── 3) ν΄λΌμ΄μ–ΈνŠΈ ──────────────────────────────────
37
+ client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
38
 
39
+ # ───────────────────────────── 4) μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ─────────────────────────────
40
+ def get_system_prompt() -> str:
 
 
41
  return """
42
  당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
43
 
 
88
  9.4. ν†€μ•€λ§€λ„ˆ: μΉœκ·Όν•˜κ³  전문적인 λŒ€ν™”μ²΄
89
  9.5. 데이터: λͺ¨λ“  μ •λ³΄μ˜ 좜처 λͺ…μ‹œ
90
  9.6. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©
 
 
91
  """
92
 
93
+ # ───────────────────────────── 5) Brave Search ν•¨μˆ˜ ───────────────────────────
94
+ def brave_search(query: str, count: int = 5):
95
+ """
96
+ Brave Web Search API 호좜 β†’ list[dict] λ°˜ν™˜
97
+ λ°˜ν™˜ ν•­λͺ©: title, link, snippet, displayed_link, index
98
+ """
99
+ if not BRAVE_KEY:
100
+ raise RuntimeError("ν™˜κ²½λ³€μˆ˜ SERPHOUSE_API_KEY(=Brave API key)κ°€ μ„€μ •λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")
101
+
102
+ headers = {
103
+ "Accept": "application/json",
104
+ "Accept-Encoding": "gzip",
105
+ "X-Subscription-Token": BRAVE_KEY
106
+ }
107
+ params = {"q": query, "count": str(count)}
108
+ resp = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
109
+ resp.raise_for_status()
110
+ data = resp.json()
111
+
112
+ web_results = (
113
+ data.get("web", {}).get("results") or
114
+ data.get("results", [])
115
+ )
116
+
117
+ articles = []
118
+ for idx, r in enumerate(web_results[:count], 1):
119
+ url = r.get("url", r.get("link", ""))
120
+ host = re.sub(r"https?://(www\\.)?", "", url).split("/")[0]
121
+ articles.append({
122
+ "index": idx,
123
+ "title": r.get("title", "제λͺ© μ—†μŒ"),
124
+ "link": url,
125
+ "snippet": r.get("description", r.get("text", "λ‚΄μš© μ—†μŒ")),
126
+ "displayed_link": host
127
+ })
128
+ return articles
129
+
130
+ # ───────────────────────────── 6) 검색 β†’ λ§ˆοΏ½οΏ½οΏ½λ‹€μš΄ ─────────────────────────────
131
+ def generate_mock_search_results(query: str) -> str:
132
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
133
+ mock = [{
134
+ "title": f"{query} κ΄€λ ¨ 가상 κ²°κ³Ό",
135
+ "link": "https://example.com",
136
+ "snippet": "API 호좜 μ‹€νŒ¨λ‘œ μƒμ„±λœ μ˜ˆμ‹œ κ²°κ³Όμž…λ‹ˆλ‹€.",
137
+ "displayed_link": "example.com"
138
+ }]
139
+ body = "\n".join(
140
+ f"### Result {i+1}: {m['title']}\n\n{m['snippet']}\n\n"
141
+ f"**좜처**: [{m['displayed_link']}]({m['link']})\n\n---\n"
142
+ for i, m in enumerate(mock)
143
+ )
144
+ return f"# 가상 검색 κ²°κ³Ό (생성: {ts})\n\n{body}"
145
+
146
+ def do_web_search(query: str) -> str:
147
+ """
148
+ Brave Search μ „μš© 검색 ν•¨μˆ˜.
149
+ μ‹€νŒ¨ν•˜κ±°λ‚˜ μΏΌν„° 초과 μ‹œ mock κ²°κ³Ό λ°˜ν™˜.
150
+ """
151
+ try:
152
+ articles = brave_search(query, count=5)
153
+ except Exception as e:
154
+ logging.error(f"Brave 검색 μ‹€νŒ¨: {e}")
155
+ return generate_mock_search_results(query)
156
+
157
+ if not articles:
158
+ return generate_mock_search_results(query)
159
+
160
+ md_lines = []
161
+ for a in articles:
162
+ md_lines.append(
163
+ f"### Result {a['index']}: {a['title']}\n\n"
164
+ f"{a['snippet']}\n\n"
165
+ f"**좜처**: [{a['displayed_link']}]({a['link']})\n\n---\n"
166
+ )
167
+ header = (
168
+ "# μ›Ή 검색 κ²°κ³Ό\n"
169
+ "μ•„λž˜ 정보λ₯Ό 닡변에 ν™œμš©ν•˜μ„Έμš”: 좜처 인용·링크 ν¬ν•¨Β·λ‹€μˆ˜ 좜처 μ’…ν•©\n\n"
170
+ )
171
+ return header + "".join(md_lines)
172
+
173
+ # ───────────────────────────── 7) 이미지·MD λ³€ν™˜ λ“± μœ ν‹Έ ───────────────────────
174
  def test_image_api_connection():
 
175
  try:
176
+ Client(IMAGE_API_URL)
177
+ return "이미지 API μ—°κ²° 성곡"
178
  except Exception as e:
179
+ logging.error(e)
180
  return f"이미지 API μ—°κ²° μ‹€νŒ¨: {e}"
181
 
182
+ def generate_image(prompt, width=768, height=768, guidance=3.5,
183
+ inference_steps=30, seed=3):
184
  if not prompt:
185
+ return None, "ν”„λ‘¬ν”„νŠΈ λΆ€μ‘±"
 
186
  try:
187
+ c = Client(IMAGE_API_URL)
188
+ res = c.predict(
189
+ prompt=prompt, width=width, height=height,
190
+ guidance=guidance, inference_steps=inference_steps,
191
+ seed=seed, do_img2img=False, init_image=None,
192
+ image2image_strength=0.8, resize_img=True,
 
 
 
 
 
 
193
  api_name="/generate_image"
194
  )
195
+ return res[0], f"Seed: {res[1]}"
 
196
  except Exception as e:
197
+ logging.error(e)
198
+ return None, str(e)
199
 
200
  def extract_image_prompt(blog_content, blog_topic):
201
+ system = f"λ‹€μŒ 글을 λ°”νƒ•μœΌλ‘œ μ μ ˆν•œ 이미지 ν”„λ‘¬ν”„νŠΈλ₯Ό μ˜μ–΄λ‘œ ν•œ μ€„λ§Œ 써쀘:\n{blog_topic}"
 
 
 
 
 
 
 
 
 
202
  try:
203
+ res = client.messages.create(
204
  model="claude-3-7-sonnet-20250219",
205
+ max_tokens=80,
206
+ system=system,
207
  messages=[{"role": "user", "content": blog_content}]
208
  )
209
+ return res.content[0].text.strip()
210
+ except Exception:
211
+ return f"A professional photo related to {blog_topic}, high quality"
 
 
 
 
 
212
 
 
213
  def convert_md_to_html(md_text, title="Ginigen Blog"):
214
+ body = markdown.markdown(md_text)
215
+ return f"""<!DOCTYPE html><html><head>
216
+ <title>{title}</title><meta charset="utf-8"></head><body>{body}</body></html>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
+ def extract_keywords(text: str, k: int = 5) -> str:
219
+ txt = re.sub(r"[^κ°€-힣a-zA-Z0-9\\s]", "", text)
220
+ return " ".join(txt.split()[:k])
221
 
222
+ # ───────────────────────────── 8) Streamlit UI ────────────────────────────────
223
  def chatbot_interface():
224
  st.title("Ginigen Blog")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ # μ„Έμ…˜ μƒνƒœ μ΄ˆκΈ°ν™”
227
+ defaults = {
228
+ "ai_model": "claude-3-7-sonnet-20250219",
229
+ "messages": [],
230
+ "auto_save": True,
231
+ "generate_image": False,
232
+ "use_web_search": False,
233
+ "image_api_status": test_image_api_connection()
234
+ }
235
+ for k, v in defaults.items():
236
+ if k not in st.session_state:
237
+ st.session_state[k] = v
238
+
239
+ sb = st.sidebar
240
+ sb.title("λŒ€ν™” 기둝 관리")
241
+ sb.toggle("μžλ™ μ €μž₯", key="auto_save")
242
+ sb.toggle("λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± ν›„ 이미지 μžλ™ 생성", key="generate_image")
243
+ sb.toggle("주제 μ›Ή 검색 및 뢄석", key="use_web_search")
244
+ sb.text(st.session_state.image_api_status)
245
+
246
+ # κΈ°μ‘΄ λ©”μ‹œμ§€ λ Œλ”λ§
247
+ for m in st.session_state.messages:
248
+ with st.chat_message(m["role"]):
249
+ st.markdown(m["content"])
250
+ if "image" in m:
251
+ st.image(m["image"], caption=m.get("image_caption", ""))
252
 
253
  # μ‚¬μš©μž μž…λ ₯
254
  if prompt := st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"):
 
256
  with st.chat_message("user"):
257
  st.markdown(prompt)
258
 
 
259
  with st.chat_message("assistant"):
260
+ placeholder = st.empty()
261
+ full_resp = ""
262
+ sys_prompt = get_system_prompt()
263
+
264
+ # (선택) Brave 검색
265
  if st.session_state.use_web_search:
266
+ with st.spinner("μ›Ή 검색 쀑…"):
267
+ q = extract_keywords(prompt)
268
+ sb.info(f"검색어: {q}")
269
+ search_md = do_web_search(q)
270
+ if "가상 검색 κ²°κ³Ό" in search_md:
271
+ sb.warning("μ‹€μ œ 검색 κ²°κ³Όλ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.")
272
+ sys_prompt += f"\n\n검색 κ²°κ³Ό:\n{search_md}\n"
273
+
274
+ # Claude 슀트리밍
 
 
 
 
 
 
 
 
 
 
 
 
275
  with client.messages.stream(
276
+ model=st.session_state.ai_model,
277
  max_tokens=MAX_TOKENS,
278
+ system=sys_prompt,
279
+ messages=[{"role": m["role"], "content": m["content"]}
280
+ for m in st.session_state.messages]
281
  ) as stream:
282
+ for t in stream.text_stream:
283
+ full_resp += t or ""
284
+ placeholder.markdown(full_resp + "β–Œ")
285
+ placeholder.markdown(full_resp)
286
+
287
+ # (선택) 이미지 생성
 
288
  if st.session_state.generate_image:
289
+ with st.spinner("이미지 생성 쀑…"):
290
+ img_prompt = extract_image_prompt(full_resp, prompt)
291
+ img, caption = generate_image(img_prompt)
292
+ if img:
293
+ st.image(img, caption=caption)
294
+ st.session_state.messages.append(
295
+ {"role": "assistant", "content": full_resp,
296
+ "image": img, "image_caption": caption}
297
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  else:
299
+ st.error(f"이미지 생성 μ‹€νŒ¨: {caption}")
300
+ st.session_state.messages.append(
301
+ {"role": "assistant", "content": full_resp}
302
+ )
 
303
  else:
304
+ st.session_state.messages.append(
305
+ {"role": "assistant", "content": full_resp}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  )
307
 
308
+ # λ‹€μš΄λ‘œλ“œ λ²„νŠΌ
309
+ st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ:")
310
+ c1, c2 = st.columns(2)
311
+ c1.download_button("λ§ˆν¬λ‹€μš΄", full_resp,
312
+ file_name=f"{prompt[:30]}.md", mime="text/markdown")
313
+ html = convert_md_to_html(full_resp, prompt[:30])
314
+ c2.download_button("HTML", html,
315
+ file_name=f"{prompt[:30]}.html", mime="text/html")
316
+
317
+ # μžλ™ μ €μž₯
318
+ if st.session_state.auto_save and st.session_state.messages:
319
+ try:
320
+ fname = f"chat_history_{datetime.now():%Y%m%d_%H%M%S}.json"
321
+ with open(fname, "w", encoding="utf-8") as f:
322
+ json.dump(st.session_state.messages, f, ensure_ascii=False, indent=2)
323
+ except Exception as e:
324
+ sb.error(f"μžλ™ μ €μž₯ 였λ₯˜: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
+ # ───────────────────────────── 9) main ────────────────────────────────────────
327
  def main():
328
  chatbot_interface()
329
 
330
  if __name__ == "__main__":
331
+ # requirements.txt 생성
332
  with open("requirements.txt", "w") as f:
333
+ f.write("\n".join([
334
+ "streamlit>=1.31.0",
335
+ "anthropic>=0.18.1",
336
+ "gradio-client>=1.8.0",
337
+ "requests>=2.32.3",
338
+ "markdown>=3.5.1",
339
+ "pillow>=10.1.0"
340
+ ]))
341
  main()