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()