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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +152 -180
app.py CHANGED
@@ -1,42 +1,40 @@
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
13
  import anthropic
14
- import requests
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단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
@@ -90,14 +88,14 @@ def get_system_prompt() -> str:
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",
@@ -105,230 +103,204 @@ def brave_search(query: str, count: int = 5):
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("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"):
255
  st.session_state.messages.append({"role": "user", "content": prompt})
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",
 
1
  """
2
+ Ginigen Blog / Streamlit App — Brave Search API Edition
3
+ ────────────────────────────────────────────────────────────────────────
4
+ * 2025-04-23 : SerpHouse μ˜μ‘΄μ„± β‡’ Brave Search API 둜 μ „λ©΄ ꡐ체
5
+ * ν™˜κ²½λ³€μˆ˜ SERPHOUSE_API_KEY β†’ Brave API Key κ·ΈλŒ€λ‘œ μ‚¬μš©
6
+ * **원본 μ½”λ“œμ˜ κΈ°λŠ₯ 100 % μœ μ§€**
7
+ - Markdown / HTML λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ (μ‚¬μ΄λ“œλ°” + λ³Έλ¬Έ)
8
+ - λŒ€ν™” 기둝 JSON μ—…λ‘œλ“œ & λ‹€μš΄λ‘œλ“œ + λ°±κ·ΈλΌμš΄λ“œ μžλ™ μ €μž₯
9
+ - 이미지 μžλ™ 생성 μ˜΅μ…˜
10
+ - Streamlit λͺ¨λ“  UI ν† κΈ€
11
+ ────────────────────────────────────────────────────────────────────────
12
  """
13
 
14
+ # ──────────────────────────────── Imports ────────────────────────────────
15
+ import os, json, re, logging, requests, markdown
16
+ from datetime import datetime
17
+
18
  import streamlit as st
 
19
  import anthropic
 
 
20
  from gradio_client import Client
21
+ # from bs4 import BeautifulSoup # ν•„μš” μ‹œ 주석 ν•΄μ œ
 
 
 
 
 
 
 
 
 
 
22
 
23
+ # ──────────────────────────────── ν™˜κ²½ λ³€μˆ˜ / μƒμˆ˜ ───────────────────────────
24
  ANTHROPIC_KEY = os.getenv("API_KEY", "")
25
+ BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # 이름 μœ μ§€
26
  BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
27
  IMAGE_API_URL = "http://211.233.58.201:7896"
28
  MAX_TOKENS = 7_999
29
 
30
+ # ──────────────────────────────── λ‘œκΉ… ──────────────────────────────────────
31
+ logging.basicConfig(level=logging.INFO,
32
+ format="%(asctime)s - %(levelname)s - %(message)s")
33
+
34
+ # ──────────────────────────────── Anthropic Client ─────────────────────────
35
  client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
36
 
37
+ # ──────────────────────────────── λΈ”λ‘œκ·Έ μž‘μ„± μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ────────────────
38
  def get_system_prompt() -> str:
39
  return """
40
  당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
 
88
  9.6. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©
89
  """
90
 
91
+ # ──────────────────────────────── Brave Search API ─────────────────────────
92
  def brave_search(query: str, count: int = 5):
93
  """
94
+ Brave Web Search API 호좜 β†’ list[dict]
95
+ λ°˜ν™˜ ν•„λ“œ: index, title, link, snippet, displayed_link
96
  """
97
  if not BRAVE_KEY:
98
+ raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) ν™˜κ²½λ³€μˆ˜κ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
99
 
100
  headers = {
101
  "Accept": "application/json",
 
103
  "X-Subscription-Token": BRAVE_KEY
104
  }
105
  params = {"q": query, "count": str(count)}
106
+ r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
107
+ r.raise_for_status()
108
+ data = r.json()
109
+
110
+ raw = data.get("web", {}).get("results") or data.get("results", [])
111
+ arts = []
112
+ for i, res in enumerate(raw[:count], 1):
113
+ url = res.get("url", res.get("link", ""))
114
+ host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
115
+ arts.append({
116
+ "index": i,
117
+ "title": res.get("title", "제λͺ© μ—†μŒ"),
 
 
 
 
118
  "link": url,
119
+ "snippet": res.get("description", res.get("text", "λ‚΄μš© μ—†μŒ")),
120
  "displayed_link": host
121
  })
122
+ return arts
123
 
124
+ def mock_results(query: str) -> str:
 
125
  ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
126
+ return (f"# 가상 검색 κ²°κ³Ό (생성: {ts})\n\n"
127
+ f"### Result 1: {query} κ΄€λ ¨ μ˜ˆμ‹œ κ²°κ³Ό\n\n"
128
+ "API 호좜 μ‹€νŒ¨λ‘œ μƒμ„±λœ μž„μ‹œ λ°μ΄ν„°μž…λ‹ˆλ‹€.\n\n"
129
+ "**좜처**: [example.com](https://example.com)\n\n---\n")
 
 
 
 
 
 
 
 
130
 
131
  def do_web_search(query: str) -> str:
 
 
 
 
132
  try:
133
+ arts = brave_search(query, 5)
134
  except Exception as e:
135
  logging.error(f"Brave 검색 μ‹€νŒ¨: {e}")
136
+ return mock_results(query)
137
+ if not arts:
138
+ return mock_results(query)
 
139
 
140
+ hdr = "# μ›Ή 검색 κ²°κ³Ό\nμ•„λž˜ 정보λ₯Ό μ°Έκ³ ν•΄μ„œ λ‹΅λ³€ν•˜μ„Έμš”.\n\n"
141
+ body = "\n".join(
142
+ f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
143
+ f"**좜처**: [{a['displayed_link']}]({a['link']})\n\n---\n"
144
+ for a in arts
 
 
 
 
 
145
  )
146
+ return hdr + body
 
 
 
 
 
 
 
 
 
147
 
148
+ # ──────────────────────────────── 이미지 Β· λ³€ν™˜ μœ ν‹Έ ────────────────────────
149
+ def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
150
+ if not prompt: return None, "ν”„λ‘¬ν”„νŠΈ λΆ€μ‘±"
 
151
  try:
152
+ res = Client(IMAGE_API_URL).predict(
153
+ prompt=prompt, width=w, height=h, guidance=g,
154
+ inference_steps=steps, seed=seed,
155
+ do_img2img=False, init_image=None,
 
156
  image2image_strength=0.8, resize_img=True,
157
+ api_name="/generate_image")
 
158
  return res[0], f"Seed: {res[1]}"
159
  except Exception as e:
160
+ logging.error(e); return None, str(e)
 
161
 
162
+ def extract_image_prompt(blog: str, topic: str):
163
+ sys = f"λ‹€μŒ κΈ€λ‘œλΆ€ν„° μ˜μ–΄ 1쀄 이미지 ν”„λ‘¬ν”„νŠΈ 생성:\n{topic}"
164
  try:
165
  res = client.messages.create(
166
  model="claude-3-7-sonnet-20250219",
167
+ max_tokens=80, system=sys,
168
+ messages=[{"role": "user", "content": blog}]
 
169
  )
170
  return res.content[0].text.strip()
171
  except Exception:
172
+ return f"A professional photo related to {topic}, high quality"
173
 
174
+ def md_to_html(md: str, title="Ginigen Blog"):
175
+ return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
 
 
176
 
177
+ def keywords(text: str, top=5):
178
+ return " ".join(re.sub(r"[^κ°€-힣a-zA-Z0-9\\s]", "", text).split()[:top])
 
179
 
180
+ # ──────────────────────────────── Streamlit UI ────────────────────────────
181
+ def ginigen_app():
182
  st.title("Ginigen Blog")
183
 
184
+ # μ„Έμ…˜ κΈ°λ³Έκ°’
185
+ defaults = dict(
186
+ ai_model="claude-3-7-sonnet-20250219",
187
+ messages=[],
188
+ auto_save=True,
189
+ generate_image=False,
190
+ use_web_search=False
191
+ )
 
192
  for k, v in defaults.items():
193
+ st.session_state.setdefault(k, v)
 
194
 
195
+ # ── μ‚¬μ΄λ“œλ°” 컨트둀
196
  sb = st.sidebar
197
  sb.title("λŒ€ν™” 기둝 관리")
198
+ sb.toggle("μžλ™ μ €μž₯", key="auto_save")
199
+ sb.toggle("이미지 μžλ™ 생성", key="generate_image")
200
+ sb.toggle("μ›Ή 검색 μ‚¬μš©", key="use_web_search")
201
+
202
+ # ── 졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ (λ§ˆν¬λ‹€μš΄ / HTML)
203
+ latest_blog = next(
204
+ (m["content"] for m in reversed(st.session_state.messages)
205
+ if m["role"] == "assistant" and m["content"].strip()), None)
206
+
207
+ if latest_blog:
208
+ title = re.search(r"# (.*?)(\n|$)", latest_blog)
209
+ title = title.group(1).strip() if title else "blog"
210
+ sb.subheader("졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
211
+ c1, c2 = sb.columns(2)
212
+ c1.download_button("Markdown", latest_blog,
213
+ file_name=f"{title}.md", mime="text/markdown")
214
+ c2.download_button("HTML", md_to_html(latest_blog, title),
215
+ file_name=f"{title}.html", mime="text/html")
216
+
217
+ # ── JSON λŒ€ν™” 기둝 μ—…λ‘œλ“œ
218
+ up = sb.file_uploader("λŒ€ν™” 기둝 뢈러였기 (.json)", type=["json"])
219
+ if up:
220
+ try:
221
+ st.session_state.messages = json.load(up)
222
+ sb.success("λŒ€ν™” 기둝 뢈러였기 μ™„λ£Œ")
223
+ except Exception as e:
224
+ sb.error(f"뢈러였기 μ‹€νŒ¨: {e}")
225
+
226
+ # ── JSON λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ
227
+ if sb.button("λŒ€ν™” 기둝 JSON λ‹€μš΄λ‘œλ“œ"):
228
+ sb.download_button("μ €μž₯", json.dumps(st.session_state.messages,
229
+ ensure_ascii=False, indent=2),
230
+ file_name="chat_history.json",
231
+ mime="application/json")
232
 
233
+ # ── κΈ°μ‘΄ λ©”μ‹œμ§€ λ Œλ”λ§
234
  for m in st.session_state.messages:
235
  with st.chat_message(m["role"]):
236
  st.markdown(m["content"])
237
  if "image" in m:
238
  st.image(m["image"], caption=m.get("image_caption", ""))
239
 
240
+ # ── μ‚¬μš©μž μž…λ ₯
241
  if prompt := st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"):
242
  st.session_state.messages.append({"role": "user", "content": prompt})
243
+ with st.chat_message("user"): st.markdown(prompt)
 
244
 
245
  with st.chat_message("assistant"):
246
+ placeholder = st.empty(); answer = ""
 
247
  sys_prompt = get_system_prompt()
248
 
 
249
  if st.session_state.use_web_search:
250
  with st.spinner("μ›Ή 검색 쀑…"):
251
+ search_md = do_web_search(keywords(prompt))
 
 
 
 
252
  sys_prompt += f"\n\n검색 κ²°κ³Ό:\n{search_md}\n"
253
 
254
  # Claude 슀트리밍
255
  with client.messages.stream(
256
+ model=st.session_state.ai_model, max_tokens=MAX_TOKENS,
 
257
  system=sys_prompt,
258
  messages=[{"role": m["role"], "content": m["content"]}
259
  for m in st.session_state.messages]
260
  ) as stream:
261
  for t in stream.text_stream:
262
+ answer += t or ""
263
+ placeholder.markdown(answer + "β–Œ")
264
+ placeholder.markdown(answer)
265
 
266
+ # 이미지 μ˜΅μ…˜
267
  if st.session_state.generate_image:
268
  with st.spinner("이미지 생성 쀑…"):
269
+ ip = extract_image_prompt(answer, prompt)
270
+ img, cap = generate_image(ip)
271
  if img:
272
+ st.image(img, caption=cap)
 
 
 
 
 
 
273
  st.session_state.messages.append(
274
+ {"role": "assistant", "content": answer,
275
+ "image": img, "image_caption": cap})
276
+ answer_entry_saved = True
277
+ if not st.session_state.generate_image:
278
  st.session_state.messages.append(
279
+ {"role": "assistant", "content": answer})
 
280
 
281
+ # λ³Έλ¬Έ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ (MD / HTML)
282
+ st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
283
+ b1, b2 = st.columns(2)
284
+ b1.download_button("Markdown", answer,
285
  file_name=f"{prompt[:30]}.md", mime="text/markdown")
286
+ b2.download_button("HTML", md_to_html(answer, prompt[:30]),
 
287
  file_name=f"{prompt[:30]}.html", mime="text/html")
288
 
289
+ # ── μžλ™ λ°±μ—… μ €μž₯
290
  if st.session_state.auto_save and st.session_state.messages:
291
  try:
292
+ fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
293
+ with open(fn, "w", encoding="utf-8") as fp:
294
+ json.dump(st.session_state.messages, fp,
295
+ ensure_ascii=False, indent=2)
296
  except Exception as e:
297
+ logging.error(f"μžλ™ μ €μž₯ μ‹€νŒ¨: {e}")
298
 
299
+ # ──────────────────────────────── main / requirements ──────────────────────
300
+ def main(): ginigen_app()
 
301
 
302
  if __name__ == "__main__":
303
+ # requirements.txt 동적 생성
304
  with open("requirements.txt", "w") as f:
305
  f.write("\n".join([
306
  "streamlit>=1.31.0",