ginipick commited on
Commit
435ea16
Β·
verified Β·
1 Parent(s): c696d7f

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1104 -0
app.py ADDED
@@ -0,0 +1,1104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, json, re, logging, requests, markdown, time, io
2
+ from datetime import datetime
3
+ import random
4
+ import base64
5
+ from io import BytesIO
6
+ from PIL import Image
7
+
8
+ import streamlit as st
9
+ from openai import OpenAI # OpenAI 라이브러리
10
+
11
+ from gradio_client import Client
12
+ import pandas as pd
13
+ import PyPDF2 # For handling PDF files
14
+
15
+ # ──────────────────────────────── Environment Variables / Constants ─────────────────────────
16
+ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
17
+ BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Keep this name
18
+ BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
19
+ BRAVE_IMAGE_ENDPOINT = "https://api.search.brave.com/res/v1/images/search"
20
+ BRAVE_VIDEO_ENDPOINT = "https://api.search.brave.com/res/v1/videos/search"
21
+ BRAVE_NEWS_ENDPOINT = "https://api.search.brave.com/res/v1/news/search"
22
+ IMAGE_API_URL = "http://211.233.58.201:7896"
23
+ MAX_TOKENS = 7999
24
+
25
+ # Brave Search modes and style definitions (in English)
26
+ SEARCH_MODES = {
27
+ "comprehensive": "Comprehensive answer with multiple sources",
28
+ "academic": "Academic and research-focused results",
29
+ "news": "Latest news and current events",
30
+ "technical": "Technical and specialized information",
31
+ "educational": "Educational and learning resources"
32
+ }
33
+
34
+ RESPONSE_STYLES = {
35
+ "professional": "Professional and formal tone",
36
+ "casual": "Friendly and conversational tone",
37
+ "simple": "Simple and easy to understand",
38
+ "detailed": "Detailed and thorough explanations"
39
+ }
40
+
41
+ # Example search queries
42
+ EXAMPLE_QUERIES = {
43
+ "example1": "What are the latest developments in quantum computing?",
44
+ "example2": "How does climate change affect biodiversity in tropical rainforests?",
45
+ "example3": "What are the economic implications of artificial intelligence in the job market?"
46
+ }
47
+
48
+ # ──────────────────────────────── Logging ────────────────────────────────
49
+ logging.basicConfig(level=logging.INFO,
50
+ format="%(asctime)s - %(levelname)s - %(message)s")
51
+
52
+ # ──────────────────────────────── OpenAI Client ──────────────────────────
53
+
54
+ # OpenAI ν΄λΌμ΄μ–ΈνŠΈμ— νƒ€μž„μ•„μ›ƒκ³Ό μž¬μ‹œλ„ 둜직 μΆ”κ°€
55
+ @st.cache_resource
56
+ def get_openai_client():
57
+ """Create an OpenAI client with timeout and retry settings."""
58
+ if not OPENAI_API_KEY:
59
+ raise RuntimeError("⚠️ OPENAI_API_KEY ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
60
+ return OpenAI(
61
+ api_key=OPENAI_API_KEY,
62
+ timeout=60.0, # νƒ€μž„μ•„μ›ƒ 60초둜 μ„€μ •
63
+ max_retries=3 # μž¬μ‹œλ„ 횟수 3회둜 μ„€μ •
64
+ )
65
+
66
+ # ──────────────────────────────── System Prompt ─────────────────────────
67
+ def get_system_prompt(mode="comprehensive", style="professional", include_search_results=True, include_uploaded_files=False) -> str:
68
+ """
69
+ Generate a system prompt for the perplexity-like interface based on:
70
+ - The selected search mode and style
71
+ - Guidelines for using web search results and uploaded files
72
+ """
73
+
74
+ # Base prompt for comprehensive mode
75
+ comprehensive_prompt = """
76
+ You are an advanced AI assistant that provides comprehensive answers with multiple sources, similar to Perplexity.
77
+
78
+ Your task is to:
79
+ 1. Thoroughly analyze the user's query
80
+ 2. Provide a clear, well-structured answer integrating information from multiple sources
81
+ 3. Include relevant images, videos, and links in your response
82
+ 4. Format your answer with proper headings, bullet points, and sections
83
+ 5. Cite sources inline and provide a references section at the end
84
+
85
+ Important guidelines:
86
+ - Organize information logically with clear section headings
87
+ - Use bullet points and numbered lists for clarity
88
+ - Include specific, factual information whenever possible
89
+ - Provide balanced perspectives on controversial topics
90
+ - Display relevant statistics, data, or quotes when appropriate
91
+ - Format your response using markdown for readability
92
+ """
93
+
94
+ # Alternative modes
95
+ mode_prompts = {
96
+ "academic": """
97
+ Your focus is on providing academic and research-focused responses:
98
+ - Prioritize peer-reviewed research and academic sources
99
+ - Include citations in a formal academic format
100
+ - Discuss methodologies and research limitations where relevant
101
+ - Present different scholarly perspectives on the topic
102
+ - Use precise, technical language appropriate for an academic audience
103
+ """,
104
+ "news": """
105
+ Your focus is on providing the latest news and current events:
106
+ - Prioritize recent news articles and current information
107
+ - Include publication dates for all news sources
108
+ - Present multiple perspectives from different news outlets
109
+ - Distinguish between facts and opinions/editorial content
110
+ - Update information with the most recent developments
111
+ """,
112
+ "technical": """
113
+ Your focus is on providing technical and specialized information:
114
+ - Use precise technical terminology appropriate to the field
115
+ - Include code snippets, formulas, or technical diagrams where relevant
116
+ - Break down complex concepts into step-by-step explanations
117
+ - Reference technical documentation, standards, and best practices
118
+ - Consider different technical approaches or methodologies
119
+ """,
120
+ "educational": """
121
+ Your focus is on providing educational and learning resources:
122
+ - Structure information in a learning-friendly progression
123
+ - Include examples, analogies, and visual explanations
124
+ - Highlight key concepts and definitions
125
+ - Suggest further learning resources at different difficulty levels
126
+ - Present information that's accessible to learners at various levels
127
+ """
128
+ }
129
+
130
+ # Response styles
131
+ style_guides = {
132
+ "professional": "Use a professional, authoritative voice. Clearly explain technical terms and present data systematically.",
133
+ "casual": "Use a relaxed, conversational style with a friendly tone. Include relatable examples and occasionally use informal expressions.",
134
+ "simple": "Use straightforward language and avoid jargon. Keep sentences and paragraphs short. Explain concepts as if to someone with no background in the subject.",
135
+ "detailed": "Provide thorough explanations with comprehensive background information. Explore nuances and edge cases. Present multiple perspectives and detailed analysis."
136
+ }
137
+
138
+ # Guidelines for using search results
139
+ search_guide = """
140
+ Guidelines for Using Search Results:
141
+ - Include source links directly in your response using markdown: [Source Name](URL)
142
+ - For each major claim or piece of information, indicate its source
143
+ - If sources conflict, explain the different perspectives and their reliability
144
+ - Include relevant images by writing: ![Image description](image_url)
145
+ - Include relevant video links when appropriate by writing: [Video: Title](video_url)
146
+ - Format search information into a cohesive, well-structured response
147
+ - Include a "References" section at the end listing all major sources with links
148
+ """
149
+
150
+ # Guidelines for using uploaded files
151
+ upload_guide = """
152
+ Guidelines for Using Uploaded Files:
153
+ - Treat the uploaded files as primary sources for your response
154
+ - Extract and highlight key information from files that directly addresses the query
155
+ - Quote relevant passages and cite the specific file
156
+ - For numerical data in CSV files, consider creating summary statements
157
+ - For PDF content, reference specific sections or pages
158
+ - Integrate file information seamlessly with web search results
159
+ - When information conflicts, prioritize file content over general web results
160
+ """
161
+
162
+ # Choose base prompt based on mode
163
+ if mode == "comprehensive":
164
+ final_prompt = comprehensive_prompt
165
+ else:
166
+ final_prompt = comprehensive_prompt + "\n" + mode_prompts.get(mode, "")
167
+
168
+ # Add style guide
169
+ if style in style_guides:
170
+ final_prompt += f"\n\nTone and Style: {style_guides[style]}"
171
+
172
+ # Add search results guidance
173
+ if include_search_results:
174
+ final_prompt += f"\n\n{search_guide}"
175
+
176
+ # Add uploaded files guidance
177
+ if include_uploaded_files:
178
+ final_prompt += f"\n\n{upload_guide}"
179
+
180
+ # Additional formatting instructions
181
+ final_prompt += """
182
+ \n\nAdditional Formatting Requirements:
183
+ - Use markdown headings (## and ###) to organize your response
184
+ - Use bold text (**text**) for emphasis on important points
185
+ - Include a "Related Questions" section at the end with 3-5 follow-up questions
186
+ - Format your response with proper spacing and paragraph breaks
187
+ - Make all links clickable by using proper markdown format: [text](url)
188
+ """
189
+
190
+ return final_prompt
191
+
192
+ # ──────────────────────────────── Brave Search API ────────────────────────
193
+ @st.cache_data(ttl=3600)
194
+ def brave_search(query: str, count: int = 20):
195
+ """
196
+ Call the Brave Web Search API β†’ list[dict]
197
+ Returns fields: index, title, link, snippet, displayed_link
198
+ """
199
+ if not BRAVE_KEY:
200
+ raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) environment variable is empty.")
201
+
202
+ headers = {
203
+ "Accept": "application/json",
204
+ "Accept-Encoding": "gzip",
205
+ "X-Subscription-Token": BRAVE_KEY
206
+ }
207
+ params = {"q": query, "count": str(count)}
208
+
209
+ for attempt in range(3):
210
+ try:
211
+ r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
212
+ r.raise_for_status()
213
+ data = r.json()
214
+
215
+ logging.info(f"Brave search result data structure: {list(data.keys())}")
216
+
217
+ raw = data.get("web", {}).get("results") or data.get("results", [])
218
+ if not raw:
219
+ logging.warning(f"No Brave search results found. Response: {data}")
220
+ raise ValueError("No search results found.")
221
+
222
+ arts = []
223
+ for i, res in enumerate(raw[:count], 1):
224
+ url = res.get("url", res.get("link", ""))
225
+ host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
226
+ arts.append({
227
+ "index": i,
228
+ "title": res.get("title", "No title"),
229
+ "link": url,
230
+ "snippet": res.get("description", res.get("text", "No snippet")),
231
+ "displayed_link": host
232
+ })
233
+
234
+ logging.info(f"Brave search success: {len(arts)} results")
235
+ return arts
236
+
237
+ except Exception as e:
238
+ logging.error(f"Brave search failure (attempt {attempt+1}/3): {e}")
239
+ if attempt < 2:
240
+ time.sleep(2)
241
+
242
+ return []
243
+
244
+ @st.cache_data(ttl=3600)
245
+ def brave_image_search(query: str, count: int = 10):
246
+ """
247
+ Call the Brave Image Search API β†’ list[dict]
248
+ Returns fields: index, title, image_url, source_url
249
+ """
250
+ if not BRAVE_KEY:
251
+ raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) environment variable is empty.")
252
+
253
+ headers = {
254
+ "Accept": "application/json",
255
+ "Accept-Encoding": "gzip",
256
+ "X-Subscription-Token": BRAVE_KEY
257
+ }
258
+ params = {
259
+ "q": query,
260
+ "count": str(count),
261
+ "search_lang": "en",
262
+ "country": "us",
263
+ "spellcheck": "1"
264
+ }
265
+
266
+ for attempt in range(3):
267
+ try:
268
+ r = requests.get(BRAVE_IMAGE_ENDPOINT, headers=headers, params=params, timeout=15)
269
+ r.raise_for_status()
270
+ data = r.json()
271
+
272
+ results = []
273
+ for i, img in enumerate(data.get("results", [])[:count], 1):
274
+ results.append({
275
+ "index": i,
276
+ "title": img.get("title", "Image"),
277
+ "image_url": img.get("image", {}).get("url", ""),
278
+ "source_url": img.get("source", ""),
279
+ "width": img.get("image", {}).get("width", 0),
280
+ "height": img.get("image", {}).get("height", 0)
281
+ })
282
+
283
+ logging.info(f"Brave image search success: {len(results)} results")
284
+ return results
285
+
286
+ except Exception as e:
287
+ logging.error(f"Brave image search failure (attempt {attempt+1}/3): {e}")
288
+ if attempt < 2:
289
+ time.sleep(2)
290
+
291
+ return []
292
+
293
+ @st.cache_data(ttl=3600)
294
+ def brave_video_search(query: str, count: int = 5):
295
+ """
296
+ Call the Brave Video Search API β†’ list[dict]
297
+ Returns fields: index, title, video_url, thumbnail_url, source
298
+ """
299
+ if not BRAVE_KEY:
300
+ raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) environment variable is empty.")
301
+
302
+ headers = {
303
+ "Accept": "application/json",
304
+ "Accept-Encoding": "gzip",
305
+ "X-Subscription-Token": BRAVE_KEY
306
+ }
307
+ params = {
308
+ "q": query,
309
+ "count": str(count)
310
+ }
311
+
312
+ for attempt in range(3):
313
+ try:
314
+ r = requests.get(BRAVE_VIDEO_ENDPOINT, headers=headers, params=params, timeout=15)
315
+ r.raise_for_status()
316
+ data = r.json()
317
+
318
+ results = []
319
+ for i, vid in enumerate(data.get("results", [])[:count], 1):
320
+ results.append({
321
+ "index": i,
322
+ "title": vid.get("title", "Video"),
323
+ "video_url": vid.get("url", ""),
324
+ "thumbnail_url": vid.get("thumbnail", {}).get("src", ""),
325
+ "source": vid.get("provider", {}).get("name", "Unknown source")
326
+ })
327
+
328
+ logging.info(f"Brave video search success: {len(results)} results")
329
+ return results
330
+
331
+ except Exception as e:
332
+ logging.error(f"Brave video search failure (attempt {attempt+1}/3): {e}")
333
+ if attempt < 2:
334
+ time.sleep(2)
335
+
336
+ return []
337
+
338
+ @st.cache_data(ttl=3600)
339
+ def brave_news_search(query: str, count: int = 5):
340
+ """
341
+ Call the Brave News Search API β†’ list[dict]
342
+ Returns fields: index, title, url, description, source, date
343
+ """
344
+ if not BRAVE_KEY:
345
+ raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) environment variable is empty.")
346
+
347
+ headers = {
348
+ "Accept": "application/json",
349
+ "Accept-Encoding": "gzip",
350
+ "X-Subscription-Token": BRAVE_KEY
351
+ }
352
+ params = {
353
+ "q": query,
354
+ "count": str(count)
355
+ }
356
+
357
+ for attempt in range(3):
358
+ try:
359
+ r = requests.get(BRAVE_NEWS_ENDPOINT, headers=headers, params=params, timeout=15)
360
+ r.raise_for_status()
361
+ data = r.json()
362
+
363
+ results = []
364
+ for i, news in enumerate(data.get("results", [])[:count], 1):
365
+ results.append({
366
+ "index": i,
367
+ "title": news.get("title", "News article"),
368
+ "url": news.get("url", ""),
369
+ "description": news.get("description", ""),
370
+ "source": news.get("source", "Unknown source"),
371
+ "date": news.get("age", "Unknown date")
372
+ })
373
+
374
+ logging.info(f"Brave news search success: {len(results)} results")
375
+ return results
376
+
377
+ except Exception as e:
378
+ logging.error(f"Brave news search failure (attempt {attempt+1}/3): {e}")
379
+ if attempt < 2:
380
+ time.sleep(2)
381
+
382
+ return []
383
+
384
+ def mock_results(query: str) -> str:
385
+ """Fallback search results if API fails or returns empty."""
386
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
387
+ return (f"# Fallback Search Content (Generated: {ts})\n\n"
388
+ f"The search API request failed or returned no results for '{query}'. "
389
+ f"Please generate a response based on any pre-existing knowledge.\n\n"
390
+ f"Consider these points:\n\n"
391
+ f"- Basic concepts and importance of {query}\n"
392
+ f"- Commonly known related statistics or trends\n"
393
+ f"- Typical expert opinions on this subject\n"
394
+ f"- Questions that readers might have\n\n"
395
+ f"Note: This is fallback guidance, not real-time data.\n\n")
396
+
397
+ def do_web_search(query: str) -> str:
398
+ """Perform web search and format the results."""
399
+ try:
400
+ # Web search
401
+ arts = brave_search(query, 20)
402
+ if not arts:
403
+ logging.warning("No search results, using fallback content")
404
+ return mock_results(query)
405
+
406
+ # Image search
407
+ images = brave_image_search(query, 5)
408
+
409
+ # Video search
410
+ videos = brave_video_search(query, 2)
411
+
412
+ # News search
413
+ news = brave_news_search(query, 3)
414
+
415
+ # Format all results
416
+ result = "# Web Search Results\nUse these results to provide a comprehensive answer with multiple sources. Include relevant images, videos, and links.\n\n"
417
+
418
+ # Add web results
419
+ result += "## Web Results\n\n"
420
+ for a in arts[:10]: # Limit to top 10 results
421
+ result += f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
422
+ result += f"**Source**: [{a['displayed_link']}]({a['link']})\n\n---\n"
423
+
424
+ # Add image results if available
425
+ if images:
426
+ result += "## Image Results\n\n"
427
+ for img in images:
428
+ if img.get('image_url'):
429
+ result += f"![{img['title']}]({img['image_url']})\n\n"
430
+ result += f"**Source**: [{img.get('source_url', 'Image source')}]({img.get('source_url', '#')})\n\n"
431
+
432
+ # Add video results if available
433
+ if videos:
434
+ result += "## Video Results\n\n"
435
+ for vid in videos:
436
+ result += f"### {vid['title']}\n\n"
437
+ if vid.get('thumbnail_url'):
438
+ result += f"![Thumbnail]({vid['thumbnail_url']})\n\n"
439
+ result += f"**Watch**: [{vid['source']}]({vid['video_url']})\n\n"
440
+
441
+ # Add news results if available
442
+ if news:
443
+ result += "## News Results\n\n"
444
+ for n in news:
445
+ result += f"### {n['title']}\n\n{n['description']}\n\n"
446
+ result += f"**Source**: [{n['source']}]({n['url']}) - {n['date']}\n\n---\n"
447
+
448
+ return result
449
+
450
+ except Exception as e:
451
+ logging.error(f"Web search process failed: {str(e)}")
452
+ return mock_results(query)
453
+
454
+ # ──────────────────────────────── File Upload Handling ─────────────────────
455
+ def process_text_file(file):
456
+ """Handle text file"""
457
+ try:
458
+ content = file.read()
459
+ file.seek(0)
460
+
461
+ text = content.decode('utf-8', errors='ignore')
462
+ if len(text) > 10000:
463
+ text = text[:9700] + "...(truncated)..."
464
+
465
+ result = f"## Text File: {file.name}\n\n"
466
+ result += text
467
+ return result
468
+ except Exception as e:
469
+ logging.error(f"Error processing text file: {str(e)}")
470
+ return f"Error processing text file: {str(e)}"
471
+
472
+ def process_csv_file(file):
473
+ """Handle CSV file"""
474
+ try:
475
+ content = file.read()
476
+ file.seek(0)
477
+
478
+ df = pd.read_csv(io.BytesIO(content))
479
+ result = f"## CSV File: {file.name}\n\n"
480
+ result += f"- Rows: {len(df)}\n"
481
+ result += f"- Columns: {len(df.columns)}\n"
482
+ result += f"- Column Names: {', '.join(df.columns.tolist())}\n\n"
483
+
484
+ result += "### Data Preview\n\n"
485
+ preview_df = df.head(10)
486
+ try:
487
+ markdown_table = preview_df.to_markdown(index=False)
488
+ if markdown_table:
489
+ result += markdown_table + "\n\n"
490
+ else:
491
+ result += "Unable to display CSV data.\n\n"
492
+ except Exception as e:
493
+ logging.error(f"Markdown table conversion error: {e}")
494
+ result += "Displaying data as text:\n\n"
495
+ result += str(preview_df) + "\n\n"
496
+
497
+ num_cols = df.select_dtypes(include=['number']).columns
498
+ if len(num_cols) > 0:
499
+ result += "### Basic Statistical Information\n\n"
500
+ try:
501
+ stats_df = df[num_cols].describe().round(2)
502
+ stats_markdown = stats_df.to_markdown()
503
+ if stats_markdown:
504
+ result += stats_markdown + "\n\n"
505
+ else:
506
+ result += "Unable to display statistical information.\n\n"
507
+ except Exception as e:
508
+ logging.error(f"Statistical info conversion error: {e}")
509
+ result += "Unable to generate statistical information.\n\n"
510
+
511
+ return result
512
+ except Exception as e:
513
+ logging.error(f"CSV file processing error: {str(e)}")
514
+ return f"Error processing CSV file: {str(e)}"
515
+
516
+ def process_pdf_file(file):
517
+ """Handle PDF file"""
518
+ try:
519
+ # Read file in bytes
520
+ file_bytes = file.read()
521
+ file.seek(0)
522
+
523
+ # Use PyPDF2
524
+ pdf_file = io.BytesIO(file_bytes)
525
+ reader = PyPDF2.PdfReader(pdf_file, strict=False)
526
+
527
+ # Basic info
528
+ result = f"## PDF File: {file.name}\n\n"
529
+ result += f"- Total pages: {len(reader.pages)}\n\n"
530
+
531
+ # Extract text by page (limit to first 5 pages)
532
+ max_pages = min(5, len(reader.pages))
533
+ all_text = ""
534
+
535
+ for i in range(max_pages):
536
+ try:
537
+ page = reader.pages[i]
538
+ page_text = page.extract_text()
539
+
540
+ current_page_text = f"### Page {i+1}\n\n"
541
+ if page_text and len(page_text.strip()) > 0:
542
+ # Limit to 1500 characters per page
543
+ if len(page_text) > 1500:
544
+ current_page_text += page_text[:1500] + "...(truncated)...\n\n"
545
+ else:
546
+ current_page_text += page_text + "\n\n"
547
+ else:
548
+ current_page_text += "(No text could be extracted from this page)\n\n"
549
+
550
+ all_text += current_page_text
551
+
552
+ # If total text is too long, break
553
+ if len(all_text) > 8000:
554
+ all_text += "...(truncating remaining pages; PDF is too large)...\n\n"
555
+ break
556
+
557
+ except Exception as page_err:
558
+ logging.error(f"Error processing PDF page {i+1}: {str(page_err)}")
559
+ all_text += f"### Page {i+1}\n\n(Error extracting content: {str(page_err)})\n\n"
560
+
561
+ if len(reader.pages) > max_pages:
562
+ all_text += f"\nNote: Only the first {max_pages} pages are shown out of {len(reader.pages)} total.\n\n"
563
+
564
+ result += "### PDF Content\n\n" + all_text
565
+ return result
566
+
567
+ except Exception as e:
568
+ logging.error(f"PDF file processing error: {str(e)}")
569
+ return f"## PDF File: {file.name}\n\nError occurred: {str(e)}\n\nThis PDF file cannot be processed."
570
+
571
+ def process_uploaded_files(files):
572
+ """Combine the contents of all uploaded files into one string."""
573
+ if not files:
574
+ return None
575
+
576
+ result = "# Uploaded File Contents\n\n"
577
+ result += "Below is the content from the files provided by the user. Integrate this data as a main source of information for your response.\n\n"
578
+
579
+ for file in files:
580
+ try:
581
+ ext = file.name.split('.')[-1].lower()
582
+ if ext == 'txt':
583
+ result += process_text_file(file) + "\n\n---\n\n"
584
+ elif ext == 'csv':
585
+ result += process_csv_file(file) + "\n\n---\n\n"
586
+ elif ext == 'pdf':
587
+ result += process_pdf_file(file) + "\n\n---\n\n"
588
+ else:
589
+ result += f"### Unsupported File: {file.name}\n\n---\n\n"
590
+ except Exception as e:
591
+ logging.error(f"File processing error {file.name}: {e}")
592
+ result += f"### File processing error: {file.name}\n\nError: {e}\n\n---\n\n"
593
+
594
+ return result
595
+
596
+ # ──────────────────────────────── Image & Utility ─────────────────────────
597
+ def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
598
+ """Image generation function (via Gradio endpoint)."""
599
+ if not prompt:
600
+ return None, "Insufficient prompt"
601
+ try:
602
+ res = Client(IMAGE_API_URL).predict(
603
+ prompt=prompt, width=w, height=h, guidance=g,
604
+ inference_steps=steps, seed=seed,
605
+ do_img2img=False, init_image=None,
606
+ image2image_strength=0.8, resize_img=True,
607
+ api_name="/generate_image"
608
+ )
609
+ return res[0], f"Seed: {res[1]}"
610
+ except Exception as e:
611
+ logging.error(e)
612
+ return None, str(e)
613
+
614
+ def extract_image_prompt(response_text: str, topic: str):
615
+ """
616
+ Generate a single-line English image prompt from the response content.
617
+ """
618
+ client = get_openai_client()
619
+ try:
620
+ response = client.chat.completions.create(
621
+ model="gpt-4.1-mini",
622
+ messages=[
623
+ {"role": "system", "content": "Generate a single-line English image prompt from the following text. Return only the prompt text, nothing else."},
624
+ {"role": "user", "content": f"Topic: {topic}\n\n---\n{response_text}\n\n---"}
625
+ ],
626
+ temperature=1,
627
+ max_tokens=80,
628
+ top_p=1
629
+ )
630
+
631
+ return response.choices[0].message.content.strip()
632
+ except Exception as e:
633
+ logging.error(f"OpenAI image prompt generation error: {e}")
634
+ return f"A professional photo related to {topic}, high quality"
635
+
636
+ def md_to_html(md: str, title="Perplexity-like Response"):
637
+ """Convert Markdown to HTML."""
638
+ return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
639
+
640
+ def keywords(text: str, top=5):
641
+ """Simple keyword extraction for query. Returns first N words (roughly)."""
642
+ cleaned = re.sub(r"[^κ°€-힣a-zA-Z0-9\s]", "", text)
643
+ return " ".join(cleaned.split()[:top])
644
+
645
+ # ──────────────────────────────── Streamlit UI ────────────────────────────
646
+ def perplexity_app():
647
+ st.title("Perplexity-like AI Assistant")
648
+
649
+ # Set default session state
650
+ if "ai_model" not in st.session_state:
651
+ st.session_state.ai_model = "gpt-4.1-mini" # κ³ μ • λͺ¨λΈ μ„€μ •
652
+ if "messages" not in st.session_state:
653
+ st.session_state.messages = []
654
+ if "auto_save" not in st.session_state:
655
+ st.session_state.auto_save = True
656
+ if "generate_image" not in st.session_state:
657
+ st.session_state.generate_image = False
658
+ if "web_search_enabled" not in st.session_state:
659
+ st.session_state.web_search_enabled = True
660
+ if "search_mode" not in st.session_state:
661
+ st.session_state.search_mode = "comprehensive"
662
+ if "response_style" not in st.session_state:
663
+ st.session_state.response_style = "professional"
664
+
665
+ # Sidebar UI
666
+ sb = st.sidebar
667
+ sb.title("Search Settings")
668
+
669
+ sb.subheader("Response Configuration")
670
+ sb.selectbox(
671
+ "Search Mode",
672
+ options=list(SEARCH_MODES.keys()),
673
+ format_func=lambda x: SEARCH_MODES[x],
674
+ key="search_mode"
675
+ )
676
+
677
+ sb.selectbox(
678
+ "Response Style",
679
+ options=list(RESPONSE_STYLES.keys()),
680
+ format_func=lambda x: RESPONSE_STYLES[x],
681
+ key="response_style"
682
+ )
683
+
684
+ # Example queries
685
+ sb.subheader("Example Queries")
686
+ c1, c2, c3 = sb.columns(3)
687
+ if c1.button("Quantum Computing", key="ex1"):
688
+ process_example(EXAMPLE_QUERIES["example1"])
689
+ if c2.button("Climate Change", key="ex2"):
690
+ process_example(EXAMPLE_QUERIES["example2"])
691
+ if c3.button("AI Economics", key="ex3"):
692
+ process_example(EXAMPLE_QUERIES["example3"])
693
+
694
+ sb.subheader("Other Settings")
695
+ sb.toggle("Auto Save", key="auto_save")
696
+ sb.toggle("Auto Image Generation", key="generate_image")
697
+
698
+ web_search_enabled = sb.toggle("Use Web Search", value=st.session_state.web_search_enabled)
699
+ st.session_state.web_search_enabled = web_search_enabled
700
+
701
+ if web_search_enabled:
702
+ st.sidebar.info("βœ… Web search results will be integrated into the response.")
703
+
704
+ # Download the latest response
705
+ latest_response = next(
706
+ (m["content"] for m in reversed(st.session_state.messages)
707
+ if m["role"] == "assistant" and m["content"].strip()),
708
+ None
709
+ )
710
+ if latest_response:
711
+ # Extract a title from the response - first heading or first line
712
+ title_match = re.search(r"# (.*?)(\n|$)", latest_response)
713
+ if title_match:
714
+ title = title_match.group(1).strip()
715
+ else:
716
+ first_line = latest_response.split('\n', 1)[0].strip()
717
+ title = first_line[:40] + "..." if len(first_line) > 40 else first_line
718
+
719
+ sb.subheader("Download Latest Response")
720
+ d1, d2 = sb.columns(2)
721
+ d1.download_button("Download as Markdown", latest_response,
722
+ file_name=f"{title}.md", mime="text/markdown")
723
+ d2.download_button("Download as HTML", md_to_html(latest_response, title),
724
+ file_name=f"{title}.html", mime="text/html")
725
+
726
+ # JSON conversation record upload
727
+ up = sb.file_uploader("Load Conversation History (.json)", type=["json"], key="json_uploader")
728
+ if up:
729
+ try:
730
+ st.session_state.messages = json.load(up)
731
+ sb.success("Conversation history loaded successfully")
732
+ except Exception as e:
733
+ sb.error(f"Failed to load: {e}")
734
+
735
+ # JSON conversation record download
736
+ if sb.button("Download Conversation as JSON"):
737
+ sb.download_button(
738
+ "Save",
739
+ data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2),
740
+ file_name="conversation_history.json",
741
+ mime="application/json"
742
+ )
743
+
744
+ # File Upload
745
+ st.subheader("Upload Files")
746
+ uploaded_files = st.file_uploader(
747
+ "Upload files to be used as reference (txt, csv, pdf)",
748
+ type=["txt", "csv", "pdf"],
749
+ accept_multiple_files=True,
750
+ key="file_uploader"
751
+ )
752
+
753
+ if uploaded_files:
754
+ file_count = len(uploaded_files)
755
+ st.success(f"{file_count} files uploaded. They will be used as sources for your query.")
756
+
757
+ with st.expander("Preview Uploaded Files", expanded=False):
758
+ for idx, file in enumerate(uploaded_files):
759
+ st.write(f"**File Name:** {file.name}")
760
+ ext = file.name.split('.')[-1].lower()
761
+
762
+ if ext == 'txt':
763
+ preview = file.read(1000).decode('utf-8', errors='ignore')
764
+ file.seek(0)
765
+ st.text_area(
766
+ f"Preview of {file.name}",
767
+ preview + ("..." if len(preview) >= 1000 else ""),
768
+ height=150
769
+ )
770
+ elif ext == 'csv':
771
+ try:
772
+ df = pd.read_csv(file)
773
+ file.seek(0)
774
+ st.write("CSV Preview (up to 5 rows)")
775
+ st.dataframe(df.head(5))
776
+ except Exception as e:
777
+ st.error(f"CSV preview failed: {e}")
778
+ elif ext == 'pdf':
779
+ try:
780
+ file_bytes = file.read()
781
+ file.seek(0)
782
+
783
+ pdf_file = io.BytesIO(file_bytes)
784
+ reader = PyPDF2.PdfReader(pdf_file, strict=False)
785
+
786
+ pc = len(reader.pages)
787
+ st.write(f"PDF File: {pc} pages")
788
+
789
+ if pc > 0:
790
+ try:
791
+ page_text = reader.pages[0].extract_text()
792
+ preview = page_text[:500] if page_text else "(No text extracted)"
793
+ st.text_area("Preview of the first page", preview + "...", height=150)
794
+ except:
795
+ st.warning("Failed to extract text from the first page")
796
+ except Exception as e:
797
+ st.error(f"PDF preview failed: {e}")
798
+
799
+ if idx < file_count - 1:
800
+ st.divider()
801
+
802
+ # Display existing messages
803
+ for m in st.session_state.messages:
804
+ with st.chat_message(m["role"]):
805
+ st.markdown(m["content"], unsafe_allow_html=True)
806
+
807
+ # Display images if present in the message
808
+ if "images" in m and m["images"]:
809
+ st.subheader("Related Images")
810
+ cols = st.columns(min(3, len(m["images"])))
811
+ for i, img_data in enumerate(m["images"]):
812
+ col_idx = i % len(cols)
813
+ with cols[col_idx]:
814
+ try:
815
+ img_url = img_data.get('url', '')
816
+ caption = img_data.get('title', 'Related image')
817
+ if img_url:
818
+ st.image(img_url, caption=caption, use_column_width=True)
819
+ if img_data.get('source'):
820
+ st.markdown(f"[Source]({img_data['source']})")
821
+ except Exception as img_err:
822
+ st.warning(f"Could not display image: {img_err}")
823
+
824
+ # Display videos if present
825
+ if "videos" in m and m["videos"]:
826
+ st.subheader("Related Videos")
827
+ for video in m["videos"]:
828
+ video_title = video.get('title', 'Related video')
829
+ video_url = video.get('url', '')
830
+ thumbnail = video.get('thumbnail', '')
831
+
832
+ # Display video with thumbnail if available
833
+ if thumbnail:
834
+ col1, col2 = st.columns([1, 3])
835
+ with col1:
836
+ try:
837
+ st.image(thumbnail, width=120)
838
+ except:
839
+ st.write("🎬")
840
+ with col2:
841
+ st.markdown(f"**[{video_title}]({video_url})**")
842
+ st.write(f"Source: {video.get('source', 'Unknown')}")
843
+ else:
844
+ st.markdown(f"🎬 **[{video_title}]({video_url})**")
845
+ st.write(f"Source: {video.get('source', 'Unknown')}")
846
+
847
+ # User input
848
+ query = st.chat_input("Enter your query or question here.")
849
+ if query:
850
+ process_input(query, uploaded_files)
851
+
852
+ # μ‚¬μ΄λ“œλ°” ν•˜λ‹¨ λ°°μ§€(링크) μΆ”κ°€
853
+ sb.markdown("---")
854
+ sb.markdown("Created by [https://ginigen.com](https://ginigen.com) | [YouTube Channel](https://www.youtube.com/@ginipickaistudio)")
855
+
856
+ def process_example(topic):
857
+ """Process the selected example query."""
858
+ process_input(topic, [])
859
+
860
+ def process_input(query: str, uploaded_files):
861
+ # Add user's message
862
+ if not any(m["role"] == "user" and m["content"] == query for m in st.session_state.messages):
863
+ st.session_state.messages.append({"role": "user", "content": query})
864
+
865
+ with st.chat_message("user"):
866
+ st.markdown(query)
867
+
868
+ with st.chat_message("assistant"):
869
+ placeholder = st.empty()
870
+ message_placeholder = st.empty()
871
+ full_response = ""
872
+
873
+ use_web_search = st.session_state.web_search_enabled
874
+ has_uploaded_files = bool(uploaded_files) and len(uploaded_files) > 0
875
+
876
+ try:
877
+ # μƒνƒœ ν‘œμ‹œ
878
+ status = st.status("Preparing to answer your query...")
879
+ status.update(label="Initializing client...")
880
+
881
+ client = get_openai_client()
882
+
883
+ # Web search
884
+ search_content = None
885
+ image_results = []
886
+ video_results = []
887
+ news_results = []
888
+
889
+ if use_web_search:
890
+ status.update(label="Performing web search...")
891
+ with st.spinner("Searching the web..."):
892
+ search_content = do_web_search(keywords(query, top=5))
893
+
894
+ # Perform specific searches for images, videos, news
895
+ try:
896
+ status.update(label="Finding images and videos...")
897
+ image_results = brave_image_search(query, 5)
898
+ video_results = brave_video_search(query, 2)
899
+ news_results = brave_news_search(query, 3)
900
+ except Exception as search_err:
901
+ logging.error(f"Media search error: {search_err}")
902
+
903
+ # Process uploaded files β†’ content
904
+ file_content = None
905
+ if has_uploaded_files:
906
+ status.update(label="Processing uploaded files...")
907
+ with st.spinner("Analyzing files..."):
908
+ file_content = process_uploaded_files(uploaded_files)
909
+
910
+ # μ΅œμ’…μ μœΌλ‘œ μ‚¬μš©ν•  이미지/λΉ„λ””μ˜€ λͺ©λ‘ ꡬ성
911
+ # (μ΄λ²ˆμ—λŠ” "fallback" 없이 Brave 검색 결과만 μ‚¬μš©)
912
+ valid_images = []
913
+ for img in image_results:
914
+ url = img.get('image_url')
915
+ if url and url.startswith('http'):
916
+ valid_images.append({
917
+ 'url': url,
918
+ 'title': img.get('title', f"Related to: {query}"),
919
+ 'source': img.get('source_url', '')
920
+ })
921
+
922
+ valid_videos = []
923
+ for vid in video_results:
924
+ url = vid.get('video_url')
925
+ if url and url.startswith('http'):
926
+ valid_videos.append({
927
+ 'url': url,
928
+ 'title': vid.get('title', 'Video'),
929
+ 'thumbnail': vid.get('thumbnail_url', ''),
930
+ 'source': vid.get('source', 'Video source')
931
+ })
932
+
933
+ # Build system prompt
934
+ status.update(label="Preparing comprehensive answer...")
935
+ sys_prompt = get_system_prompt(
936
+ mode=st.session_state.search_mode,
937
+ style=st.session_state.response_style,
938
+ include_search_results=use_web_search,
939
+ include_uploaded_files=has_uploaded_files
940
+ )
941
+
942
+ # λ©”μ‹œμ§€ ꡬ성
943
+ api_messages = [
944
+ {"role": "system", "content": sys_prompt}
945
+ ]
946
+
947
+ user_content = query
948
+
949
+ # 검색 κ²°κ³Ό μΆ”κ°€
950
+ if search_content:
951
+ user_content += "\n\n" + search_content
952
+
953
+ # 파일 λ‚΄μš© μΆ”κ°€
954
+ if file_content:
955
+ user_content += "\n\n" + file_content
956
+
957
+ # 이미지/λΉ„λ””μ˜€ 메타정보λ₯Ό user_content에 μΆ”κ°€
958
+ if valid_images:
959
+ user_content += "\n\n# Available Images\n"
960
+ for i, img in enumerate(valid_images):
961
+ user_content += f"\n{i+1}. ![{img['title']}]({img['url']})\n"
962
+ if img['source']:
963
+ user_content += f" Source: {img['source']}\n"
964
+
965
+ if valid_videos:
966
+ user_content += "\n\n# Available Videos\n"
967
+ for i, vid in enumerate(valid_videos):
968
+ user_content += f"\n{i+1}. **{vid['title']}** - [{vid['source']}]({vid['url']})\n"
969
+
970
+ # OpenAI API에 전달할 μ΅œμ’… λ©”μ‹œμ§€
971
+ api_messages.append({"role": "user", "content": user_content})
972
+
973
+ # OpenAI API 슀트리밍 호좜
974
+ try:
975
+ stream = client.chat.completions.create(
976
+ model="gpt-4.1-mini",
977
+ messages=api_messages,
978
+ temperature=1,
979
+ max_tokens=MAX_TOKENS,
980
+ top_p=1,
981
+ stream=True
982
+ )
983
+
984
+ # 슀트리밍으둜 partial content μˆ˜μ‹ 
985
+ for chunk in stream:
986
+ if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None:
987
+ content_delta = chunk.choices[0].delta.content
988
+ full_response += content_delta
989
+ message_placeholder.markdown(full_response + "β–Œ", unsafe_allow_html=True)
990
+
991
+ # μ΅œμ’… 응닡 ν‘œμ‹œ
992
+ message_placeholder.markdown(full_response, unsafe_allow_html=True)
993
+
994
+ # μ‹€μ œ κ²€μƒ‰λœ 이미지λ₯Ό UI에 ν‘œμ‹œ
995
+ if valid_images:
996
+ st.subheader("Related Images")
997
+ image_cols = st.columns(min(3, len(valid_images)))
998
+
999
+ for i, img_data in enumerate(valid_images):
1000
+ col_idx = i % len(image_cols)
1001
+ try:
1002
+ with image_cols[col_idx]:
1003
+ img_url = img_data['url']
1004
+ caption = img_data['title']
1005
+ st.image(img_url, caption=caption, use_column_width=True)
1006
+ if img_data.get('source'):
1007
+ st.markdown(f"[Source]({img_data['source']})")
1008
+ except Exception as img_err:
1009
+ logging.warning(f"Error displaying image: {img_err}")
1010
+
1011
+ # μ‹€μ œ κ²€μƒ‰λœ λΉ„λ””μ˜€λ₯Ό UI에 ν‘œμ‹œ
1012
+ if valid_videos:
1013
+ st.subheader("Related Videos")
1014
+ for video in valid_videos:
1015
+ video_title = video.get('title', 'Related video')
1016
+ video_url = video.get('url', '')
1017
+ thumbnail = video.get('thumbnail', '')
1018
+
1019
+ if thumbnail:
1020
+ try:
1021
+ col1, col2 = st.columns([1, 3])
1022
+ with col1:
1023
+ try:
1024
+ st.image(thumbnail, width=120)
1025
+ except:
1026
+ st.write("🎬")
1027
+ with col2:
1028
+ st.markdown(f"**[{video_title}]({video_url})**")
1029
+ st.write(f"Source: {video.get('source', 'Unknown')}")
1030
+ except Exception as vid_err:
1031
+ st.markdown(f"🎬 **[{video_title}]({video_url})**")
1032
+ st.write(f"Source: {video.get('source', 'Unknown')}")
1033
+ else:
1034
+ st.markdown(f"🎬 **[{video_title}]({video_url})**")
1035
+ st.write(f"Source: {video.get('source', 'Unknown')}")
1036
+
1037
+ status.update(label="Response completed!", state="complete")
1038
+
1039
+ # μ„Έμ…˜ μ €μž₯
1040
+ st.session_state.messages.append({
1041
+ "role": "assistant",
1042
+ "content": full_response,
1043
+ "images": valid_images,
1044
+ "videos": valid_videos
1045
+ })
1046
+
1047
+ except Exception as api_error:
1048
+ error_message = str(api_error)
1049
+ logging.error(f"API error: {error_message}")
1050
+ status.update(label=f"Error: {error_message}", state="error")
1051
+ raise Exception(f"Response generation error: {error_message}")
1052
+
1053
+ # μΆ”κ°€ 이미지 생성(μ˜΅μ…˜)
1054
+ if st.session_state.generate_image and full_response:
1055
+ with st.spinner("Generating custom image..."):
1056
+ try:
1057
+ ip = extract_image_prompt(full_response, query)
1058
+ img, cap = generate_image(ip)
1059
+ if img:
1060
+ st.subheader("AI-Generated Image")
1061
+ st.image(img, caption=cap)
1062
+ except Exception as img_error:
1063
+ logging.error(f"Image generation error: {str(img_error)}")
1064
+ st.warning("Custom image generation failed.")
1065
+
1066
+ # λ‹€μš΄λ‘œλ“œ λ²„νŠΌ
1067
+ if full_response:
1068
+ st.subheader("Download This Response")
1069
+ c1, c2 = st.columns(2)
1070
+ c1.download_button(
1071
+ "Markdown",
1072
+ data=full_response,
1073
+ file_name=f"{query[:30]}.md",
1074
+ mime="text/markdown"
1075
+ )
1076
+ c2.download_button(
1077
+ "HTML",
1078
+ data=md_to_html(full_response, query[:30]),
1079
+ file_name=f"{query[:30]}.html",
1080
+ mime="text/html"
1081
+ )
1082
+
1083
+ # Auto save
1084
+ if st.session_state.auto_save and st.session_state.messages:
1085
+ try:
1086
+ fn = f"conversation_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
1087
+ with open(fn, "w", encoding="utf-8") as fp:
1088
+ json.dump(st.session_state.messages, fp, ensure_ascii=False, indent=2)
1089
+ except Exception as e:
1090
+ logging.error(f"Auto-save failed: {e}")
1091
+
1092
+ except Exception as e:
1093
+ error_message = str(e)
1094
+ placeholder.error(f"An error occurred: {error_message}")
1095
+ logging.error(f"Process input error: {error_message}")
1096
+ ans = f"An error occurred while processing your request: {error_message}"
1097
+ st.session_state.messages.append({"role": "assistant", "content": ans})
1098
+
1099
+ # ──────────────────────────────── main ────────────────────────────────────
1100
+ def main():
1101
+ perplexity_app()
1102
+
1103
+ if __name__ == "__main__":
1104
+ main()