openfree commited on
Commit
52f7adf
ยท
1 Parent(s): 6655092

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +498 -581
app.py CHANGED
@@ -2,118 +2,119 @@
2
 
3
  import os
4
  import re
5
- import tempfile
6
- import gc # garbage collector ์ถ”๊ฐ€
7
- from collections.abc import Iterator
8
- from threading import Thread
9
  import json
10
  import requests
11
- import cv2
 
 
12
  import gradio as gr
13
- import spaces
14
- import torch
15
  from loguru import logger
16
- from PIL import Image
17
- from transformers import AutoProcessor, Gemma3ForConditionalGeneration, TextIteratorStreamer
18
-
19
- # CSV/TXT ๋ถ„์„
20
  import pandas as pd
21
- # PDF ํ…์ŠคํŠธ ์ถ”์ถœ
22
  import PyPDF2
23
 
24
  ##############################################################################
25
- # ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ ํ•จ์ˆ˜ ์ถ”๊ฐ€
26
  ##############################################################################
27
- def clear_cuda_cache():
28
- """CUDA ์บ์‹œ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ๋น„์›๋‹ˆ๋‹ค."""
29
- if torch.cuda.is_available():
30
- torch.cuda.empty_cache()
31
- gc.collect()
 
 
 
 
 
 
32
 
33
  ##############################################################################
34
- # SERPHouse API key from environment variable
35
  ##############################################################################
36
- SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
 
37
 
38
  ##############################################################################
39
- # ๊ฐ„๋‹จํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ ํ•จ์ˆ˜ (ํ•œ๊ธ€ + ์•ŒํŒŒ๋ฒณ + ์ˆซ์ž + ๊ณต๋ฐฑ ๋ณด์กด)
40
  ##############################################################################
41
  def extract_keywords(text: str, top_k: int = 5) -> str:
42
  """
43
- 1) ํ•œ๊ธ€(๊ฐ€-ํžฃ), ์˜์–ด(a-zA-Z), ์ˆซ์ž(0-9), ๊ณต๋ฐฑ๋งŒ ๋‚จ๊น€
44
- 2) ๊ณต๋ฐฑ ๊ธฐ์ค€ ํ† ํฐ ๋ถ„๋ฆฌ
45
- 3) ์ตœ๋Œ€ top_k๊ฐœ๋งŒ
46
  """
 
 
 
47
  text = re.sub(r"[^a-zA-Z0-9๊ฐ€-ํžฃ\s]", "", text)
48
  tokens = text.split()
49
- key_tokens = tokens[:top_k]
 
 
 
 
 
50
  return " ".join(key_tokens)
51
 
52
  ##############################################################################
53
- # SerpHouse Live endpoint ํ˜ธ์ถœ
54
- # - ์ƒ์œ„ 20๊ฐœ ๊ฒฐ๊ณผ JSON์„ LLM์— ๋„˜๊ธธ ๋•Œ link, snippet ๋“ฑ ๋ชจ๋‘ ํฌํ•จ
55
  ##############################################################################
56
- def do_web_search(query: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
57
  """
58
- ์ƒ์œ„ 20๊ฐœ 'organic' ๊ฒฐ๊ณผ item ์ „์ฒด(์ œ๋ชฉ, link, snippet ๋“ฑ)๋ฅผ
59
- JSON ๋ฌธ์ž์—ด ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜
60
  """
 
 
 
61
  try:
62
  url = "https://api.serphouse.com/serp/live"
63
 
64
- # ๊ธฐ๋ณธ GET ๋ฐฉ์‹์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ ๊ฐ„์†Œํ™”ํ•˜๊ณ  ๊ฒฐ๊ณผ ์ˆ˜๋ฅผ 20๊ฐœ๋กœ ์ œํ•œ
65
  params = {
66
  "q": query,
67
  "domain": "google.com",
68
- "serp_type": "web", # ๊ธฐ๋ณธ ์›น ๊ฒ€์ƒ‰
69
  "device": "desktop",
70
- "lang": "en",
71
- "num": "20" # ์ตœ๋Œ€ 20๊ฐœ ๊ฒฐ๊ณผ๋งŒ ์š”์ฒญ
72
  }
73
 
74
  headers = {
75
  "Authorization": f"Bearer {SERPHOUSE_API_KEY}"
76
  }
77
 
78
- logger.info(f"SerpHouse API ํ˜ธ์ถœ ์ค‘... ๊ฒ€์ƒ‰์–ด: {query}")
79
- logger.info(f"์š”์ฒญ URL: {url} - ํŒŒ๋ผ๋ฏธํ„ฐ: {params}")
80
 
81
- # GET ์š”์ฒญ ์ˆ˜ํ–‰
82
- response = requests.get(url, headers=headers, params=params, timeout=60)
83
  response.raise_for_status()
84
 
85
- logger.info(f"SerpHouse API ์‘๋‹ต ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")
86
  data = response.json()
87
 
88
- # ๋‹ค์–‘ํ•œ ์‘๋‹ต ๊ตฌ์กฐ ์ฒ˜๋ฆฌ
89
  results = data.get("results", {})
90
  organic = None
91
 
92
- # ๊ฐ€๋Šฅํ•œ ์‘๋‹ต ๊ตฌ์กฐ 1
93
  if isinstance(results, dict) and "organic" in results:
94
  organic = results["organic"]
95
-
96
- # ๊ฐ€๋Šฅํ•œ ์‘๋‹ต ๊ตฌ์กฐ 2 (์ค‘์ฒฉ๋œ results)
97
  elif isinstance(results, dict) and "results" in results:
98
  if isinstance(results["results"], dict) and "organic" in results["results"]:
99
  organic = results["results"]["organic"]
100
-
101
- # ๊ฐ€๋Šฅํ•œ ์‘๋‹ต ๊ตฌ์กฐ 3 (์ตœ์ƒ์œ„ organic)
102
  elif "organic" in data:
103
  organic = data["organic"]
104
 
105
  if not organic:
106
- logger.warning("์‘๋‹ต์—์„œ organic ๊ฒฐ๊ณผ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
107
- logger.debug(f"์‘๋‹ต ๊ตฌ์กฐ: {list(data.keys())}")
108
- if isinstance(results, dict):
109
- logger.debug(f"results ๊ตฌ์กฐ: {list(results.keys())}")
110
- return "No web search results found or unexpected API response structure."
111
 
112
- # ๊ฒฐ๊ณผ ์ˆ˜ ์ œํ•œ ๋ฐ ์ปจํ…์ŠคํŠธ ๊ธธ์ด ์ตœ์ ํ™”
113
  max_results = min(20, len(organic))
114
  limited_organic = organic[:max_results]
115
 
116
- # ๊ฒฐ๊ณผ ํ˜•์‹ ๊ฐœ์„  - ๋งˆํฌ๋‹ค์šด ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅํ•˜์—ฌ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ
117
  summary_lines = []
118
  for idx, item in enumerate(limited_organic, start=1):
119
  title = item.get("title", "No title")
@@ -121,104 +122,132 @@ def do_web_search(query: str) -> str:
121
  snippet = item.get("snippet", "No description")
122
  displayed_link = item.get("displayed_link", link)
123
 
124
- # ๋งˆํฌ๋‹ค์šด ํ˜•์‹ (๋งํฌ ํด๋ฆญ ๊ฐ€๋Šฅ)
125
  summary_lines.append(
126
  f"### Result {idx}: {title}\n\n"
127
  f"{snippet}\n\n"
128
- f"**์ถœ์ฒ˜**: [{displayed_link}]({link})\n\n"
129
  f"---\n"
130
  )
131
 
132
- # ๋ชจ๋ธ์—๊ฒŒ ๋ช…ํ™•ํ•œ ์ง€์นจ ์ถ”๊ฐ€
133
  instructions = """
134
- # ์›น ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ
135
- ์•„๋ž˜๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋‹ต๋ณ€ํ•  ๋•Œ ์ด ์ •๋ณด๋ฅผ ํ™œ์šฉํ•˜์„ธ์š”:
136
- 1. ๊ฐ ๊ฒฐ๊ณผ์˜ ์ œ๋ชฉ, ๋‚ด์šฉ, ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”
137
- 2. ๋‹ต๋ณ€์— ๊ด€๋ จ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ธ์šฉํ•˜์„ธ์š” (์˜ˆ: "X ์ถœ์ฒ˜์— ๋”ฐ๋ฅด๋ฉด...")
138
- 3. ์‘๋‹ต์— ์‹ค์ œ ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ํฌํ•จํ•˜์„ธ์š”
139
- 4. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”
140
  """
141
 
142
  search_results = instructions + "\n".join(summary_lines)
143
- logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ {len(limited_organic)}๊ฐœ ์ฒ˜๋ฆฌ ์™„๋ฃŒ")
144
  return search_results
145
 
 
 
 
 
 
 
146
  except Exception as e:
147
  logger.error(f"Web search failed: {e}")
148
  return f"Web search failed: {str(e)}"
149
 
150
-
151
- ##############################################################################
152
- # ๋ชจ๋ธ/ํ”„๋กœ์„ธ์„œ ๋กœ๋”ฉ
153
- ##############################################################################
154
- MAX_CONTENT_CHARS = 2000
155
- MAX_INPUT_LENGTH = 2096 # ์ตœ๋Œ€ ์ž…๋ ฅ ํ† ํฐ ์ˆ˜ ์ œํ•œ ์ถ”๊ฐ€
156
- model_id = os.getenv("MODEL_ID", "VIDraft/Gemma-3-R1984-27B")
157
-
158
- processor = AutoProcessor.from_pretrained(model_id, padding_side="left")
159
- model = Gemma3ForConditionalGeneration.from_pretrained(
160
- model_id,
161
- device_map="auto",
162
- torch_dtype=torch.bfloat16,
163
- attn_implementation="eager" # ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด "flash_attention_2"๋กœ ๋ณ€๊ฒฝ
164
- )
165
- MAX_NUM_IMAGES = int(os.getenv("MAX_NUM_IMAGES", "5"))
166
-
167
-
168
  ##############################################################################
169
- # CSV, TXT, PDF ๋ถ„์„ ํ•จ์ˆ˜
170
  ##############################################################################
171
  def analyze_csv_file(path: str) -> str:
172
- """
173
- CSV ํŒŒ์ผ์„ ์ „์ฒด ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜. ๋„ˆ๋ฌด ๊ธธ ๊ฒฝ์šฐ ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
174
- """
 
175
  try:
176
- df = pd.read_csv(path)
177
- if df.shape[0] > 50 or df.shape[1] > 10:
178
- df = df.iloc[:50, :10]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  df_str = df.to_string()
180
  if len(df_str) > MAX_CONTENT_CHARS:
181
  df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
182
- return f"**[CSV File: {os.path.basename(path)}]**\n\n{df_str}"
 
183
  except Exception as e:
184
- return f"Failed to read CSV ({os.path.basename(path)}): {str(e)}"
185
-
186
 
187
  def analyze_txt_file(path: str) -> str:
188
- """
189
- TXT ํŒŒ์ผ ์ „๋ฌธ ์ฝ๊ธฐ. ๋„ˆ๋ฌด ๊ธธ๋ฉด ์ผ๋ถ€๋งŒ ํ‘œ์‹œ.
190
- """
191
- try:
192
- with open(path, "r", encoding="utf-8") as f:
193
- text = f.read()
194
- if len(text) > MAX_CONTENT_CHARS:
195
- text = text[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
196
- return f"**[TXT File: {os.path.basename(path)}]**\n\n{text}"
197
- except Exception as e:
198
- return f"Failed to read TXT ({os.path.basename(path)}): {str(e)}"
199
-
 
 
 
 
 
 
 
 
 
 
200
 
201
  def pdf_to_markdown(pdf_path: str) -> str:
202
- """
203
- PDF ํ…์ŠคํŠธ๋ฅผ Markdown์œผ๋กœ ๋ณ€ํ™˜. ํŽ˜์ด์ง€๋ณ„๋กœ ๊ฐ„๋‹จํžˆ ํ…์ŠคํŠธ ์ถ”์ถœ.
204
- """
 
205
  text_chunks = []
206
  try:
207
  with open(pdf_path, "rb") as f:
208
  reader = PyPDF2.PdfReader(f)
209
- max_pages = min(5, len(reader.pages))
 
 
 
 
 
210
  for page_num in range(max_pages):
211
- page = reader.pages[page_num]
212
- page_text = page.extract_text() or ""
213
- page_text = page_text.strip()
214
- if page_text:
215
- if len(page_text) > MAX_CONTENT_CHARS // max_pages:
216
- page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
217
- text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
218
- if len(reader.pages) > max_pages:
219
- text_chunks.append(f"\n...(Showing {max_pages} of {len(reader.pages)} pages)...")
 
 
 
 
 
220
  except Exception as e:
221
- return f"Failed to read PDF ({os.path.basename(pdf_path)}): {str(e)}"
 
222
 
223
  full_text = "\n".join(text_chunks)
224
  if len(full_text) > MAX_CONTENT_CHARS:
@@ -226,365 +255,256 @@ def pdf_to_markdown(pdf_path: str) -> str:
226
 
227
  return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
228
 
229
-
230
  ##############################################################################
231
- # ์ด๋ฏธ์ง€/๋น„๋””์˜ค ์—…๋กœ๋“œ ์ œํ•œ ๊ฒ€์‚ฌ
232
  ##############################################################################
233
- def count_files_in_new_message(paths: list[str]) -> tuple[int, int]:
234
- image_count = 0
235
- video_count = 0
236
- for path in paths:
237
- if path.endswith(".mp4"):
238
- video_count += 1
239
- elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", path, re.IGNORECASE):
240
- image_count += 1
241
- return image_count, video_count
242
-
243
-
244
- def count_files_in_history(history: list[dict]) -> tuple[int, int]:
245
- image_count = 0
246
- video_count = 0
247
- for item in history:
248
- if item["role"] != "user" or isinstance(item["content"], str):
249
- continue
250
- if isinstance(item["content"], list) and len(item["content"]) > 0:
251
- file_path = item["content"][0]
252
- if isinstance(file_path, str):
253
- if file_path.endswith(".mp4"):
254
- video_count += 1
255
- elif re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE):
256
- image_count += 1
257
- return image_count, video_count
258
-
259
-
260
- def validate_media_constraints(message: dict, history: list[dict]) -> bool:
261
- media_files = []
262
- for f in message["files"]:
263
- if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE) or f.endswith(".mp4"):
264
- media_files.append(f)
265
-
266
- new_image_count, new_video_count = count_files_in_new_message(media_files)
267
- history_image_count, history_video_count = count_files_in_history(history)
268
- image_count = history_image_count + new_image_count
269
- video_count = history_video_count + new_video_count
270
-
271
- if video_count > 1:
272
- gr.Warning("Only one video is supported.")
273
- return False
274
- if video_count == 1:
275
- if image_count > 0:
276
- gr.Warning("Mixing images and videos is not allowed.")
277
- return False
278
- if "<image>" in message["text"]:
279
- gr.Warning("Using <image> tags with video files is not supported.")
280
- return False
281
- if video_count == 0 and image_count > MAX_NUM_IMAGES:
282
- gr.Warning(f"You can upload up to {MAX_NUM_IMAGES} images.")
283
- return False
284
-
285
- if "<image>" in message["text"]:
286
- image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
287
- image_tag_count = message["text"].count("<image>")
288
- if image_tag_count != len(image_files):
289
- gr.Warning("The number of <image> tags in the text does not match the number of image files.")
290
- return False
291
-
292
- return True
293
-
294
-
295
- ##############################################################################
296
- # ๋น„๋””์˜ค ์ฒ˜๋ฆฌ - ์ž„์‹œ ํŒŒ์ผ ์ถ”์  ์ฝ”๋“œ ์ถ”๊ฐ€
297
- ##############################################################################
298
- def downsample_video(video_path: str) -> list[tuple[Image.Image, float]]:
299
- vidcap = cv2.VideoCapture(video_path)
300
- fps = vidcap.get(cv2.CAP_PROP_FPS)
301
- total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
302
- frame_interval = max(int(fps), int(total_frames / 10))
303
- frames = []
304
-
305
- for i in range(0, total_frames, frame_interval):
306
- vidcap.set(cv2.CAP_PROP_POS_FRAMES, i)
307
- success, image = vidcap.read()
308
- if success:
309
- image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
310
- # ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์ค„์ด๊ธฐ ์ถ”๊ฐ€
311
- image = cv2.resize(image, (0, 0), fx=0.5, fy=0.5)
312
- pil_image = Image.fromarray(image)
313
- timestamp = round(i / fps, 2)
314
- frames.append((pil_image, timestamp))
315
- if len(frames) >= 5:
316
- break
317
-
318
- vidcap.release()
319
- return frames
320
-
321
 
322
- def process_video(video_path: str) -> tuple[list[dict], list[str]]:
323
- content = []
324
- temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์„ ์œ„ํ•œ ๋ฆฌ์ŠคํŠธ
325
-
326
- frames = downsample_video(video_path)
327
- for frame in frames:
328
- pil_image, timestamp = frame
329
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
330
- pil_image.save(temp_file.name)
331
- temp_files.append(temp_file.name) # ์ถ”์ ์„ ์œ„ํ•ด ๊ฒฝ๋กœ ์ €์žฅ
332
- content.append({"type": "text", "text": f"Frame {timestamp}:"})
333
- content.append({"type": "image", "url": temp_file.name})
334
-
335
- return content, temp_files
336
 
 
 
 
337
 
338
  ##############################################################################
339
- # interleaved <image> ์ฒ˜๋ฆฌ
340
  ##############################################################################
341
- def process_interleaved_images(message: dict) -> list[dict]:
342
- parts = re.split(r"(<image>)", message["text"])
343
- content = []
344
- image_index = 0
345
 
346
- image_files = [f for f in message["files"] if re.search(r"\.(png|jpg|jpeg|gif|webp)$", f, re.IGNORECASE)]
 
 
 
 
 
 
 
 
 
347
 
348
- for part in parts:
349
- if part == "<image>" and image_index < len(image_files):
350
- content.append({"type": "image", "url": image_files[image_index]})
351
- image_index += 1
352
- elif part.strip():
353
- content.append({"type": "text", "text": part.strip()})
 
 
 
 
 
354
  else:
355
- if isinstance(part, str) and part != "<image>":
356
- content.append({"type": "text", "text": part})
357
- return content
358
-
359
-
360
- ##############################################################################
361
- # PDF + CSV + TXT + ์ด๋ฏธ์ง€/๋น„๋””์˜ค
362
- ##############################################################################
363
- def is_image_file(file_path: str) -> bool:
364
- return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
365
-
366
- def is_video_file(file_path: str) -> bool:
367
- return file_path.endswith(".mp4")
368
-
369
- def is_document_file(file_path: str) -> bool:
370
- return (
371
- file_path.lower().endswith(".pdf")
372
- or file_path.lower().endswith(".csv")
373
- or file_path.lower().endswith(".txt")
374
- )
375
-
376
-
377
- def process_new_user_message(message: dict) -> tuple[list[dict], list[str]]:
378
- temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์šฉ ๋ฆฌ์ŠคํŠธ
379
 
380
- if not message["files"]:
381
- return [{"type": "text", "text": message["text"]}], temp_files
382
-
383
- video_files = [f for f in message["files"] if is_video_file(f)]
384
- image_files = [f for f in message["files"] if is_image_file(f)]
385
- csv_files = [f for f in message["files"] if f.lower().endswith(".csv")]
386
- txt_files = [f for f in message["files"] if f.lower().endswith(".txt")]
387
- pdf_files = [f for f in message["files"] if f.lower().endswith(".pdf")]
388
-
389
- content_list = [{"type": "text", "text": message["text"]}]
390
-
391
  for csv_path in csv_files:
392
  csv_analysis = analyze_csv_file(csv_path)
393
- content_list.append({"type": "text", "text": csv_analysis})
394
 
395
  for txt_path in txt_files:
396
  txt_analysis = analyze_txt_file(txt_path)
397
- content_list.append({"type": "text", "text": txt_analysis})
398
 
399
  for pdf_path in pdf_files:
400
  pdf_markdown = pdf_to_markdown(pdf_path)
401
- content_list.append({"type": "text", "text": pdf_markdown})
402
-
 
 
 
 
 
 
 
 
 
403
  if video_files:
404
- video_content, video_temp_files = process_video(video_files[0])
405
- content_list += video_content
406
- temp_files.extend(video_temp_files)
407
- return content_list, temp_files
408
-
409
- if "<image>" in message["text"] and image_files:
410
- interleaved_content = process_interleaved_images({"text": message["text"], "files": image_files})
411
- if content_list and content_list[0]["type"] == "text":
412
- content_list = content_list[1:]
413
- return interleaved_content + content_list, temp_files
414
- else:
415
- for img_path in image_files:
416
- content_list.append({"type": "image", "url": img_path})
417
-
418
- return content_list, temp_files
419
-
420
 
421
- ##############################################################################
422
- # history -> LLM ๋ฉ”์‹œ์ง€ ๋ณ€ํ™˜
423
- ##############################################################################
424
  def process_history(history: list[dict]) -> list[dict]:
 
425
  messages = []
426
- current_user_content: list[dict] = []
427
  for item in history:
428
  if item["role"] == "assistant":
429
- if current_user_content:
430
- messages.append({"role": "user", "content": current_user_content})
431
- current_user_content = []
432
- messages.append({"role": "assistant", "content": [{"type": "text", "text": item["content"]}]})
433
- else:
434
  content = item["content"]
435
  if isinstance(content, str):
436
- current_user_content.append({"type": "text", "text": content})
 
 
 
437
  elif isinstance(content, list) and len(content) > 0:
438
- file_path = content[0]
439
- if is_image_file(file_path):
440
- current_user_content.append({"type": "image", "url": file_path})
441
- else:
442
- current_user_content.append({"type": "text", "text": f"[File: {os.path.basename(file_path)}]"})
443
-
444
- if current_user_content:
445
- messages.append({"role": "user", "content": current_user_content})
446
-
 
 
447
  return messages
448
 
449
-
450
  ##############################################################################
451
- # ๋ชจ๋ธ ์ƒ์„ฑ ํ•จ์ˆ˜์—์„œ OOM ์บ์น˜
452
  ##############################################################################
453
- def _model_gen_with_oom_catch(**kwargs):
454
- """
455
- ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ OutOfMemoryError๋ฅผ ์žก์•„์ฃผ๊ธฐ ์œ„ํ•ด
456
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  try:
458
- model.generate(**kwargs)
459
- except torch.cuda.OutOfMemoryError:
460
- raise RuntimeError(
461
- "[OutOfMemoryError] GPU ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. "
462
- "Max New Tokens์„ ์ค„์ด๊ฑฐ๋‚˜, ํ”„๋กฌํ”„ํŠธ ๊ธธ์ด๋ฅผ ์ค„์—ฌ์ฃผ์„ธ์š”."
 
463
  )
464
- finally:
465
- # ์ƒ์„ฑ ์™„๋ฃŒ ํ›„ ํ•œ๋ฒˆ ๋” ์บ์‹œ ๋น„์šฐ๊ธฐ
466
- clear_cuda_cache()
467
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
 
469
  ##############################################################################
470
- # ๋ฉ”์ธ ์ถ”๋ก  ํ•จ์ˆ˜ (web search ์ฒดํฌ ์‹œ ์ž๋™ ํ‚ค์›Œ๋“œ์ถ”์ถœ->๊ฒ€์ƒ‰->๊ฒฐ๊ณผ system msg)
471
  ##############################################################################
472
- @spaces.GPU(duration=120)
473
  def run(
474
  message: dict,
475
  history: list[dict],
476
- system_prompt: str = "",
477
  max_new_tokens: int = 512,
478
  use_web_search: bool = False,
479
- web_search_query: str = "",
 
480
  ) -> Iterator[str]:
481
-
482
- if not validate_media_constraints(message, history):
483
- yield ""
484
- return
485
-
486
- temp_files = [] # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ ์šฉ
487
 
488
  try:
489
- combined_system_msg = ""
490
-
491
- # ๋‚ด๋ถ€์ ์œผ๋กœ๋งŒ ์‚ฌ์šฉ (UI์—์„œ๋Š” ๋ณด์ด์ง€ ์•Š์Œ)
 
 
 
 
 
492
  if system_prompt.strip():
493
- combined_system_msg += f"[System Prompt]\n{system_prompt.strip()}\n\n"
494
-
 
495
  if use_web_search:
496
- user_text = message["text"]
497
- ws_query = extract_keywords(user_text, top_k=5)
498
- if ws_query.strip():
499
- logger.info(f"[Auto WebSearch Keyword] {ws_query!r}")
500
- ws_result = do_web_search(ws_query)
501
- combined_system_msg += f"[Search top-20 Full Items Based on user prompt]\n{ws_result}\n\n"
502
- # >>> ์ถ”๊ฐ€๋œ ์•ˆ๋‚ด ๋ฌธ๊ตฌ (๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ link ๋“ฑ ์ถœ์ฒ˜๋ฅผ ํ™œ์šฉ)
503
- combined_system_msg += "[์ฐธ๊ณ : ์œ„ ๊ฒ€์ƒ‰๊ฒฐ๊ณผ ๋‚ด์šฉ๊ณผ link๋ฅผ ์ถœ์ฒ˜๋กœ ์ธ์šฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•ด ์ฃผ์„ธ์š”.]\n\n"
504
- combined_system_msg += """
505
- [์ค‘์š” ์ง€์‹œ์‚ฌํ•ญ]
506
- 1. ๋‹ต๋ณ€์— ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ์ฐพ์€ ์ •๋ณด์˜ ์ถœ์ฒ˜๋ฅผ ๋ฐ˜๋“œ์‹œ ์ธ์šฉํ•˜์„ธ์š”.
507
- 2. ์ถœ์ฒ˜ ์ธ์šฉ ์‹œ "[์ถœ์ฒ˜ ์ œ๋ชฉ](๋งํฌ)" ํ˜•์‹์˜ ๋งˆํฌ๋‹ค์šด ๋งํฌ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”.
508
- 3. ์—ฌ๋Ÿฌ ์ถœ์ฒ˜์˜ ์ •๋ณด๋ฅผ ์ข…ํ•ฉํ•˜์—ฌ ๋‹ต๋ณ€ํ•˜์„ธ์š”.
509
- 4. ๋‹ต๋ณ€ ๋งˆ์ง€๋ง‰์— "์ฐธ๊ณ  ์ž๋ฃŒ:" ์„น์…˜์„ ์ถ”๊ฐ€ํ•˜๊ณ  ์‚ฌ์šฉํ•œ ์ฃผ์š” ์ถœ์ฒ˜ ๋งํฌ๋ฅผ ๋‚˜์—ดํ•˜์„ธ์š”.
510
- """
511
- else:
512
- combined_system_msg += "[No valid keywords found, skipping WebSearch]\n\n"
513
-
514
- messages = []
515
- if combined_system_msg.strip():
516
- messages.append({
517
- "role": "system",
518
- "content": [{"type": "text", "text": combined_system_msg.strip()}],
519
- })
520
-
521
- messages.extend(process_history(history))
522
-
523
- user_content, user_temp_files = process_new_user_message(message)
524
- temp_files.extend(user_temp_files) # ์ž„์‹œ ํŒŒ์ผ ์ถ”์ 
525
 
526
- for item in user_content:
527
- if item["type"] == "text" and len(item["text"]) > MAX_CONTENT_CHARS:
528
- item["text"] = item["text"][:MAX_CONTENT_CHARS] + "\n...(truncated)..."
529
- messages.append({"role": "user", "content": user_content})
530
-
531
- inputs = processor.apply_chat_template(
532
- messages,
533
- add_generation_prompt=True,
534
- tokenize=True,
535
- return_dict=True,
536
- return_tensors="pt",
537
- ).to(device=model.device, dtype=torch.bfloat16)
538
 
539
- # ์ž…๋ ฅ ํ† ํฐ ์ˆ˜ ์ œํ•œ ์ถ”๊ฐ€
540
- if inputs.input_ids.shape[1] > MAX_INPUT_LENGTH:
541
- inputs.input_ids = inputs.input_ids[:, -MAX_INPUT_LENGTH:]
542
- if 'attention_mask' in inputs:
543
- inputs.attention_mask = inputs.attention_mask[:, -MAX_INPUT_LENGTH:]
544
 
545
- streamer = TextIteratorStreamer(processor, timeout=30.0, skip_prompt=True, skip_special_tokens=True)
546
- gen_kwargs = dict(
547
- inputs,
548
- streamer=streamer,
549
- max_new_tokens=max_new_tokens,
550
- )
551
-
552
- t = Thread(target=_model_gen_with_oom_catch, kwargs=gen_kwargs)
553
- t.start()
554
-
555
- output = ""
556
- for new_text in streamer:
557
- output += new_text
558
- yield output
559
-
560
- except Exception as e:
561
- logger.error(f"Error in run: {str(e)}")
562
- yield f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"
563
-
564
- finally:
565
- # ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ
566
- for temp_file in temp_files:
567
- try:
568
- if os.path.exists(temp_file):
569
- os.unlink(temp_file)
570
- logger.info(f"Deleted temp file: {temp_file}")
571
- except Exception as e:
572
- logger.warning(f"Failed to delete temp file {temp_file}: {e}")
573
 
574
- # ๋ช…์‹œ์  ๋ฉ”๋ชจ๋ฆฌ ์ •๋ฆฌ
575
- try:
576
- del inputs, streamer
577
- except:
578
- pass
579
 
580
- clear_cuda_cache()
581
-
582
-
 
 
 
 
583
 
584
  ##############################################################################
585
- # ์˜ˆ์‹œ๋“ค (๋ชจ๋‘ ์˜์–ด๋กœ)
586
  ##############################################################################
587
  examples = [
 
588
  [
589
  {
590
  "text": "Compare the contents of the two PDF files.",
@@ -594,256 +514,253 @@ examples = [
594
  ],
595
  }
596
  ],
 
597
  [
598
  {
599
  "text": "Summarize and analyze the contents of the CSV file.",
600
  "files": ["assets/additional-examples/sample-csv.csv"],
601
  }
602
  ],
 
603
  [
604
  {
605
- "text": "Assume the role of a friendly and understanding girlfriend. Describe this video.",
606
- "files": ["assets/additional-examples/tmp.mp4"],
607
- }
608
- ],
609
- [
610
- {
611
- "text": "Describe the cover and read the text on it.",
612
- "files": ["assets/additional-examples/maz.jpg"],
613
- }
614
- ],
615
- [
616
- {
617
- "text": "I already have this supplement <image> and I plan to buy this product <image>. Are there any precautions when taking them together?",
618
- "files": ["assets/additional-examples/pill1.png", "assets/additional-examples/pill2.png"],
619
  }
620
  ],
 
621
  [
622
  {
623
- "text": "Solve this integral.",
624
- "files": ["assets/additional-examples/4.png"],
625
  }
626
  ],
627
- [
628
- {
629
- "text": "When was this ticket issued, and what is its price?",
630
- "files": ["assets/additional-examples/2.png"],
631
- }
632
- ],
633
- [
634
- {
635
- "text": "Based on the sequence of these images, create a short story.",
636
- "files": [
637
- "assets/sample-images/09-1.png",
638
- "assets/sample-images/09-2.png",
639
- "assets/sample-images/09-3.png",
640
- "assets/sample-images/09-4.png",
641
- "assets/sample-images/09-5.png",
642
- ],
643
- }
644
- ],
645
- [
646
- {
647
- "text": "Write Python code using matplotlib to plot a bar chart that matches this image.",
648
- "files": ["assets/additional-examples/barchart.png"],
649
- }
650
- ],
651
- [
652
- {
653
- "text": "Read the text in the image and write it out in Markdown format.",
654
- "files": ["assets/additional-examples/3.png"],
655
- }
656
- ],
657
- [
658
- {
659
- "text": "What does this sign say?",
660
- "files": ["assets/sample-images/02.png"],
661
- }
662
- ],
663
- [
664
- {
665
- "text": "Compare the two images and describe their similarities and differences.",
666
- "files": ["assets/sample-images/03.png"],
667
- }
668
- ],
669
  ]
670
 
671
  ##############################################################################
672
- # Gradio UI (Blocks) ๊ตฌ์„ฑ (์ขŒ์ธก ์‚ฌ์ด๋“œ ๋ฉ”๋‰ด ์—†์ด ์ „์ฒดํ™”๋ฉด ์ฑ„ํŒ…)
673
  ##############################################################################
674
  css = """
675
- /* 1) UI๋ฅผ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ฐ€์žฅ ๋„“๊ฒŒ (width 100%) ๊ณ ์ •ํ•˜์—ฌ ํ‘œ์‹œ */
676
  .gradio-container {
677
- background: rgba(255, 255, 255, 0.7); /* ๋ฐฐ๊ฒฝ ํˆฌ๋ช…๋„ ์ฆ๊ฐ€ */
678
  padding: 30px 40px;
679
- margin: 20px auto; /* ์œ„์•„๋ž˜ ์—ฌ๋ฐฑ๋งŒ ์œ ์ง€ */
680
  width: 100% !important;
681
- max-width: none !important; /* 1200px ์ œํ•œ ์ œ๊ฑฐ */
 
 
682
  }
 
683
  .fillable {
684
  width: 100% !important;
685
  max-width: 100% !important;
686
  }
687
- /* 2) ๋ฐฐ๊ฒฝ์„ ์™„์ „ํžˆ ํˆฌ๋ช…ํ•˜๊ฒŒ ๋ณ€๊ฒฝ */
 
688
  body {
689
- background: transparent; /* ์™„์ „ ํˆฌ๋ช… ๋ฐฐ๊ฒฝ */
690
  margin: 0;
691
  padding: 0;
692
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
693
  color: #333;
694
  }
695
- /* ๋ฒ„ํŠผ ์ƒ‰์ƒ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ  ํˆฌ๋ช…ํ•˜๊ฒŒ */
 
696
  button, .btn {
697
- background: transparent !important; /* ์ƒ‰์ƒ ์™„์ „ํžˆ ์ œ๊ฑฐ */
698
- border: 1px solid #ddd; /* ๊ฒฝ๊ณ„์„ ๋งŒ ์‚ด์ง ์ถ”๊ฐ€ */
699
- color: #333;
700
- padding: 12px 24px;
701
  text-transform: uppercase;
702
- font-weight: bold;
703
- letter-spacing: 1px;
704
  cursor: pointer;
 
 
705
  }
 
706
  button:hover, .btn:hover {
707
- background: rgba(0, 0, 0, 0.05) !important; /* ํ˜ธ๋ฒ„ ์‹œ ์•„์ฃผ ์‚ด์ง ์–ด๋‘ก๊ฒŒ๋งŒ */
 
 
708
  }
709
 
710
- /* examples ๊ด€๋ จ ๋ชจ๋“  ์ƒ‰์ƒ ์ œ๊ฑฐ */
711
  #examples_container, .examples-container {
712
- margin: auto;
713
  width: 90%;
714
- background: transparent !important;
 
 
715
  }
 
716
  #examples_row, .examples-row {
717
  justify-content: center;
718
- background: transparent !important;
719
  }
720
 
721
- /* examples ๋ฒ„ํŠผ ๋‚ด๋ถ€์˜ ๋ชจ๋“  ์ƒ‰์ƒ ์ œ๊ฑฐ */
722
  .gr-samples-table button,
723
- .gr-samples-table .gr-button,
724
- .gr-samples-table .gr-sample-btn,
725
  .gr-examples button,
726
- .gr-examples .gr-button,
727
- .gr-examples .gr-sample-btn,
728
- .examples button,
729
- .examples .gr-button,
730
- .examples .gr-sample-btn {
731
- background: transparent !important;
732
- border: 1px solid #ddd;
733
- color: #333;
734
  }
735
 
736
- /* examples ๋ฒ„ํŠผ ํ˜ธ๋ฒ„ ์‹œ์—๋„ ์ƒ‰์ƒ ์—†๊ฒŒ */
737
  .gr-samples-table button:hover,
738
- .gr-samples-table .gr-button:hover,
739
- .gr-samples-table .gr-sample-btn:hover,
740
  .gr-examples button:hover,
741
- .gr-examples .gr-button:hover,
742
- .gr-examples .gr-sample-btn:hover,
743
- .examples button:hover,
744
- .examples .gr-button:hover,
745
- .examples .gr-sample-btn:hover {
746
- background: rgba(0, 0, 0, 0.05) !important;
747
  }
748
 
749
- /* ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค ์š”์†Œ๋“ค๋„ ํˆฌ๋ช…ํ•˜๊ฒŒ */
750
- .chatbox, .chatbot, .message {
751
- background: transparent !important;
 
 
752
  }
753
 
754
- /* ์ž…๋ ฅ์ฐฝ ํˆฌ๋ช…๋„ ์กฐ์ • */
755
- .multimodal-textbox, textarea, input {
756
- background: rgba(255, 255, 255, 0.5) !important;
 
757
  }
758
 
759
- /* ๋ชจ๋“  ์ปจํ…Œ์ด๋„ˆ ์š”์†Œ์— ๋ฐฐ๊ฒฝ์ƒ‰ ์ œ๊ฑฐ */
760
- .container, .wrap, .box, .panel, .gr-panel {
761
- background: transparent !important;
 
 
 
 
762
  }
763
 
764
- /* ์˜ˆ์ œ ์„น์…˜์˜ ๋ชจ๋“  ์š”์†Œ์—์„œ ๋ฐฐ๊ฒฝ์ƒ‰ ์ œ๊ฑฐ */
765
- .gr-examples-container, .gr-examples, .gr-sample, .gr-sample-row, .gr-sample-cell {
766
- background: transparent !important;
 
767
  }
768
- """
769
 
770
- title_html = """
771
- <h1 align="center" style="margin-bottom: 0.2em; font-size: 1.6em;"> ๐Ÿค— Gemma3-R1984-27B </h1>
772
- <p align="center" style="font-size:1.1em; color:#555;">
773
- โœ…Agentic AI Platform โœ…Reasoning & Uncensored โœ…Multimodal & VLM โœ…Deep-Research & RAG <br>
774
- Operates on an โœ…'NVIDIA A100 GPU' as an independent local server, enhancing security and preventing information leakage.<br>
775
- @Model Rpository: VIDraft/Gemma-3-R1984-27B, @Based by 'Google Gemma-3-27b', @Powered by 'MOUSE-II'(VIDRAFT)
776
- </p>
777
- """
 
778
 
 
 
 
 
779
 
780
- with gr.Blocks(css=css, title="Gemma3-R1984-27B") as demo:
781
- gr.Markdown(title_html)
 
 
 
782
 
783
- # Display the web search option (while the system prompt and token slider remain hidden)
784
- web_search_checkbox = gr.Checkbox(
785
- label="Deep Research",
786
- value=False
787
- )
788
 
789
- # Used internally but not visible to the user
790
- system_prompt_box = gr.Textbox(
791
- lines=3,
792
- value="You are a deep thinking AI that may use extremely long chains of thought to thoroughly analyze the problem and deliberate using systematic reasoning processes to arrive at a correct solution before answering.",
793
- visible=False # hidden from view
794
- )
795
-
796
- max_tokens_slider = gr.Slider(
797
- label="Max New Tokens",
798
- minimum=100,
799
- maximum=8000,
800
- step=50,
801
- value=1000,
802
- visible=False # hidden from view
803
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
 
805
- web_search_text = gr.Textbox(
806
- lines=1,
807
- label="(Unused) Web Search Query",
808
- placeholder="No direct input needed",
809
- visible=False # hidden from view
810
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
 
812
- # Configure the chat interface
813
  chat = gr.ChatInterface(
814
  fn=run,
815
  type="messages",
816
- chatbot=gr.Chatbot(type="messages", scale=1, allow_tags=["image"]),
817
  textbox=gr.MultimodalTextbox(
818
  file_types=[
819
  ".webp", ".png", ".jpg", ".jpeg", ".gif",
820
  ".mp4", ".csv", ".txt", ".pdf"
821
  ],
822
  file_count="multiple",
823
- autofocus=True
 
824
  ),
825
  multimodal=True,
826
  additional_inputs=[
827
- system_prompt_box,
828
  max_tokens_slider,
829
  web_search_checkbox,
830
- web_search_text,
831
  ],
832
  stop_btn=False,
833
- title='<a href="https://discord.gg/openfreeai" target="_blank">https://discord.gg/openfreeai</a>',
834
  examples=examples,
835
  run_examples_on_click=False,
836
  cache_examples=False,
837
- css_paths=None,
838
  delete_cache=(1800, 1800),
839
  )
840
 
841
- # Example section - since examples are already set in ChatInterface, this is for display only
842
- with gr.Row(elem_id="examples_row"):
843
- with gr.Column(scale=12, elem_id="examples_container"):
844
- gr.Markdown("### Example Inputs (click to load)")
845
-
846
-
847
  if __name__ == "__main__":
848
- # Run locally
849
- demo.launch()
 
2
 
3
  import os
4
  import re
 
 
 
 
5
  import json
6
  import requests
7
+ from collections.abc import Iterator
8
+ from threading import Thread
9
+
10
  import gradio as gr
 
 
11
  from loguru import logger
 
 
 
 
12
  import pandas as pd
 
13
  import PyPDF2
14
 
15
  ##############################################################################
16
+ # API Configuration
17
  ##############################################################################
18
+ FRIENDLI_TOKEN = os.environ.get("FRIENDLI_TOKEN")
19
+ if not FRIENDLI_TOKEN:
20
+ raise ValueError("Please set FRIENDLI_TOKEN environment variable")
21
+
22
+ FRIENDLI_MODEL_ID = "dep89a2fld32mcm"
23
+ FRIENDLI_API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
24
+
25
+ # SERPHouse API key
26
+ SERPHOUSE_API_KEY = os.getenv("SERPHOUSE_API_KEY", "")
27
+ if not SERPHOUSE_API_KEY:
28
+ logger.warning("SERPHOUSE_API_KEY not set. Web search functionality will be limited.")
29
 
30
  ##############################################################################
31
+ # File Processing Constants
32
  ##############################################################################
33
+ MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
34
+ MAX_CONTENT_CHARS = 2000
35
 
36
  ##############################################################################
37
+ # Improved Keyword Extraction
38
  ##############################################################################
39
  def extract_keywords(text: str, top_k: int = 5) -> str:
40
  """
41
+ Extract keywords: supports English and Korean
 
 
42
  """
43
+ stop_words = {'์€', '๋Š”', '์ด', '๊ฐ€', '์„', '๋ฅผ', '์˜', '์—', '์—์„œ',
44
+ 'the', 'is', 'at', 'on', 'in', 'a', 'an', 'and', 'or', 'but'}
45
+
46
  text = re.sub(r"[^a-zA-Z0-9๊ฐ€-ํžฃ\s]", "", text)
47
  tokens = text.split()
48
+
49
+ key_tokens = [
50
+ token for token in tokens
51
+ if token.lower() not in stop_words and len(token) > 1
52
+ ][:top_k]
53
+
54
  return " ".join(key_tokens)
55
 
56
  ##############################################################################
57
+ # File Size Validation
 
58
  ##############################################################################
59
+ def validate_file_size(file_path: str) -> bool:
60
+ """Check if file size is within limits"""
61
+ try:
62
+ file_size = os.path.getsize(file_path)
63
+ return file_size <= MAX_FILE_SIZE
64
+ except:
65
+ return False
66
+
67
+ ##############################################################################
68
+ # Web Search Function
69
+ ##############################################################################
70
+ def do_web_search(query: str, use_korean: bool = False) -> str:
71
  """
72
+ Search web and return top 20 organic results
 
73
  """
74
+ if not SERPHOUSE_API_KEY:
75
+ return "Web search unavailable. API key not configured."
76
+
77
  try:
78
  url = "https://api.serphouse.com/serp/live"
79
 
 
80
  params = {
81
  "q": query,
82
  "domain": "google.com",
83
+ "serp_type": "web",
84
  "device": "desktop",
85
+ "lang": "ko" if use_korean else "en",
86
+ "num": "20"
87
  }
88
 
89
  headers = {
90
  "Authorization": f"Bearer {SERPHOUSE_API_KEY}"
91
  }
92
 
93
+ logger.info(f"Calling SerpHouse API... Query: {query}")
 
94
 
95
+ response = requests.get(url, headers=headers, params=params, timeout=30)
 
96
  response.raise_for_status()
97
 
 
98
  data = response.json()
99
 
100
+ # Parse results
101
  results = data.get("results", {})
102
  organic = None
103
 
 
104
  if isinstance(results, dict) and "organic" in results:
105
  organic = results["organic"]
 
 
106
  elif isinstance(results, dict) and "results" in results:
107
  if isinstance(results["results"], dict) and "organic" in results["results"]:
108
  organic = results["results"]["organic"]
 
 
109
  elif "organic" in data:
110
  organic = data["organic"]
111
 
112
  if not organic:
113
+ return "No search results found or unexpected API response structure."
 
 
 
 
114
 
 
115
  max_results = min(20, len(organic))
116
  limited_organic = organic[:max_results]
117
 
 
118
  summary_lines = []
119
  for idx, item in enumerate(limited_organic, start=1):
120
  title = item.get("title", "No title")
 
122
  snippet = item.get("snippet", "No description")
123
  displayed_link = item.get("displayed_link", link)
124
 
 
125
  summary_lines.append(
126
  f"### Result {idx}: {title}\n\n"
127
  f"{snippet}\n\n"
128
+ f"**Source**: [{displayed_link}]({link})\n\n"
129
  f"---\n"
130
  )
131
 
 
132
  instructions = """
133
+ # Web Search Results
134
+ Below are the search results. Use this information when answering questions:
135
+ 1. Reference the title, content, and source links
136
+ 2. Explicitly cite sources in your answer (e.g., "According to source X...")
137
+ 3. Include actual source links in your response
138
+ 4. Synthesize information from multiple sources
139
  """
140
 
141
  search_results = instructions + "\n".join(summary_lines)
 
142
  return search_results
143
 
144
+ except requests.exceptions.Timeout:
145
+ logger.error("Web search timeout")
146
+ return "Web search timed out. Please try again."
147
+ except requests.exceptions.RequestException as e:
148
+ logger.error(f"Web search network error: {e}")
149
+ return "Network error during web search."
150
  except Exception as e:
151
  logger.error(f"Web search failed: {e}")
152
  return f"Web search failed: {str(e)}"
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  ##############################################################################
155
+ # File Analysis Functions
156
  ##############################################################################
157
  def analyze_csv_file(path: str) -> str:
158
+ """Analyze CSV file with size validation and encoding handling"""
159
+ if not validate_file_size(path):
160
+ return f"โš ๏ธ Error: File size exceeds {MAX_FILE_SIZE/1024/1024:.1f}MB limit."
161
+
162
  try:
163
+ encodings = ['utf-8', 'cp949', 'euc-kr', 'latin-1']
164
+ df = None
165
+
166
+ for encoding in encodings:
167
+ try:
168
+ df = pd.read_csv(path, encoding=encoding, nrows=50)
169
+ break
170
+ except UnicodeDecodeError:
171
+ continue
172
+
173
+ if df is None:
174
+ return f"Failed to read CSV: Unsupported encoding"
175
+
176
+ total_rows = len(pd.read_csv(path, encoding=encoding, usecols=[0]))
177
+
178
+ if df.shape[1] > 10:
179
+ df = df.iloc[:, :10]
180
+
181
+ summary = f"**Data size**: {total_rows} rows x {df.shape[1]} columns\n"
182
+ summary += f"**Showing**: Top {min(50, total_rows)} rows\n"
183
+ summary += f"**Columns**: {', '.join(df.columns)}\n\n"
184
+
185
  df_str = df.to_string()
186
  if len(df_str) > MAX_CONTENT_CHARS:
187
  df_str = df_str[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
188
+
189
+ return f"**[CSV File: {os.path.basename(path)}]**\n\n{summary}{df_str}"
190
  except Exception as e:
191
+ logger.error(f"CSV read error: {e}")
192
+ return f"Failed to read CSV file ({os.path.basename(path)}): {str(e)}"
193
 
194
  def analyze_txt_file(path: str) -> str:
195
+ """Analyze text file with automatic encoding detection"""
196
+ if not validate_file_size(path):
197
+ return f"โš ๏ธ Error: File size exceeds {MAX_FILE_SIZE/1024/1024:.1f}MB limit."
198
+
199
+ encodings = ['utf-8', 'cp949', 'euc-kr', 'latin-1', 'utf-16']
200
+
201
+ for encoding in encodings:
202
+ try:
203
+ with open(path, "r", encoding=encoding) as f:
204
+ text = f.read()
205
+
206
+ file_size = os.path.getsize(path)
207
+ size_info = f"**File size**: {file_size/1024:.1f}KB\n\n"
208
+
209
+ if len(text) > MAX_CONTENT_CHARS:
210
+ text = text[:MAX_CONTENT_CHARS] + "\n...(truncated)..."
211
+
212
+ return f"**[TXT File: {os.path.basename(path)}]**\n\n{size_info}{text}"
213
+ except UnicodeDecodeError:
214
+ continue
215
+
216
+ return f"Failed to read text file ({os.path.basename(path)}): Unsupported encoding"
217
 
218
  def pdf_to_markdown(pdf_path: str) -> str:
219
+ """Convert PDF to markdown with improved error handling"""
220
+ if not validate_file_size(pdf_path):
221
+ return f"โš ๏ธ Error: File size exceeds {MAX_FILE_SIZE/1024/1024:.1f}MB limit."
222
+
223
  text_chunks = []
224
  try:
225
  with open(pdf_path, "rb") as f:
226
  reader = PyPDF2.PdfReader(f)
227
+ total_pages = len(reader.pages)
228
+ max_pages = min(5, total_pages)
229
+
230
+ text_chunks.append(f"**Total pages**: {total_pages}")
231
+ text_chunks.append(f"**Showing**: First {max_pages} pages\n")
232
+
233
  for page_num in range(max_pages):
234
+ try:
235
+ page = reader.pages[page_num]
236
+ page_text = page.extract_text() or ""
237
+ page_text = page_text.strip()
238
+
239
+ if page_text:
240
+ if len(page_text) > MAX_CONTENT_CHARS // max_pages:
241
+ page_text = page_text[:MAX_CONTENT_CHARS // max_pages] + "...(truncated)"
242
+ text_chunks.append(f"## Page {page_num+1}\n\n{page_text}\n")
243
+ except Exception as e:
244
+ text_chunks.append(f"## Page {page_num+1}\n\nFailed to read page: {str(e)}\n")
245
+
246
+ if total_pages > max_pages:
247
+ text_chunks.append(f"\n...({max_pages}/{total_pages} pages shown)...")
248
  except Exception as e:
249
+ logger.error(f"PDF read error: {e}")
250
+ return f"Failed to read PDF file ({os.path.basename(pdf_path)}): {str(e)}"
251
 
252
  full_text = "\n".join(text_chunks)
253
  if len(full_text) > MAX_CONTENT_CHARS:
 
255
 
256
  return f"**[PDF File: {os.path.basename(pdf_path)}]**\n\n{full_text}"
257
 
 
258
  ##############################################################################
259
+ # File Type Check Functions
260
  ##############################################################################
261
+ def is_image_file(file_path: str) -> bool:
262
+ """Check if file is an image"""
263
+ return bool(re.search(r"\.(png|jpg|jpeg|gif|webp)$", file_path, re.IGNORECASE))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
+ def is_video_file(file_path: str) -> bool:
266
+ """Check if file is a video"""
267
+ return bool(re.search(r"\.(mp4|avi|mov|mkv)$", file_path, re.IGNORECASE))
 
 
 
 
 
 
 
 
 
 
 
268
 
269
+ def is_document_file(file_path: str) -> bool:
270
+ """Check if file is a document"""
271
+ return bool(re.search(r"\.(pdf|csv|txt)$", file_path, re.IGNORECASE))
272
 
273
  ##############################################################################
274
+ # Message Processing Functions
275
  ##############################################################################
276
+ def process_new_user_message(message: dict) -> str:
277
+ """Process user message and convert to text"""
278
+ content_parts = [message["text"]]
 
279
 
280
+ if not message.get("files"):
281
+ return message["text"]
282
+
283
+ # Classify files
284
+ csv_files = []
285
+ txt_files = []
286
+ pdf_files = []
287
+ image_files = []
288
+ video_files = []
289
+ unknown_files = []
290
 
291
+ for file_path in message["files"]:
292
+ if file_path.lower().endswith(".csv"):
293
+ csv_files.append(file_path)
294
+ elif file_path.lower().endswith(".txt"):
295
+ txt_files.append(file_path)
296
+ elif file_path.lower().endswith(".pdf"):
297
+ pdf_files.append(file_path)
298
+ elif is_image_file(file_path):
299
+ image_files.append(file_path)
300
+ elif is_video_file(file_path):
301
+ video_files.append(file_path)
302
  else:
303
+ unknown_files.append(file_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
 
305
+ # Process document files
 
 
 
 
 
 
 
 
 
 
306
  for csv_path in csv_files:
307
  csv_analysis = analyze_csv_file(csv_path)
308
+ content_parts.append(csv_analysis)
309
 
310
  for txt_path in txt_files:
311
  txt_analysis = analyze_txt_file(txt_path)
312
+ content_parts.append(txt_analysis)
313
 
314
  for pdf_path in pdf_files:
315
  pdf_markdown = pdf_to_markdown(pdf_path)
316
+ content_parts.append(pdf_markdown)
317
+
318
+ # Warning messages for unsupported files
319
+ if image_files:
320
+ image_names = [os.path.basename(f) for f in image_files]
321
+ content_parts.append(
322
+ f"\nโš ๏ธ **Image files detected**: {', '.join(image_names)}\n"
323
+ "This demo currently does not support image analysis. "
324
+ "Please describe the image content in text if you need help with it."
325
+ )
326
+
327
  if video_files:
328
+ video_names = [os.path.basename(f) for f in video_files]
329
+ content_parts.append(
330
+ f"\nโš ๏ธ **Video files detected**: {', '.join(video_names)}\n"
331
+ "This demo currently does not support video analysis. "
332
+ "Please describe the video content in text if you need help with it."
333
+ )
334
+
335
+ if unknown_files:
336
+ unknown_names = [os.path.basename(f) for f in unknown_files]
337
+ content_parts.append(
338
+ f"\nโš ๏ธ **Unsupported file format**: {', '.join(unknown_names)}\n"
339
+ "Supported formats: PDF, CSV, TXT"
340
+ )
341
+
342
+ return "\n\n".join(content_parts)
 
343
 
 
 
 
344
  def process_history(history: list[dict]) -> list[dict]:
345
+ """Convert conversation history to Friendli API format"""
346
  messages = []
347
+
348
  for item in history:
349
  if item["role"] == "assistant":
350
+ messages.append({
351
+ "role": "assistant",
352
+ "content": item["content"]
353
+ })
354
+ else: # user
355
  content = item["content"]
356
  if isinstance(content, str):
357
+ messages.append({
358
+ "role": "user",
359
+ "content": content
360
+ })
361
  elif isinstance(content, list) and len(content) > 0:
362
+ # File processing
363
+ file_info = []
364
+ for file_path in content:
365
+ if isinstance(file_path, str):
366
+ file_info.append(f"[File: {os.path.basename(file_path)}]")
367
+ if file_info:
368
+ messages.append({
369
+ "role": "user",
370
+ "content": " ".join(file_info)
371
+ })
372
+
373
  return messages
374
 
 
375
  ##############################################################################
376
+ # Streaming Response Handler
377
  ##############################################################################
378
+ def stream_friendli_response(messages: list[dict], max_tokens: int = 1000) -> Iterator[str]:
379
+ """Get streaming response from Friendli AI API"""
380
+ headers = {
381
+ "Authorization": f"Bearer {FRIENDLI_TOKEN}",
382
+ "Content-Type": "application/json"
383
+ }
384
+
385
+ payload = {
386
+ "model": FRIENDLI_MODEL_ID,
387
+ "messages": messages,
388
+ "max_tokens": max_tokens,
389
+ "top_p": 0.8,
390
+ "temperature": 0.7,
391
+ "stream": True,
392
+ "stream_options": {
393
+ "include_usage": True
394
+ }
395
+ }
396
+
397
  try:
398
+ response = requests.post(
399
+ FRIENDLI_API_URL,
400
+ headers=headers,
401
+ json=payload,
402
+ stream=True,
403
+ timeout=60
404
  )
405
+ response.raise_for_status()
406
+
407
+ full_response = ""
408
+ for line in response.iter_lines():
409
+ if line:
410
+ line_text = line.decode('utf-8')
411
+ if line_text.startswith("data: "):
412
+ data_str = line_text[6:]
413
+ if data_str == "[DONE]":
414
+ break
415
+
416
+ try:
417
+ data = json.loads(data_str)
418
+ if "choices" in data and len(data["choices"]) > 0:
419
+ delta = data["choices"][0].get("delta", {})
420
+ content = delta.get("content", "")
421
+ if content:
422
+ full_response += content
423
+ yield full_response
424
+ except json.JSONDecodeError:
425
+ logger.warning(f"JSON parsing failed: {data_str}")
426
+ continue
427
+
428
+ except requests.exceptions.Timeout:
429
+ yield "โš ๏ธ Response timeout. Please try again."
430
+ except requests.exceptions.RequestException as e:
431
+ logger.error(f"Friendli API network error: {e}")
432
+ yield f"โš ๏ธ Network error occurred: {str(e)}"
433
+ except Exception as e:
434
+ logger.error(f"Friendli API error: {str(e)}")
435
+ yield f"โš ๏ธ API call error: {str(e)}"
436
 
437
  ##############################################################################
438
+ # Main Inference Function
439
  ##############################################################################
440
+
441
  def run(
442
  message: dict,
443
  history: list[dict],
 
444
  max_new_tokens: int = 512,
445
  use_web_search: bool = False,
446
+ use_korean: bool = False,
447
+ system_prompt: str = "",
448
  ) -> Iterator[str]:
 
 
 
 
 
 
449
 
450
  try:
451
+ # Prepare system message
452
+ messages = []
453
+
454
+ if use_korean:
455
+ combined_system_msg = "๋„ˆ๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ ์—ญํ• ์ด๋‹ค. ํ•œ๊ตญ์–ด๋กœ ์นœ์ ˆํ•˜๊ณ  ์ •ํ™•ํ•˜๊ฒŒ ๋‹ต๋ณ€ํ•ด๋ผ."
456
+ else:
457
+ combined_system_msg = "You are an AI assistant. Please respond helpfully and accurately in English."
458
+
459
  if system_prompt.strip():
460
+ combined_system_msg += f"\n\n{system_prompt.strip()}"
461
+
462
+ # Web search processing
463
  if use_web_search:
464
+ user_text = message.get("text", "")
465
+ if user_text:
466
+ ws_query = extract_keywords(user_text, top_k=5)
467
+ if ws_query.strip():
468
+ logger.info(f"[Auto web search keywords] {ws_query!r}")
469
+ ws_result = do_web_search(ws_query, use_korean=use_korean)
470
+ if not ws_result.startswith("Web search"):
471
+ combined_system_msg += f"\n\n[Search Results]\n{ws_result}"
472
+ if use_korean:
473
+ combined_system_msg += "\n\n[์ค‘์š”: ๋‹ต๋ณ€์— ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์˜ ์ถœ์ฒ˜๋ฅผ ๋ฐ˜๋“œ์‹œ ์ธ์šฉํ•˜์„ธ์š”]"
474
+ else:
475
+ combined_system_msg += "\n\n[Important: Always cite sources from search results in your answer]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
+ messages.append({
478
+ "role": "system",
479
+ "content": combined_system_msg
480
+ })
 
 
 
 
 
 
 
 
481
 
482
+ # Add conversation history
483
+ messages.extend(process_history(history))
 
 
 
484
 
485
+ # Process current message
486
+ user_content = process_new_user_message(message)
487
+ messages.append({
488
+ "role": "user",
489
+ "content": user_content
490
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
 
492
+ # Debug log
493
+ logger.debug(f"Total messages: {len(messages)}")
 
 
 
494
 
495
+ # Call Friendli API and stream
496
+ for response_text in stream_friendli_response(messages, max_new_tokens):
497
+ yield response_text
498
+
499
+ except Exception as e:
500
+ logger.error(f"run function error: {str(e)}")
501
+ yield f"โš ๏ธ Sorry, an error occurred: {str(e)}"
502
 
503
  ##############################################################################
504
+ # Examples
505
  ##############################################################################
506
  examples = [
507
+ # PDF comparison example
508
  [
509
  {
510
  "text": "Compare the contents of the two PDF files.",
 
514
  ],
515
  }
516
  ],
517
+ # CSV analysis example
518
  [
519
  {
520
  "text": "Summarize and analyze the contents of the CSV file.",
521
  "files": ["assets/additional-examples/sample-csv.csv"],
522
  }
523
  ],
524
+ # Web search example
525
  [
526
  {
527
+ "text": "Explain discord.gg/openfreeai",
528
+ "files": [],
 
 
 
 
 
 
 
 
 
 
 
 
529
  }
530
  ],
531
+ # Code generation example
532
  [
533
  {
534
+ "text": "Write Python code to generate Fibonacci sequence.",
535
+ "files": [],
536
  }
537
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  ]
539
 
540
  ##############################################################################
541
+ # Gradio UI - CSS Styles (Removed blue colors)
542
  ##############################################################################
543
  css = """
544
+ /* Full width UI */
545
  .gradio-container {
546
+ background: rgba(255, 255, 255, 0.95);
547
  padding: 30px 40px;
548
+ margin: 20px auto;
549
  width: 100% !important;
550
+ max-width: none !important;
551
+ border-radius: 12px;
552
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
553
  }
554
+
555
  .fillable {
556
  width: 100% !important;
557
  max-width: 100% !important;
558
  }
559
+
560
+ /* Background */
561
  body {
562
+ background: linear-gradient(135deg, #f5f7fa 0%, #e0e0e0 100%);
563
  margin: 0;
564
  padding: 0;
565
+ font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
566
  color: #333;
567
  }
568
+
569
+ /* Button styles - neutral gray */
570
  button, .btn {
571
+ background: #6b7280 !important;
572
+ border: none;
573
+ color: white !important;
574
+ padding: 10px 20px;
575
  text-transform: uppercase;
576
+ font-weight: 600;
577
+ letter-spacing: 0.5px;
578
  cursor: pointer;
579
+ border-radius: 6px;
580
+ transition: all 0.3s ease;
581
  }
582
+
583
  button:hover, .btn:hover {
584
+ background: #4b5563 !important;
585
+ transform: translateY(-1px);
586
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
587
  }
588
 
589
+ /* Examples section */
590
  #examples_container, .examples-container {
591
+ margin: 20px auto;
592
  width: 90%;
593
+ background: rgba(255, 255, 255, 0.8);
594
+ padding: 20px;
595
+ border-radius: 8px;
596
  }
597
+
598
  #examples_row, .examples-row {
599
  justify-content: center;
 
600
  }
601
 
602
+ /* Example buttons */
603
  .gr-samples-table button,
 
 
604
  .gr-examples button,
605
+ .examples button {
606
+ background: #f0f2f5 !important;
607
+ border: 1px solid #d1d5db;
608
+ color: #374151 !important;
609
+ margin: 5px;
610
+ font-size: 14px;
 
 
611
  }
612
 
 
613
  .gr-samples-table button:hover,
 
 
614
  .gr-examples button:hover,
615
+ .examples button:hover {
616
+ background: #e5e7eb !important;
617
+ border-color: #9ca3af;
 
 
 
618
  }
619
 
620
+ /* Chat interface */
621
+ .chatbox, .chatbot {
622
+ background: white !important;
623
+ border-radius: 8px;
624
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
625
  }
626
 
627
+ .message {
628
+ padding: 15px;
629
+ margin: 10px 0;
630
+ border-radius: 8px;
631
  }
632
 
633
+ /* Input styles */
634
+ .multimodal-textbox, textarea, input[type="text"] {
635
+ background: white !important;
636
+ border: 1px solid #d1d5db;
637
+ border-radius: 6px;
638
+ padding: 10px;
639
+ font-size: 16px;
640
  }
641
 
642
+ .multimodal-textbox:focus, textarea:focus, input[type="text"]:focus {
643
+ border-color: #6b7280;
644
+ outline: none;
645
+ box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
646
  }
 
647
 
648
+ /* Warning messages */
649
+ .warning-box {
650
+ background: #fef3c7 !important;
651
+ border: 1px solid #f59e0b;
652
+ border-radius: 8px;
653
+ padding: 15px;
654
+ margin: 10px 0;
655
+ color: #92400e;
656
+ }
657
 
658
+ /* Headings */
659
+ h1, h2, h3 {
660
+ color: #1f2937;
661
+ }
662
 
663
+ /* Links - neutral gray */
664
+ a {
665
+ color: #6b7280;
666
+ text-decoration: none;
667
+ }
668
 
669
+ a:hover {
670
+ text-decoration: underline;
671
+ color: #4b5563;
672
+ }
 
673
 
674
+ /* Slider */
675
+ .gr-slider {
676
+ margin: 15px 0;
677
+ }
678
+
679
+ /* Checkbox */
680
+ input[type="checkbox"] {
681
+ width: 18px;
682
+ height: 18px;
683
+ margin-right: 8px;
684
+ }
685
+
686
+ /* Scrollbar */
687
+ ::-webkit-scrollbar {
688
+ width: 8px;
689
+ height: 8px;
690
+ }
691
+
692
+ ::-webkit-scrollbar-track {
693
+ background: #f1f1f1;
694
+ }
695
+
696
+ ::-webkit-scrollbar-thumb {
697
+ background: #888;
698
+ border-radius: 4px;
699
+ }
700
+
701
+ ::-webkit-scrollbar-thumb:hover {
702
+ background: #555;
703
+ }
704
+ """
705
+
706
+ ##############################################################################
707
+ # Gradio UI Main
708
+ ##############################################################################
709
+ with gr.Blocks(css=css, title="Gemma-3-R1984-27B Chatbot") as demo:
710
+ # Title
711
+ gr.Markdown("# ๐Ÿค— Gemma-3-R1984-27B Chatbot")
712
+ gr.Markdown("Community: [https://discord.gg/openfreeai](https://discord.gg/openfreeai)")
713
 
714
+ # UI Components
715
+ with gr.Row():
716
+ with gr.Column(scale=2):
717
+ web_search_checkbox = gr.Checkbox(
718
+ label="๐Ÿ” Enable Deep Research (Web Search)",
719
+ value=False,
720
+ info="Check for questions requiring latest information"
721
+ )
722
+ with gr.Column(scale=1):
723
+ korean_checkbox = gr.Checkbox(
724
+ label="๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ธ€ (Korean)",
725
+ value=False,
726
+ info="Check for Korean responses"
727
+ )
728
+ with gr.Column(scale=1):
729
+ max_tokens_slider = gr.Slider(
730
+ label="Max Tokens",
731
+ minimum=100,
732
+ maximum=8000,
733
+ step=50,
734
+ value=1000,
735
+ info="Adjust response length"
736
+ )
737
 
738
+ # Main chat interface
739
  chat = gr.ChatInterface(
740
  fn=run,
741
  type="messages",
742
+ chatbot=gr.Chatbot(type="messages", scale=1),
743
  textbox=gr.MultimodalTextbox(
744
  file_types=[
745
  ".webp", ".png", ".jpg", ".jpeg", ".gif",
746
  ".mp4", ".csv", ".txt", ".pdf"
747
  ],
748
  file_count="multiple",
749
+ autofocus=True,
750
+ placeholder="Enter text or upload PDF, CSV, TXT files. (Images/videos not supported in this demo)"
751
  ),
752
  multimodal=True,
753
  additional_inputs=[
 
754
  max_tokens_slider,
755
  web_search_checkbox,
756
+ korean_checkbox,
757
  ],
758
  stop_btn=False,
 
759
  examples=examples,
760
  run_examples_on_click=False,
761
  cache_examples=False,
 
762
  delete_cache=(1800, 1800),
763
  )
764
 
 
 
 
 
 
 
765
  if __name__ == "__main__":
766
+ demo.launch()