AUXteam commited on
Commit
58d52ab
·
verified ·
1 Parent(s): 0c1d8cd

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app.py +153 -151
  2. requirements.txt +2 -1
app.py CHANGED
@@ -18,6 +18,7 @@ from typing import List, Dict, Any, Optional, Tuple, Generator
18
  import traceback
19
  import base64
20
  from transformers import AutoTokenizer
 
21
 
22
 
23
 
@@ -30,10 +31,10 @@ except ImportError:
30
  # ============================================================
31
  # Configuration
32
  # ============================================================
33
- MODEL_NAME = os.getenv("MODEL_NAME", "OpenResearcher/Nemotron-3-Nano-30B-A3B")
34
- REMOTE_API_BASE = os.getenv("REMOTE_API_BASE", "")
35
- SERPER_API_KEY = os.getenv("SERPER_API_KEY", "")
36
- MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "4096")) # Safe limit for ZeroGPU
37
 
38
  # ============================================================
39
  # System Prompt & Tools
@@ -153,14 +154,20 @@ TOOL_CONTENT = """
153
  # Browser Tool Implementation
154
  # ============================================================
155
  class SimpleBrowser:
156
- """Browser tool using Serper API."""
157
 
158
- def __init__(self, serper_key: str):
159
- self.serper_key = serper_key
160
  self.pages: Dict[str, Dict] = {}
161
  self.page_stack: List[str] = []
162
  self.link_map: Dict[int, Dict] = {} # Map from cursor ID (int) to {url, title}
163
  self.used_citations = [] # List of cursor IDs (int) in order of first appearance
 
 
 
 
 
 
 
164
 
165
  @property
166
  def current_cursor(self) -> int:
@@ -178,11 +185,8 @@ class SimpleBrowser:
178
  return self.used_citations.index(cursor)
179
 
180
  def get_page_info(self, cursor: int) -> Optional[Dict[str, str]]:
181
- # Prioritize link_map as it stores search result metadata
182
  if cursor in self.link_map:
183
  return self.link_map[cursor]
184
-
185
- # Fallback to page_stack for opened pages
186
  if 0 <= cursor < len(self.page_stack):
187
  url = self.page_stack[cursor]
188
  page = self.pages.get(url)
@@ -194,71 +198,104 @@ class SimpleBrowser:
194
  lines = text.split('\n')
195
  return '\n'.join(f"L{i + offset}: {line}" for i, line in enumerate(lines))
196
 
197
- def _clean_links(self, results: List[Dict], query: str) -> Tuple[str, Dict[int, str]]:
198
- link_map = {}
199
- lines = []
200
-
201
- for i, r in enumerate(results):
202
- title = html.escape(r.get('title', 'No Title'))
203
- url = r.get('link', r.get('url', ''))
204
- snippet = html.escape(r.get('snippet', r.get('summary', '')))
205
-
206
- try:
207
- domain = url.split('/')[2] if url else ''
208
- except:
209
- domain = ''
210
-
211
- try:
212
- domain = url.split('/')[2] if url else ''
213
- except:
214
- domain = ''
215
-
216
- self.link_map[i] = {'url': url, 'title': title}
217
- link_map[i] = {'url': url, 'title': title}
218
- link_text = f"【{i}†{title}†{domain}】" if domain else f"【{i}†{title}】"
219
- lines.append(f"{link_text}")
220
- lines.append(f" {snippet}")
221
- lines.append("")
 
 
 
 
222
 
223
- return '\n'.join(lines), link_map
 
 
224
 
225
- async def search(self, query: str, topn: int = 10) -> str:
226
- url = "https://google.serper.dev/search"
227
- headers = {'X-API-KEY': self.serper_key, 'Content-Type': 'application/json'}
228
- payload = json.dumps({"q": query, "num": topn})
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
- async with httpx.AsyncClient() as client:
231
- try:
232
- response = await client.post(url, headers=headers, data=payload, timeout=20.0)
233
- if response.status_code != 200:
234
- return f"Error: Search failed with status {response.status_code}"
235
-
236
- data = response.json()
237
- results = data.get("organic", [])
238
- if not results:
239
- return f"No results found for: '{query}'"
240
-
241
- content, new_link_map = self._clean_links(results, query)
242
- self.link_map.update(new_link_map) # Merge new links
243
- pseudo_url = f"web-search://q={query}&ts={int(time.time())}"
244
- cursor = self.current_cursor + 1
245
-
246
- page_data = {
247
- 'url': pseudo_url,
248
- 'title': f"Search Results: {query}",
249
- 'text': content,
250
- 'urls': {str(k): v['url'] for k, v in new_link_map.items()}
251
  }
252
- self.pages[pseudo_url] = page_data
253
- self.page_stack.append(pseudo_url)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
- header = f"{page_data['title']} ({pseudo_url})\n**viewing lines [0 - {len(content.split(chr(10)))-1}]**\n\n"
256
- body = self._format_line_numbers(content)
257
 
258
- return f"[{cursor}] {header}{body}"
259
 
260
- except Exception as e:
261
- return f"Error during search: {str(e)}"
262
 
263
  async def open(self, id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1, **kwargs) -> str:
264
  target_url = None
@@ -288,50 +325,23 @@ class SimpleBrowser:
288
  if not target_url:
289
  return "Error: Could not determine target URL"
290
 
291
- headers = {'X-API-KEY': self.serper_key, 'Content-Type': 'application/json'}
292
- payload = json.dumps({"url": target_url})
293
-
294
- async with httpx.AsyncClient() as client:
295
- try:
296
- response = await client.post("https://scrape.serper.dev/", headers=headers, data=payload, timeout=30.0)
297
- if response.status_code != 200:
298
- return f"Error fetching URL: {response.status_code}"
299
-
300
- data = response.json()
301
- text = data.get("text", "")
302
- title = data.get("metadata", {}).get("title", "") if isinstance(data.get("metadata"), dict) else ""
303
-
304
- if not text:
305
- return f"No content found at URL"
306
-
307
- lines = text.split('\n')
308
- content = '\n'.join(lines)
309
-
310
- max_lines = 150
311
- if len(lines) > max_lines:
312
- content = '\n'.join(lines[:max_lines]) + "\n\n...(content truncated)..."
313
-
314
- new_cursor = self.current_cursor + 1
315
- page_data = {
316
- 'url': target_url,
317
- 'title': title or target_url,
318
- 'text': content,
319
- 'urls': {}
320
- }
321
- self.pages[target_url] = page_data
322
- self.page_stack.append(target_url)
323
-
324
- start = max(0, loc) if loc >= 0 else 0
325
- display_lines = content.split('\n')
326
- end = min(len(display_lines), start + num_lines) if num_lines > 0 else len(display_lines)
327
-
328
- header = f"{title or target_url} ({target_url})\n**viewing lines [{start} - {end-1}] of {len(display_lines)-1}**\n\n"
329
- body = self._format_line_numbers('\n'.join(display_lines[start:end]), offset=start)
330
-
331
- return f"[{new_cursor}] {header}{body}"
332
 
333
- except Exception as e:
334
- return f"Error fetching URL: {str(e)}"
335
 
336
  def find(self, pattern: str, cursor: int = -1) -> str:
337
  if not self.page_stack:
@@ -389,10 +399,12 @@ tokenizer = None
389
  def load_tokenizer():
390
  global tokenizer
391
  if tokenizer is None:
392
- print(f"Loading tokenizer: {MODEL_NAME}")
 
 
393
  try:
394
  tokenizer = AutoTokenizer.from_pretrained(
395
- MODEL_NAME,
396
  trust_remote_code=True
397
  )
398
  print("Tokenizer loaded successfully!")
@@ -765,7 +777,7 @@ def render_tool_result(result: str, fn_name: str) -> str:
765
  if len(result) > max_length:
766
  formatted_result = formatted_result[:max_length] + '<br><br><em style="color: #9ca3af;">...(content truncated for display)...</em>'
767
 
768
- return f'''<div class="result-card-expanded" style="border-left: 3px solid {border_color};">
769
  <div class="result-header-expanded">{tool_label}</div>
770
  <div class="result-content-expanded" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; line-height: 1.7; color: #374151;">{title_html}{formatted_result}</div>
771
  </div>'''
@@ -789,20 +801,31 @@ def render_user_message(question: str) -> str:
789
 
790
 
791
  # ============================================================
792
- # Remote API Generation (via vLLM-compatible endpoint)
793
  # ============================================================
794
 
 
 
 
 
 
 
 
 
795
 
796
  async def generate_response(prompt: str, max_new_tokens: int = MAX_NEW_TOKENS) -> str:
797
- """Generate response using vLLM OpenAI-compatible API."""
798
- # Use /completions endpoint for raw prompt
 
 
 
799
  url = f"{REMOTE_API_BASE}/completions"
800
  headers = {
801
  "Content-Type": "application/json",
802
- "ngrok-skip-browser-warning": "true", # 绕过 ngrok 免费版的浏览器警告页面
803
  }
804
  payload = {
805
- "model": MODEL_NAME,
806
  "prompt": prompt,
807
  "max_tokens": max_new_tokens,
808
  "temperature": 0.7,
@@ -814,7 +837,7 @@ async def generate_response(prompt: str, max_new_tokens: int = MAX_NEW_TOKENS) -
814
  response = await client.post(url, json=payload, headers=headers, timeout=300.0)
815
 
816
  if response.status_code != 200:
817
- raise Exception(f"vLLM API error {response.status_code}: {response.text}")
818
 
819
  data = response.json()
820
  return data["choices"][0]["text"]
@@ -825,7 +848,6 @@ async def generate_response(prompt: str, max_new_tokens: int = MAX_NEW_TOKENS) -
825
  # ============================================================
826
  async def run_agent_streaming(
827
  question: str,
828
- serper_key: str,
829
  max_rounds: int
830
  ) -> Generator[str, None, None]:
831
  global tokenizer
@@ -834,14 +856,6 @@ async def run_agent_streaming(
834
  yield "<p style='color: var(--body-text-color-subdued); text-align: center; padding: 2rem;'>Please enter a question to begin.</p>"
835
  return
836
 
837
- if not serper_key:
838
- yield """<div class="error-message">
839
- <p><strong>Serper API Key Required</strong></p>
840
- <p>Please configure your Serper API Key in the left sidebar under <strong>Settings</strong>.</p>
841
- <p>Don't have an API key? <a href="https://serper.dev/" target="_blank" style="color: #667eea; text-decoration: underline;">Get one here →</a></p>
842
- </div>"""
843
- return
844
-
845
  # Load tokenizer for prompt formatting
846
  try:
847
  load_tokenizer()
@@ -849,7 +863,7 @@ async def run_agent_streaming(
849
  yield f"<p style='color:#dc2626;'>Error loading tokenizer: {html.escape(str(e))}</p>"
850
  return
851
 
852
- browser = SimpleBrowser(serper_key)
853
  tools = json.loads(TOOL_CONTENT)
854
 
855
  system_prompt = DEVELOPER_CONTENT + f"\n\nToday's date: {datetime.now().strftime('%Y-%m-%d')}"
@@ -886,7 +900,7 @@ async def run_agent_streaming(
886
  html_parts.append('<div class="thinking-streaming">Processing...</div>')
887
  yield ''.join(html_parts)
888
 
889
- # Call ZeroGPU function
890
  generated = await generate_response(prompt, max_new_tokens=MAX_NEW_TOKENS)
891
 
892
  # Remove placeholder
@@ -947,7 +961,7 @@ async def run_agent_streaming(
947
  result = ""
948
  try:
949
  if actual_fn == "search":
950
- result = await browser.search(args.get("query", ""), args.get("topn", 10))
951
  elif actual_fn == "open":
952
  result = await browser.open(**args)
953
  elif actual_fn == "find":
@@ -2344,15 +2358,13 @@ def create_interface():
2344
  """
2345
 
2346
  with gr.Blocks(css=INLINE_CSS, theme=gr.themes.Soft(), js=CAROUSEL_JS) as demo:
2347
- # Header with logo and title images - convert to base64 for proper rendering
2348
- # Files are in the same directory as app.py (test1/)
2349
  logo_path = os.path.join(script_dir, "or-logo1.png")
2350
  title_path = os.path.join(script_dir, "openresearcher-title.svg")
2351
 
2352
  logo_base64 = image_to_base64(logo_path)
2353
  title_base64 = image_to_base64(title_path)
2354
 
2355
- # Build header HTML with base64 images
2356
  header_html = f"""
2357
  <div style="
2358
  text-align: center;
@@ -2406,16 +2418,6 @@ def create_interface():
2406
  <span class="settings-title">⚙️ Settings</span>
2407
  </div>
2408
  ''')
2409
- serper_input = gr.Textbox(
2410
- label="",
2411
- value=SERPER_API_KEY,
2412
- type="password",
2413
- placeholder="Enter your Serper API key...",
2414
- show_label=False,
2415
- elem_id="serper-api-input",
2416
- container=False,
2417
- visible=False
2418
- )
2419
  max_rounds_input = gr.Slider(
2420
  minimum=1,
2421
  maximum=200,
@@ -2513,7 +2515,7 @@ def create_interface():
2513
  clear_btn = gr.Button("🗑 Clear", scale=1)
2514
 
2515
  # Function to hide welcome and show output
2516
- async def start_research(question, serper_key, max_rounds):
2517
  # Generator that first hides welcome, then streams results
2518
  # Also clears the input box for the next question
2519
 
@@ -2521,13 +2523,13 @@ def create_interface():
2521
  # IMPORTANT: Don't use empty string for output, or JS will hide the output area!
2522
  yield "", '<div style="text-align: center; padding: 2rem; color: #6b7280;">Delving into it...</div>', ""
2523
 
2524
- async for result in run_agent_streaming(question, serper_key, max_rounds):
2525
  yield "", result, ""
2526
 
2527
  # Event handlers
2528
  submit_event = submit_btn.click(
2529
  fn=start_research,
2530
- inputs=[question_input, serper_input, max_rounds_input],
2531
  outputs=[welcome_html, output_area, question_input],
2532
  show_progress="hidden",
2533
  concurrency_limit=20
@@ -2535,7 +2537,7 @@ def create_interface():
2535
 
2536
  question_input.submit(
2537
  fn=start_research,
2538
- inputs=[question_input, serper_input, max_rounds_input],
2539
  outputs=[welcome_html, output_area, question_input],
2540
  show_progress="hidden",
2541
  concurrency_limit=20
@@ -2563,7 +2565,7 @@ def create_interface():
2563
 
2564
  if __name__ == "__main__":
2565
  print("="*60)
2566
- print("OpenResearcher DeepSearch Agent - ZeroGPU Space")
2567
  print("="*60)
2568
  demo = create_interface()
2569
- demo.queue(default_concurrency_limit=20).launch()
 
18
  import traceback
19
  import base64
20
  from transformers import AutoTokenizer
21
+ from gradio_client import Client as GradioClient
22
 
23
 
24
 
 
31
  # ============================================================
32
  # Configuration
33
  # ============================================================
34
+ MODEL_NAME = os.getenv("MODEL_NAME", "alias-fast")
35
+ REMOTE_API_BASE = os.getenv("REMOTE_API_BASE", "https://api.helmholtz-blablador.fz-juelich.de/v1")
36
+ BLABLADOR_API_KEY = os.getenv("BLABLADOR_API_KEY", "")
37
+ MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "4096"))
38
 
39
  # ============================================================
40
  # System Prompt & Tools
 
154
  # Browser Tool Implementation
155
  # ============================================================
156
  class SimpleBrowser:
157
+ """Browser tool using victor/websearch Gradio API."""
158
 
159
+ def __init__(self):
 
160
  self.pages: Dict[str, Dict] = {}
161
  self.page_stack: List[str] = []
162
  self.link_map: Dict[int, Dict] = {} # Map from cursor ID (int) to {url, title}
163
  self.used_citations = [] # List of cursor IDs (int) in order of first appearance
164
+ try:
165
+ # victor/websearch is a public space, but we can pass token if available
166
+ hf_token = os.getenv("HF_TOKEN", "")
167
+ self.client = GradioClient("victor/websearch", hf_token=hf_token if hf_token else None)
168
+ except Exception as e:
169
+ print(f"Error initializing Gradio client: {e}")
170
+ self.client = None
171
 
172
  @property
173
  def current_cursor(self) -> int:
 
185
  return self.used_citations.index(cursor)
186
 
187
  def get_page_info(self, cursor: int) -> Optional[Dict[str, str]]:
 
188
  if cursor in self.link_map:
189
  return self.link_map[cursor]
 
 
190
  if 0 <= cursor < len(self.page_stack):
191
  url = self.page_stack[cursor]
192
  page = self.pages.get(url)
 
198
  lines = text.split('\n')
199
  return '\n'.join(f"L{i + offset}: {line}" for i, line in enumerate(lines))
200
 
201
+ def _parse_websearch_output(self, output: str) -> List[Dict]:
202
+ results = []
203
+ # Split by the separator ---, handling potential variations in newlines
204
+ parts = re.split(r'\n---\n|^\s*---\s*$', output, flags=re.MULTILINE)
205
+ for part in parts:
206
+ part = part.strip()
207
+ if not part or "Successfully extracted content" in part:
208
+ continue
209
+
210
+ title_match = re.search(r'## (.*)', part)
211
+ domain_match = re.search(r'\*\*Domain:\*\* (.*)', part)
212
+ url_match = re.search(r'\*\*URL:\*\* (.*)', part)
213
+
214
+ if title_match and url_match:
215
+ title = title_match.group(1).strip()
216
+ url = url_match.group(1).strip()
217
+ domain = domain_match.group(1).strip() if domain_match else ""
218
+
219
+ # Content starts after metadata
220
+ metadata_end = url_match.end()
221
+ content = part[metadata_end:].strip()
222
+
223
+ results.append({
224
+ 'title': title,
225
+ 'url': url,
226
+ 'domain': domain,
227
+ 'content': content
228
+ })
229
+ return results
230
 
231
+ async def search(self, query: str, topn: int = 4) -> str:
232
+ if not self.client:
233
+ return "Error: Search client not initialized"
234
 
235
+ try:
236
+ # Call the Gradio API
237
+ loop = asyncio.get_event_loop()
238
+ result_str = await loop.run_in_executor(
239
+ None,
240
+ lambda: self.client.predict(
241
+ query=query,
242
+ search_type="search",
243
+ num_results=topn,
244
+ api_name="/search_web"
245
+ )
246
+ )
247
+
248
+ results = self._parse_websearch_output(result_str)
249
+ if not results:
250
+ return f"No results found for: '{query}'"
251
 
252
+ # Populate pages and link_map
253
+ new_link_map = {}
254
+ lines = []
255
+
256
+ for i, r in enumerate(results):
257
+ title = r['title']
258
+ url = r['url']
259
+ domain = r['domain']
260
+ content = r['content']
261
+
262
+ # Create a snippet for the search result view
263
+ snippet = content[:200].replace('\n', ' ') + "..."
264
+
265
+ self.link_map[i] = {'url': url, 'title': title}
266
+ new_link_map[i] = {'url': url, 'title': title}
267
+
268
+ # Cache the full content
269
+ self.pages[url] = {
270
+ 'url': url,
271
+ 'title': title,
272
+ 'text': content
273
  }
274
+
275
+ link_text = f"【{i}†{title}†{domain}】" if domain else f"【{i}†{title}】"
276
+ lines.append(f"{link_text}")
277
+ lines.append(f" {snippet}")
278
+ lines.append("")
279
+
280
+ formatted_content = '\n'.join(lines)
281
+ pseudo_url = f"web-search://q={query}&ts={int(time.time())}"
282
+ cursor = self.current_cursor + 1
283
+
284
+ self.pages[pseudo_url] = {
285
+ 'url': pseudo_url,
286
+ 'title': f"Search Results: {query}",
287
+ 'text': formatted_content,
288
+ 'urls': {str(k): v['url'] for k, v in new_link_map.items()}
289
+ }
290
+ self.page_stack.append(pseudo_url)
291
 
292
+ header = f"Search Results: {query} ({pseudo_url})\n**viewing lines [0 - {len(formatted_content.split(chr(10)))-1}]**\n\n"
293
+ body = self._format_line_numbers(formatted_content)
294
 
295
+ return f"[{cursor}] {header}{body}"
296
 
297
+ except Exception as e:
298
+ return f"Error during search: {str(e)}"
299
 
300
  async def open(self, id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1, **kwargs) -> str:
301
  target_url = None
 
325
  if not target_url:
326
  return "Error: Could not determine target URL"
327
 
328
+ # Check if we already have the page content cached
329
+ if target_url in self.pages:
330
+ page = self.pages[target_url]
331
+ text = page['text']
332
+ lines = text.split('\n')
333
+
334
+ new_cursor = self.current_cursor + 1
335
+ self.page_stack.append(target_url)
336
+
337
+ start = max(0, loc) if loc >= 0 else 0
338
+ end = min(len(lines), start + num_lines) if num_lines > 0 else len(lines)
339
+
340
+ header = f"{page['title']} ({target_url})\n**viewing lines [{start} - {end-1}] of {len(lines)-1}**\n\n"
341
+ body = self._format_line_numbers('\n'.join(lines[start:end]), offset=start)
342
+ return f"[{new_cursor}] {header}{body}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
+ return f"Error: Content for {target_url} not found in search results. The current search API only provides content for pages returned in search results."
 
345
 
346
  def find(self, pattern: str, cursor: int = -1) -> str:
347
  if not self.page_stack:
 
399
  def load_tokenizer():
400
  global tokenizer
401
  if tokenizer is None:
402
+ # We use Nemotron as a proxy tokenizer for token counting
403
+ token_model = "OpenResearcher/Nemotron-3-Nano-30B-A3B"
404
+ print(f"Loading tokenizer: {token_model}")
405
  try:
406
  tokenizer = AutoTokenizer.from_pretrained(
407
+ token_model,
408
  trust_remote_code=True
409
  )
410
  print("Tokenizer loaded successfully!")
 
777
  if len(result) > max_length:
778
  formatted_result = formatted_result[:max_length] + '<br><br><em style="color: #9ca3af;">...(content truncated for display)...</em>'
779
 
780
+ return f'''<div class="result-card-expanded" style="border-left: 3_solid {border_color};">
781
  <div class="result-header-expanded">{tool_label}</div>
782
  <div class="result-content-expanded" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; line-height: 1.7; color: #374151;">{title_html}{formatted_result}</div>
783
  </div>'''
 
801
 
802
 
803
  # ============================================================
804
+ # Remote API Generation (via OpenAI-compatible endpoint)
805
  # ============================================================
806
 
807
+ def count_tokens(text: str) -> int:
808
+ """Count tokens in text using the loaded tokenizer."""
809
+ try:
810
+ tok = load_tokenizer()
811
+ return len(tok.encode(text))
812
+ except Exception:
813
+ # Fallback to rough estimate if tokenizer fails
814
+ return len(text) // 4
815
 
816
  async def generate_response(prompt: str, max_new_tokens: int = MAX_NEW_TOKENS) -> str:
817
+ """Generate response using OpenAI-compatible API with model switching."""
818
+ # Choose model based on prompt length
819
+ prompt_tokens = count_tokens(prompt)
820
+ selected_model = "alias-large" if prompt_tokens > 4000 else "alias-fast"
821
+
822
  url = f"{REMOTE_API_BASE}/completions"
823
  headers = {
824
  "Content-Type": "application/json",
825
+ "Authorization": f"Bearer {BLABLADOR_API_KEY}"
826
  }
827
  payload = {
828
+ "model": selected_model,
829
  "prompt": prompt,
830
  "max_tokens": max_new_tokens,
831
  "temperature": 0.7,
 
837
  response = await client.post(url, json=payload, headers=headers, timeout=300.0)
838
 
839
  if response.status_code != 200:
840
+ raise Exception(f"LLM API error {response.status_code}: {response.text}")
841
 
842
  data = response.json()
843
  return data["choices"][0]["text"]
 
848
  # ============================================================
849
  async def run_agent_streaming(
850
  question: str,
 
851
  max_rounds: int
852
  ) -> Generator[str, None, None]:
853
  global tokenizer
 
856
  yield "<p style='color: var(--body-text-color-subdued); text-align: center; padding: 2rem;'>Please enter a question to begin.</p>"
857
  return
858
 
 
 
 
 
 
 
 
 
859
  # Load tokenizer for prompt formatting
860
  try:
861
  load_tokenizer()
 
863
  yield f"<p style='color:#dc2626;'>Error loading tokenizer: {html.escape(str(e))}</p>"
864
  return
865
 
866
+ browser = SimpleBrowser()
867
  tools = json.loads(TOOL_CONTENT)
868
 
869
  system_prompt = DEVELOPER_CONTENT + f"\n\nToday's date: {datetime.now().strftime('%Y-%m-%d')}"
 
900
  html_parts.append('<div class="thinking-streaming">Processing...</div>')
901
  yield ''.join(html_parts)
902
 
903
+ # Call generation function
904
  generated = await generate_response(prompt, max_new_tokens=MAX_NEW_TOKENS)
905
 
906
  # Remove placeholder
 
961
  result = ""
962
  try:
963
  if actual_fn == "search":
964
+ result = await browser.search(args.get("query", ""), args.get("topn", 4))
965
  elif actual_fn == "open":
966
  result = await browser.open(**args)
967
  elif actual_fn == "find":
 
2358
  """
2359
 
2360
  with gr.Blocks(css=INLINE_CSS, theme=gr.themes.Soft(), js=CAROUSEL_JS) as demo:
2361
+ # Header with logo and title images
 
2362
  logo_path = os.path.join(script_dir, "or-logo1.png")
2363
  title_path = os.path.join(script_dir, "openresearcher-title.svg")
2364
 
2365
  logo_base64 = image_to_base64(logo_path)
2366
  title_base64 = image_to_base64(title_path)
2367
 
 
2368
  header_html = f"""
2369
  <div style="
2370
  text-align: center;
 
2418
  <span class="settings-title">⚙️ Settings</span>
2419
  </div>
2420
  ''')
 
 
 
 
 
 
 
 
 
 
2421
  max_rounds_input = gr.Slider(
2422
  minimum=1,
2423
  maximum=200,
 
2515
  clear_btn = gr.Button("🗑 Clear", scale=1)
2516
 
2517
  # Function to hide welcome and show output
2518
+ async def start_research(question, max_rounds):
2519
  # Generator that first hides welcome, then streams results
2520
  # Also clears the input box for the next question
2521
 
 
2523
  # IMPORTANT: Don't use empty string for output, or JS will hide the output area!
2524
  yield "", '<div style="text-align: center; padding: 2rem; color: #6b7280;">Delving into it...</div>', ""
2525
 
2526
+ async for result in run_agent_streaming(question, max_rounds):
2527
  yield "", result, ""
2528
 
2529
  # Event handlers
2530
  submit_event = submit_btn.click(
2531
  fn=start_research,
2532
+ inputs=[question_input, max_rounds_input],
2533
  outputs=[welcome_html, output_area, question_input],
2534
  show_progress="hidden",
2535
  concurrency_limit=20
 
2537
 
2538
  question_input.submit(
2539
  fn=start_research,
2540
+ inputs=[question_input, max_rounds_input],
2541
  outputs=[welcome_html, output_area, question_input],
2542
  show_progress="hidden",
2543
  concurrency_limit=20
 
2565
 
2566
  if __name__ == "__main__":
2567
  print("="*60)
2568
+ print("OpenResearcher DeepSearch Agent - Helmholtz Blablador Provider")
2569
  print("="*60)
2570
  demo = create_interface()
2571
+ demo.queue(default_concurrency_limit=20).launch()
requirements.txt CHANGED
@@ -8,4 +8,5 @@ bitsandbytes
8
  sentencepiece
9
  protobuf
10
  json5
11
- accelerate
 
 
8
  sentencepiece
9
  protobuf
10
  json5
11
+ accelerate
12
+ gradio_client