AUXteam's picture
Add debug logging for API key
56ad74b verified
# -*- coding: utf-8 -*-
"""
OpenResearcher DeepSearch Agent - Hugging Face Space
Uses ZeroGPU for efficient inference with the Nemotron model
Aligned with app_local.py frontend and logic
"""
import os
import gradio as gr
import httpx
import json
import json5
import re
import time
import html
import asyncio
from datetime import datetime
from typing import List, Dict, Any, Optional, Tuple, Generator
import traceback
import base64
from transformers import AutoTokenizer
from gradio_client import Client as GradioClient
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
# ============================================================
# Configuration
# ============================================================
MODEL_NAME = os.getenv("MODEL_NAME", "alias-fast")
REMOTE_API_BASE = os.getenv("REMOTE_API_BASE", "https://api.helmholtz-blablador.fz-juelich.de/v1")
BLABLADOR_API_KEY = os.getenv("BLABLADOR_API_KEY", "")
MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "4096"))
# ============================================================
# System Prompt & Tools
# ============================================================
DEVELOPER_CONTENT = """
You are a helpful assistant and harmless assistant.
You will be able to use a set of browsering tools to answer user queries.
Tool for browsing.
The `cursor` appears in brackets before each browsing display: `[{cursor}]`.
Cite information from the tool using the following format:
`ใ€{cursor}โ€ L{line_start}(-L{line_end})?ใ€‘`, for example: `ใ€6โ€ L9-L11ใ€‘` or `ใ€8โ€ L3ใ€‘`.
Do not quote more than 10 words directly from the tool output.
sources=web
""".strip()
TOOL_CONTENT = """
[
{
"type": "function",
"function": {
"name": "browser.search",
"description": "Searches for information related to a query and displays top N results. Returns a list of search results with titles, URLs, and summaries.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query string"
},
"topn": {
"type": "integer",
"description": "Number of results to display",
"default": 10
}
},
"required": [
"query"
]
}
}
},
{
"type": "function",
"function": {
"name": "browser.open",
"description": "Opens a link from the current page or a fully qualified URL. Can scroll to a specific location and display a specific number of lines. Valid link ids are displayed with the formatting: ใ€{id}โ€ .*ใ€‘.",
"parameters": {
"type": "object",
"properties": {
"id": {
"type": [
"integer",
"string"
],
"description": "Link id from current page (integer) or fully qualified URL (string). Default is -1 (most recent page)",
"default": -1
},
"cursor": {
"type": "integer",
"description": "Page cursor to operate on. If not provided, the most recent page is implied",
"default": -1
},
"loc": {
"type": "integer",
"description": "Starting line number. If not provided, viewport will be positioned at the beginning or centered on relevant passage",
"default": -1
},
"num_lines": {
"type": "integer",
"description": "Number of lines to display",
"default": -1
},
"view_source": {
"type": "boolean",
"description": "Whether to view page source",
"default": false
},
"source": {
"type": "string",
"description": "The source identifier (e.g., 'web')"
}
},
"required": []
}
}
},
{
"type": "function",
"function": {
"name": "browser.find",
"description": "Finds exact matches of a pattern in the current page or a specified page by cursor.",
"parameters": {
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "The exact text pattern to search for"
},
"cursor": {
"type": "integer",
"description": "Page cursor to search in. If not provided, searches in the current page",
"default": -1
}
},
"required": [
"pattern"
]
}
}
}
]
""".strip()
# ============================================================
# Browser Tool Implementation
# ============================================================
class SimpleBrowser:
"""Browser tool using victor/websearch Gradio API."""
def __init__(self):
self.pages: Dict[str, Dict] = {}
self.page_stack: List[str] = []
self.link_map: Dict[int, Dict] = {} # Map from cursor ID (int) to {url, title}
self.used_citations = [] # List of cursor IDs (int) in order of first appearance
try:
# victor/websearch is a public space, but we can pass token if available
hf_token = os.getenv("HF_TOKEN", "")
# Use 'token' instead of 'hf_token' for Gradio Client
self.client = GradioClient("victor/websearch", token=hf_token if hf_token else None)
except Exception as e:
print(f"Error initializing Gradio client: {e}")
self.client = None
@property
def current_cursor(self) -> int:
return len(self.page_stack) - 1
def add_link(self, cursor: int, url: str, title: str = ""):
self.link_map[cursor] = {'url': url, 'title': title}
def get_link_info(self, cursor: int) -> Optional[dict]:
return self.link_map.get(cursor)
def get_citation_index(self, cursor: int) -> int:
if cursor not in self.used_citations:
self.used_citations.append(cursor)
return self.used_citations.index(cursor)
def get_page_info(self, cursor: int) -> Optional[Dict[str, str]]:
if cursor in self.link_map:
return self.link_map[cursor]
if 0 <= cursor < len(self.page_stack):
url = self.page_stack[cursor]
page = self.pages.get(url)
if page:
return {'url': url, 'title': page.get('title', '')}
return None
def _format_line_numbers(self, text: str, offset: int = 0) -> str:
lines = text.split('\n')
return '\n'.join(f"L{i + offset}: {line}" for i, line in enumerate(lines))
def _parse_websearch_output(self, output: str) -> List[Dict]:
results = []
# Split by the separator ---, handling potential variations in newlines
parts = re.split(r'\n---\n|^\s*---\s*$', output, flags=re.MULTILINE)
for part in parts:
part = part.strip()
if not part or "Successfully extracted content" in part:
continue
title_match = re.search(r'## (.*)', part)
domain_match = re.search(r'\*\*Domain:\*\* (.*)', part)
url_match = re.search(r'\*\*URL:\*\* (.*)', part)
if title_match and url_match:
title = title_match.group(1).strip()
url = url_match.group(1).strip()
domain = domain_match.group(1).strip() if domain_match else ""
# Content starts after metadata
metadata_end = url_match.end()
content = part[metadata_end:].strip()
results.append({
'title': title,
'url': url,
'domain': domain,
'content': content
})
return results
async def search(self, query: str, topn: int = 4) -> str:
if not self.client:
return "Error: Search client not initialized"
try:
# Call the Gradio API
loop = asyncio.get_event_loop()
result_str = await loop.run_in_executor(
None,
lambda: self.client.predict(
query=query,
search_type="search",
num_results=topn,
api_name="/search_web"
)
)
results = self._parse_websearch_output(result_str)
if not results:
return f"No results found for: '{query}'"
# Populate pages and link_map
new_link_map = {}
lines = []
for i, r in enumerate(results):
title = r['title']
url = r['url']
domain = r['domain']
content = r['content']
# Create a snippet for the search result view
snippet = content[:200].replace('\n', ' ') + "..."
self.link_map[i] = {'url': url, 'title': title}
new_link_map[i] = {'url': url, 'title': title}
# Cache the full content
self.pages[url] = {
'url': url,
'title': title,
'text': content
}
link_text = f"ใ€{i}โ€ {title}โ€ {domain}ใ€‘" if domain else f"ใ€{i}โ€ {title}ใ€‘"
lines.append(f"{link_text}")
lines.append(f" {snippet}")
lines.append("")
formatted_content = '\n'.join(lines)
pseudo_url = f"web-search://q={query}&ts={int(time.time())}"
cursor = self.current_cursor + 1
self.pages[pseudo_url] = {
'url': pseudo_url,
'title': f"Search Results: {query}",
'text': formatted_content,
'urls': {str(k): v['url'] for k, v in new_link_map.items()}
}
self.page_stack.append(pseudo_url)
header = f"Search Results: {query} ({pseudo_url})\n**viewing lines [0 - {len(formatted_content.split(chr(10)))-1}]**\n\n"
body = self._format_line_numbers(formatted_content)
return f"[{cursor}] {header}{body}"
except Exception as e:
return f"Error during search: {str(e)}"
async def open(self, id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1, **kwargs) -> str:
target_url = None
if isinstance(id, str) and id.startswith("http"):
target_url = id
elif isinstance(id, int) and id >= 0:
info = self.link_map.get(id)
target_url = info['url'] if info else None
if not target_url:
return f"Error: Invalid link id '{id}'. Available: {list(self.link_map.keys())}"
elif cursor >= 0 and cursor < len(self.page_stack):
page_url = self.page_stack[cursor]
page = self.pages.get(page_url)
if page:
text = page['text']
lines = text.split('\n')
start = max(0, loc) if loc >= 0 else 0
end = min(len(lines), start + num_lines) if num_lines > 0 else len(lines)
header = f"{page['title']} ({page['url']})\n**viewing lines [{start} - {end-1}] of {len(lines)-1}**\n\n"
body = self._format_line_numbers('\n'.join(lines[start:end]), offset=start)
return f"[{cursor}] {header}{body}"
else:
return "Error: No valid target specified"
if not target_url:
return "Error: Could not determine target URL"
# Check if we already have the page content cached
if target_url in self.pages:
page = self.pages[target_url]
text = page['text']
lines = text.split('\n')
new_cursor = self.current_cursor + 1
self.page_stack.append(target_url)
start = max(0, loc) if loc >= 0 else 0
end = min(len(lines), start + num_lines) if num_lines > 0 else len(lines)
header = f"{page['title']} ({target_url})\n**viewing lines [{start} - {end-1}] of {len(lines)-1}**\n\n"
body = self._format_line_numbers('\n'.join(lines[start:end]), offset=start)
return f"[{new_cursor}] {header}{body}"
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."
def find(self, pattern: str, cursor: int = -1) -> str:
if not self.page_stack:
return "Error: No page open"
page_url = self.page_stack[cursor] if cursor >= 0 and cursor < len(self.page_stack) else self.page_stack[-1]
page = self.pages.get(page_url)
if not page:
return "Error: Page not found"
text = page['text']
lines = text.split('\n')
matches = []
for i, line in enumerate(lines):
if str(pattern).lower() in line.lower():
start = max(0, i - 1)
end = min(len(lines), i + 3)
context = '\n'.join(f"L{j}: {lines[j]}" for j in range(start, end))
matches.append(f"# ใ€{len(matches)}โ€ match at L{i}ใ€‘\n{context}")
if len(matches) >= 10:
break
if not matches:
return f"No matches found for: '{pattern}'"
result_url = f"{page_url}/find?pattern={pattern}"
new_cursor = self.current_cursor + 1
result_content = '\n\n'.join(matches)
page_data = {
'url': result_url,
'title': f"Find results for: '{pattern}'",
'text': result_content,
'urls': {}
}
self.pages[result_url] = page_data
self.page_stack.append(result_url)
header = f"Find results for text: `{pattern}` in `{page['title']}`\n\n"
return f"[{new_cursor}] {header}{result_content}"
def get_cursor_url(self, cursor: int) -> Optional[str]:
if cursor >= 0 and cursor < len(self.page_stack):
return self.page_stack[cursor]
return None
# ============================================================
# Tokenizer Loading
# ============================================================
tokenizer = None
def load_tokenizer():
global tokenizer
if tokenizer is None:
# We use Nemotron as a proxy tokenizer for token counting
token_model = "OpenResearcher/Nemotron-3-Nano-30B-A3B"
print(f"Loading tokenizer: {token_model}")
try:
tokenizer = AutoTokenizer.from_pretrained(
token_model,
trust_remote_code=True
)
print("Tokenizer loaded successfully!")
except Exception as e:
print(f"Error loading tokenizer: {e}")
import traceback
traceback.print_exc()
raise
return tokenizer
# ============================================================
# Text Processing
# ============================================================
def extract_thinking(text: str) -> Tuple[Optional[str], str]:
reasoning_content = None
content = text
if '<think>' in content and '</think>' in content:
match = re.search(r'<think>(.*?)</think>', content, re.DOTALL)
if match:
reasoning_content = match.group(1).strip()
content = content.replace(match.group(0), "").strip()
elif '</think>' in content:
match = re.search(r'^(.*?)</think>', content, re.DOTALL)
if match:
reasoning_content = match.group(1).strip()
content = content.replace(match.group(0), "").strip()
return reasoning_content, content
def parse_tool_call(text: str) -> Tuple[Optional[Dict], str]:
tool_call_text = None
content = text
if '<tool_call>' in content and '</tool_call>' in content:
match = re.search(r'<tool_call>(.*?)</tool_call>', content, re.DOTALL)
if match:
tool_call_text = match.group(1).strip()
content = content.replace(match.group(0), "").strip()
elif '</tool_call>' in content:
match = re.search(r'^(.*?)</tool_call>', content, re.DOTALL)
if match:
tool_call_text = match.group(1).strip()
content = content.replace(match.group(0), "").strip()
if tool_call_text:
try:
if "```json" in tool_call_text:
tool_call_text = tool_call_text.split("```json")[1].split("```")[0].strip()
elif "```" in tool_call_text:
tool_call_text = tool_call_text.split("```")[1].split("```")[0].strip()
parsed = json5.loads(tool_call_text)
return parsed, content
except:
pass
func_match = re.search(r'<function=([\w.]+)>', tool_call_text)
if func_match:
tool_name = func_match.group(1)
tool_args = {}
params = re.finditer(r'<parameter=([\w]+)>\s*(.*?)\s*</parameter>', tool_call_text, re.DOTALL)
for p in params:
param_name = p.group(1)
param_value = p.group(2).strip()
if param_value.startswith('"') and param_value.endswith('"'):
param_value = param_value[1:-1]
try:
if param_value.isdigit():
param_value = int(param_value)
except:
pass
tool_args[param_name] = param_value
return {"name": tool_name, "arguments": tool_args}, content
return None, content
def is_final_answer(text: str) -> bool:
t = text.lower()
return (
('<answer>' in t and '</answer>' in t) or
'final answer:' in t or
('exact answer:' in t and 'confidence:' in t)
)
# ============================================================
# HTML Rendering Helpers (From app_local.py)
# ============================================================
def render_citations(text: str, browser: SimpleBrowser) -> str:
"""Convert citation markers to clickable HTML links."""
def replace_citation(m):
cursor_str = m.group(1)
# l1 = m.group(2)
# l2 = m.group(3)
try:
cursor = int(cursor_str)
index = browser.get_citation_index(cursor)
# Check if we have URL info
info = browser.get_page_info(cursor)
if info and info.get('url'):
# Return clickable index link pointing to reference section
# Aligned with generate_html_example.py style (green via CSS class)
url = info.get('url')
return f'<a href="{html.escape(url)}" target="_blank" class="citation-link">[{index}]</a>'
# Fallback if no URL
return f'<span class="citation-link">[{index}]</span>'
except Exception as e:
# print(f"Error in replace_citation: {e}, match: {m.group(0)}")
pass
return m.group(0)
# First pass: replace citations with linked citations
result = re.sub(r'[ใ€\[](\d+)โ€ .*?[ใ€‘\]]', replace_citation, text)
# Second pass: Deduplicate adjacent identical citations
# Matches: <a ...>[N]</a> followed by optional whitespace and same link
# We repeat this until no more changes to handle multiple duplicates
while True:
new_result = re.sub(r'(<a [^>]+>\[\d+\]</a>)(\s*)\1', r'\1', result)
if new_result == result:
break
result = new_result
# Convert basic markdown to HTML
result = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', result)
result = re.sub(r'\*(.+?)\*', r'<em>\1</em>', result)
result = re.sub(r'`(.+?)`', r'<code>\1</code>', result)
result = result.replace('\n\n', '</p><p>').replace('\n', '<br>')
if not result.startswith('<p>'):
result = f'<p>{result}</p>'
return result
def render_thinking_streaming(text: str) -> str:
"""Render thinking content in streaming mode (visible, with animation)."""
escaped = html.escape(text)
return f'<div class="thinking-streaming">{escaped}</div>'
def render_thinking_collapsed(text: str) -> str:
"""Render thinking content in collapsed mode after completion."""
escaped = html.escape(text)
preview = text[:100] + "..." if len(text) > 100 else text
preview_escaped = html.escape(preview)
return f'''<details class="thinking-collapsed">
<summary>Thought process: "{preview_escaped}"</summary>
<div class="thinking-content">{escaped}</div>
</details>'''
def render_tool_call(fn_name: str, args: dict, browser: SimpleBrowser = None) -> str:
"""Render a tool call card with unified format and subtle distinction."""
border_colors = {
"browser.search": "#667eea",
"browser.open": "#4facfe",
"browser.find": "#fa709a"
}
border_color = border_colors.get(fn_name, "#9ca3af")
if fn_name == "browser.search":
query = str(args.get('query', ''))
return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
<div class="tool-info">
<div class="tool-name">Searching the web</div>
<div class="tool-detail">Query: "{html.escape(query)}"</div>
</div>
</div>'''
elif fn_name == "browser.open":
link_id = args.get('id', '')
url_info = ""
if browser and isinstance(link_id, int) and link_id >= 0:
info = browser.link_map.get(link_id)
url = info.get('url', "") if info else ""
if url:
try:
domain = url.split('/')[2]
url_info = f" โ†’ {domain}"
except:
url_info = ""
return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
<div class="tool-info">
<div class="tool-name">Opening page</div>
<div class="tool-detail">Link #{link_id}{url_info}</div>
</div>
</div>'''
elif fn_name == "browser.find":
pattern = str(args.get('pattern', ''))
return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
<div class="tool-info">
<div class="tool-name">Finding in page</div>
<div class="tool-detail">Pattern: "{html.escape(pattern)}"</div>
</div>
</div>'''
else:
return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
<div class="tool-info">
<div class="tool-name">{html.escape(str(fn_name))}</div>
<div class="tool-detail">{html.escape(json.dumps(args))}</div>
</div>
</div>'''
def render_tool_result(result: str, fn_name: str) -> str:
"""Render tool result in an expanded card with direct HTML rendering."""
import uuid
tool_label = {
"browser.search": "๐Ÿ” Search Results",
"browser.open": "๐Ÿ“„ Page Content",
"browser.find": "๐Ÿ”Ž Find Results"
}.get(fn_name, "๐Ÿ“‹ Result")
border_colors = {
"browser.search": "#667eea",
"browser.open": "#4facfe",
"browser.find": "#86efac"
}
border_color = border_colors.get(fn_name, "#9ca3af")
# ===== SEARCH RESULTS =====
if fn_name == "browser.search" and "<html>" in result and "<ul>" in result:
ul_match = re.search(r'<ul>(.*?)</ul>', result, re.DOTALL)
if ul_match:
ul_content = ul_match.group(1)
items = re.findall(r"<li><a href='([^']+)'>([^<]+)</a>\s*([^<]*)</li>", ul_content)
if items:
lines = result.split('\n')
search_title = ""
if lines and re.match(r'^\[\d+\]\s+Search Results:', lines[0]):
match = re.match(r'^\[(\d+)\]\s+(.+?)\s+\(web-search://', lines[0])
if match:
ref_num, title = match.groups()
title = re.sub(r'\s+\(web-search://.*$', '', lines[0])
title = re.sub(r'^\[\d+\]\s+', '', title)
search_title = f'''
<div style="background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%); padding: 0.875rem 1rem; margin: -1.25rem -1.25rem 1rem -1.25rem; border-bottom: 1px solid #e0e7ff;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="background: #667eea; color: white; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;">ใ€{ref_num}ใ€‘</span>
<span style="color: #1e40af; font-weight: 600; font-size: 0.95rem;">{html.escape(title)}</span>
</div>
</div>
'''
result_html = '<div style="display: flex; flex-direction: column; gap: 0.75rem;">'
for idx, (url, title, summary) in enumerate(items, 1):
card_id = f"search-card-{uuid.uuid4().hex[:8]}"
result_html += f'''
<div class="search-result-card" style="background: white; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; transition: all 0.2s ease;">
<div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.875rem;">
<a href="{html.escape(url)}" target="_blank"
style="color: #667eea; font-weight: 600; font-size: 0.75rem; min-width: 30px; text-decoration: none;">ใ€{idx}ใ€‘</a>
<a href="{html.escape(url)}" target="_blank"
style="color: #1f2937; font-weight: 600; font-size: 0.9rem; text-decoration: none; flex: 1;">
{html.escape(title)}
</a>
</div>
<div style="padding: 0 0.875rem 0.875rem 0.875rem; border-top: 1px solid #f3f4f6;">
<div style="color: #6b7280; font-size: 0.85rem; line-height: 1.5; margin-top: 0.75rem;">
{html.escape(summary)}
</div>
<div style="color: #9ca3af; font-size: 0.75rem; margin-top: 0.5rem; font-family: monospace; word-break: break-all;">
{html.escape(url)}
</div>
</div>
</div>
'''
result_html += '</div>'
return f'''<div class="result-card-expanded" style="border-left: 3px solid {border_color};">
<div class="result-header-expanded">{tool_label}</div>
<div class="result-content-expanded" style="font-family: inherit;">{search_title}{result_html}</div>
</div>'''
# ===== BROWSER.OPEN and BROWSER.FIND =====
lines = result.split('\n')
title_html = ""
content_start_idx = 0
pattern_to_highlight = None
if lines and re.match(r'^\[\d+\]\s+.+\s+\(.+\)$', lines[0]):
first_line = lines[0]
match = re.match(r'^\[(\d+)\]\s+(.+?)\s+\((.+)\)$', first_line)
if match:
ref_num, title, url = match.groups()
if fn_name == "browser.find":
pattern_match = re.search(r'Find Results:\s*(.+)', title)
if pattern_match:
pattern_to_highlight = pattern_match.group(1).strip()
is_clickable = not url.startswith('web-search://')
if is_clickable:
title_html = f'''
<div style="background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); padding: 0.875rem 1rem; margin: -1.25rem -1.25rem 1rem -1.25rem; border-bottom: 1px solid #e0e7ff;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="background: {border_color}; color: white; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;">ใ€{ref_num}ใ€‘</span>
<a href="{html.escape(url)}" target="_blank"
style="color: #1e40af; font-weight: 600; font-size: 0.95rem; text-decoration: none; flex: 1;">
{html.escape(title)}
</a>
</div>
<div style="color: #64748b; font-size: 0.75rem; margin-top: 0.25rem; font-family: monospace;">
{html.escape(url)}
</div>
</div>
'''
else:
title_html = f'''
<div style="background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%); padding: 0.875rem 1rem; margin: -1.25rem -1.25rem 1rem -1.25rem; border-bottom: 1px solid #e0e7ff;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span style="background: {border_color}; color: white; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;">ใ€{ref_num}ใ€‘</span>
<span style="color: #1e40af; font-weight: 600; font-size: 0.95rem;">{html.escape(title)}</span>
</div>
</div>
'''
content_start_idx = 1
if content_start_idx < len(lines) and lines[content_start_idx].startswith('**viewing lines'):
content_start_idx += 1
if content_start_idx < len(lines) and lines[content_start_idx].strip() == '':
content_start_idx += 1
cleaned_lines = []
for line in lines[content_start_idx:]:
cleaned_line = re.sub(r'^L\d+:\s*', '', line)
cleaned_lines.append(cleaned_line)
cleaned_content = '\n'.join(cleaned_lines)
formatted_result = html.escape(cleaned_content)
if pattern_to_highlight and fn_name == "browser.find":
escaped_pattern = re.escape(pattern_to_highlight)
def highlight_match(match):
return f'<mark style="background: #86efac; padding: 0.125rem 0.25rem; border-radius: 2px; font-weight: 600; color: #064e3b;">{match.group(0)}</mark>'
formatted_result = re.sub(
escaped_pattern,
highlight_match,
formatted_result,
flags=re.IGNORECASE
)
def make_citation_clickable(match):
full_text = match.group(0)
parts_match = re.match(r'ใ€(\d+)โ€ ([^โ€ ]+)โ€ ([^ใ€‘]+)ใ€‘', full_text)
if parts_match:
ref_num = parts_match.group(1)
title = parts_match.group(2)
domain = parts_match.group(3)
url = f"https://{domain}" if not domain.startswith('http') else domain
return f'<a href="{html.escape(url)}" target="_blank" style="background: #e0f7fa; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; color: #006064; font-weight: 500; text-decoration: none; display: inline-block;" title="{html.escape(title)}">ใ€{ref_num}โ€ {html.escape(domain)}ใ€‘</a>'
else:
simple_match = re.match(r'ใ€(\d+)โ€ ([^ใ€‘]+)ใ€‘', full_text)
if simple_match:
ref_num = simple_match.group(1)
text = simple_match.group(2)
return f'<span style="background: #e0f7fa; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; color: #006064; font-weight: 500;">ใ€{ref_num}โ€ {html.escape(text)}ใ€‘</span>'
return full_text
formatted_result = re.sub(r'ใ€\d+โ€ [^ใ€‘]+ใ€‘', make_citation_clickable, formatted_result)
formatted_result = formatted_result.replace('\n\n', '</p><p style="margin: 0.75rem 0;">')
formatted_result = formatted_result.replace('\n', '<br>')
if not formatted_result.startswith('<p'):
formatted_result = f'<p style="margin: 0.75rem 0;">{formatted_result}</p>'
max_length = 5000
if len(result) > max_length:
formatted_result = formatted_result[:max_length] + '<br><br><em style="color: #9ca3af;">...(content truncated for display)...</em>'
return f'''<div class="result-card-expanded" style="border-left: 3_solid {border_color};">
<div class="result-header-expanded">{tool_label}</div>
<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>
</div>'''
def render_round_badge(round_num: int, max_rounds: int) -> str:
return f'<div class="round-badge">Round {round_num}/{max_rounds}</div>'
def render_answer(text: str, browser: SimpleBrowser) -> str:
rendered = render_citations(text, browser)
return f'<div class="answer-section">{rendered}</div>'
def render_completion() -> str:
return '<div class="completion-msg">Research Complete</div>'
def render_user_message(question: str) -> str:
escaped = html.escape(question)
return f'''<div class="user-message-bubble">
<div class="user-message-content">{escaped}</div>
</div>'''
# ============================================================
# Remote API Generation (via OpenAI-compatible endpoint)
# ============================================================
def count_tokens(text: str) -> int:
"""Count tokens in text using the loaded tokenizer."""
try:
tok = load_tokenizer()
return len(tok.encode(text))
except Exception:
# Fallback to rough estimate if tokenizer fails
return len(text) // 4
async def generate_response(prompt: str, max_new_tokens: int = MAX_NEW_TOKENS) -> str:
"""Generate response using OpenAI-compatible API with model switching."""
# Choose model based on prompt length
prompt_tokens = count_tokens(prompt)
selected_model = "alias-large" if prompt_tokens > 4000 else "alias-fast"
url = f"{REMOTE_API_BASE}/completions"
headers = {
"Content-Type": "application/json"
}
if BLABLADOR_API_KEY:
headers["Authorization"] = f"Bearer {BLABLADOR_API_KEY}"
payload = {
"model": selected_model,
"prompt": prompt,
"max_tokens": max_new_tokens,
"temperature": 0.7,
"top_p": 0.9,
"stop": ["\n<tool_response>", "<tool_response>"],
}
async with httpx.AsyncClient() as client:
response = await client.post(url, json=payload, headers=headers, timeout=300.0)
if response.status_code != 200:
raise Exception(f"LLM API error {response.status_code}: {response.text}")
data = response.json()
return data["choices"][0]["text"]
# ============================================================
# Streaming Agent Runner
# ============================================================
async def run_agent_streaming(
question: str,
max_rounds: int
) -> Generator[str, None, None]:
global tokenizer
if not question.strip():
yield "<p style='color: var(--body-text-color-subdued); text-align: center; padding: 2rem;'>Please enter a question to begin.</p>"
return
if not BLABLADOR_API_KEY:
yield """<div class="error-message">
<p><strong>BLABLADOR_API_KEY Missing</strong></p>
<p>The Blablador API Key is not configured in the Space secrets. Please add it to your Space settings.</p>
</div>"""
return
# Load tokenizer for prompt formatting
try:
load_tokenizer()
except Exception as e:
yield f"<p style='color:#dc2626;'>Error loading tokenizer: {html.escape(str(e))}</p>"
return
browser = SimpleBrowser()
tools = json.loads(TOOL_CONTENT)
system_prompt = DEVELOPER_CONTENT + f"\n\nToday's date: {datetime.now().strftime('%Y-%m-%d')}"
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": question}
]
stop_strings = ["\n<tool_response>", "<tool_response>"]
html_parts = [render_user_message(question)]
yield ''.join(html_parts)
round_num = 0
try:
while round_num < max_rounds:
round_num += 1
html_parts.append(render_round_badge(round_num, max_rounds))
yield ''.join(html_parts)
prompt = tokenizer.apply_chat_template(
messages,
tools=tools,
tokenize=False,
add_generation_prompt=True
)
try:
print(f"\n{'='*60}")
print(f"Round {round_num}")
print(f"{'='*60}")
html_parts.append('<div class="thinking-streaming">Processing...</div>')
yield ''.join(html_parts)
# Call generation function
generated = await generate_response(prompt, max_new_tokens=MAX_NEW_TOKENS)
# Remove placeholder
html_parts.pop()
except Exception as e:
html_parts.pop() # Remove placeholder
html_parts.append(f"<p style='color:#dc2626;'>Generation Error: {html.escape(str(e))}</p>")
yield ''.join(html_parts)
return
for stop_str in stop_strings:
if stop_str in generated:
generated = generated[:generated.find(stop_str)]
reasoning, content = extract_thinking(generated)
tool_call, clean_content = parse_tool_call(content)
if reasoning:
html_parts.append(render_thinking_collapsed(reasoning))
yield ''.join(html_parts)
if tool_call:
fn_name = tool_call.get("name", "unknown")
args = tool_call.get("arguments", {})
html_parts.append(render_tool_call(fn_name, args, browser))
yield ''.join(html_parts)
if clean_content.strip() and not tool_call:
rendered = render_citations(clean_content, browser)
html_parts.append(f'<div class="answer-section">{rendered}</div>')
yield ''.join(html_parts)
non_thinking = generated.split('</think>', 1)[1].strip() if '</think>' in generated else generated.strip()
messages.append({
"role": "assistant",
"content": non_thinking if tool_call is None else "",
"reasoning_content": reasoning,
"tool_calls": [{
"id": str(round_num),
"type": "function",
"function": {
"name": tool_call.get("name", ""),
"arguments": tool_call.get("arguments", {})
}
}] if tool_call else None
})
if tool_call:
fn_name = tool_call.get("name", "")
args = tool_call.get("arguments", {})
if fn_name.startswith("browser."):
actual_fn = fn_name.split(".", 1)[1]
else:
actual_fn = fn_name
result = ""
try:
if actual_fn == "search":
result = await browser.search(args.get("query", ""), args.get("topn", 4))
elif actual_fn == "open":
result = await browser.open(**args)
elif actual_fn == "find":
result = browser.find(args.get("pattern", ""), args.get("cursor", -1))
else:
result = f"Unknown tool: {fn_name}"
except Exception as e:
result = f"Tool error: {str(e)}\n{traceback.format_exc()}"
html_parts.append(render_tool_result(result, fn_name))
yield ''.join(html_parts)
messages.append({
"role": "tool",
"tool_call_id": str(round_num),
"content": result
})
continue
if is_final_answer(generated):
html_parts.append(render_completion())
yield ''.join(html_parts)
break
if round_num >= max_rounds:
html_parts.append('<div class="completion-msg" style="background:#fef3c7;border-color:#f59e0b;color:#92400e;">Maximum rounds reached</div>')
yield ''.join(html_parts)
# Generate Reference Section
if browser.used_citations:
html_parts.append('<details class="reference-section">')
html_parts.append('<summary class="reference-title">References</summary>')
for i, cursor in enumerate(browser.used_citations):
info = browser.get_page_info(cursor)
if info:
url = info.get('url', '#')
title = info.get('title', 'Unknown Source')
else:
url = "#"
title = "Unknown Source"
ref_item = f'''
<div class="reference-item">
<div style="display: flex; align-items: baseline;">
<span class="ref-number">[{i}]</span>
<a href="{html.escape(url)}" target="_blank" class="ref-text">{html.escape(title)}</a>
</div>
<div class="ref-url" style="text-align: left;">{html.escape(url)}</div>
</div>
'''
html_parts.append(ref_item)
html_parts.append('</details>')
yield ''.join(html_parts)
except Exception as e:
tb = traceback.format_exc()
html_parts.append(f'<div style="color:#dc2626;"><p>Error: {html.escape(str(e))}</p><pre>{html.escape(tb)}</pre></div>')
yield ''.join(html_parts)
# ============================================================
# Gradio Interface
# ============================================================
CAROUSEL_JS = r"""
(function() {
let currentExample = 0;
const totalExamples = 3;
let carouselInitialized = false;
let layoutInitialized = false;
function updateCarousel() {
const items = document.querySelectorAll('.carousel-item');
const dots = document.querySelectorAll('.carousel-dot');
items.forEach((item, index) => {
if (index === currentExample) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
dots.forEach((dot, index) => {
if (index === currentExample) {
dot.classList.add('active');
} else {
dot.classList.remove('active');
}
});
}
function setExample(text) {
const container = document.querySelector('#question-input');
if (container) {
const textbox = container.querySelector('textarea');
if (textbox) {
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
nativeInputValueSetter.call(textbox, text);
textbox.dispatchEvent(new Event('input', { bubbles: true }));
textbox.focus();
}
}
}
function initCarousel() {
if (carouselInitialized) return;
const prevBtn = document.getElementById('prev-btn');
const nextBtn = document.getElementById('next-btn');
const items = document.querySelectorAll('.carousel-item');
const dots = document.querySelectorAll('.carousel-dot');
if (!prevBtn || !nextBtn || items.length === 0) return;
carouselInitialized = true;
prevBtn.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
currentExample = (currentExample - 1 + totalExamples) % totalExamples;
updateCarousel();
};
nextBtn.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
currentExample = (currentExample + 1) % totalExamples;
updateCarousel();
};
dots.forEach((dot, index) => {
dot.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
currentExample = index;
updateCarousel();
};
});
items.forEach((item, index) => {
item.onclick = function(e) {
e.preventDefault();
e.stopPropagation();
const text = this.getAttribute('data-text');
if (text) {
setExample(text);
}
};
});
}
function isAutoScrollEnabled() {
const checkbox = document.querySelector('#auto-scroll-checkbox input[type="checkbox"]');
return checkbox ? checkbox.checked : true;
}
function scrollToBottom() {
if (!isAutoScrollEnabled()) return;
const outputArea = document.querySelector('#output-area');
if (outputArea) {
// ็›ดๆŽฅๆปšๅŠจ #output-area
outputArea.scrollTop = outputArea.scrollHeight;
}
}
// ็›‘ๅฌ่พ“ๅ‡บๅŒบๅŸŸ็š„ๅ†…ๅฎนๅ˜ๅŒ–๏ผŒ่‡ชๅŠจๆปšๅŠจ
function setupAutoScroll() {
const outputArea = document.querySelector('#output-area');
if (outputArea) {
const observer = new MutationObserver(function() {
// ๅปถ่ฟŸๆปšๅŠจไปฅ็กฎไฟ DOM ๅทฒๆ›ดๆ–ฐ
requestAnimationFrame(function() {
setTimeout(scrollToBottom, 50);
});
});
observer.observe(outputArea, { childList: true, subtree: true, characterData: true });
}
}
function updateOutputVisibility() {
const outputArea = document.getElementById('output-area');
if (outputArea) {
const content = outputArea.innerHTML.trim();
// ๆฃ€ๆŸฅๆ˜ฏๅฆๆœ‰ๅฎž้™…ๅ†…ๅฎน๏ผˆไธๅชๆ˜ฏ็ฉบ็š„ div ๆˆ–็ฉบ็™ฝ๏ผ‰
const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
if (hasContent) {
outputArea.classList.remove('hidden-output');
outputArea.classList.add('has-content');
outputArea.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important; height: 50vh !important; min-height: 250px !important; max-height: 50vh !important; overflow-y: scroll !important; padding: 1rem !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; background: #fafafa !important;';
} else {
outputArea.classList.add('hidden-output');
outputArea.classList.remove('has-content');
outputArea.style.cssText = 'display: none !important; visibility: hidden !important; opacity: 0 !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important;';
}
}
}
function initLayout() {
if (layoutInitialized) return;
const mainContent = document.getElementById('main-content');
const outputArea = document.getElementById('output-area');
if (!mainContent) return;
layoutInitialized = true;
mainContent.classList.add('initial-state');
// ๅˆๅง‹ๅŒ–ๆ—ถ็ซ‹ๅณ้š่—็ฉบ็š„่พ“ๅ‡บๅŒบๅŸŸ - ไฝฟ็”จๅ†…่”ๆ ทๅผ็กฎไฟ็”Ÿๆ•ˆ
if (outputArea) {
const content = outputArea.innerHTML.trim();
const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
if (!hasContent) {
outputArea.style.cssText = 'display: none !important; visibility: hidden !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; opacity: 0 !important;';
outputArea.classList.add('hidden-output');
outputArea.classList.remove('has-content');
}
}
// ่ฎพ็ฝฎ่‡ชๅŠจๆปšๅŠจ็›‘ๅฌ
setupAutoScroll();
const outputObserver = new MutationObserver(function() {
const content = outputArea ? outputArea.innerHTML.trim() : '';
const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
if (hasContent) {
mainContent.classList.remove('initial-state');
outputArea.classList.remove('hidden-output');
outputArea.classList.add('has-content');
outputArea.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important; height: 50vh !important; min-height: 250px !important; max-height: 50vh !important; overflow-y: scroll !important; padding: 1rem !important; border: 1px solid #e5e7eb !important; border-radius: 8px !important; background: #fafafa !important;';
setTimeout(scrollToBottom, 100);
} else {
mainContent.classList.add('initial-state');
if (outputArea) {
outputArea.classList.add('hidden-output');
outputArea.classList.remove('has-content');
outputArea.style.cssText = 'display: none !important; visibility: hidden !important; opacity: 0 !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important;';
}
}
});
if (outputArea) {
outputObserver.observe(outputArea, { childList: true, subtree: true, characterData: true });
}
const questionInput = document.querySelector('#question-input textarea');
if (questionInput) {
questionInput.focus();
}
}
const observer = new MutationObserver(function(mutations, obs) {
initCarousel();
initLayout();
if (carouselInitialized && layoutInitialized) {
obs.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// ็ซ‹ๅณๅฐ่ฏ•้š่—่พ“ๅ‡บๅŒบๅŸŸ๏ผˆๅœจ DOM ๅฎŒๅ…จๅŠ ่ฝฝไน‹ๅ‰๏ผ‰
function hideOutputAreaEarly() {
const outputArea = document.getElementById('output-area');
if (outputArea) {
const content = outputArea.innerHTML.trim();
const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
if (!hasContent) {
outputArea.style.cssText = 'display: none !important; visibility: hidden !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important; padding: 0 !important; margin: 0 !important; border: none !important; opacity: 0 !important;';
outputArea.classList.add('hidden-output');
outputArea.classList.remove('has-content');
}
}
}
// ๅคšๆฌกๅฐ่ฏ•้š่—๏ผŒ็กฎไฟๅœจๅ„็งๆ—ถๆœบ้ƒฝ่ƒฝ็”Ÿๆ•ˆ
hideOutputAreaEarly();
document.addEventListener('DOMContentLoaded', hideOutputAreaEarly);
setTimeout(hideOutputAreaEarly, 0);
setTimeout(hideOutputAreaEarly, 100);
setTimeout(hideOutputAreaEarly, 300);
setTimeout(hideOutputAreaEarly, 500);
setTimeout(function() { initCarousel(); initLayout(); }, 1000);
setTimeout(function() { initCarousel(); initLayout(); }, 2000);
// ๆทฑ่‰ฒๆจกๅผ้€‚้… - ๅŠจๆ€ๆ›ดๆ–ฐ output-area ่ƒŒๆ™ฏ่‰ฒ๏ผˆ้˜ฒ้—ช็ƒไผ˜ๅŒ–็‰ˆ๏ผ‰
let lastUpdateTime = 0;
const UPDATE_THROTTLE = 50; // ๆœ€ๅฐๆ›ดๆ–ฐ้—ด้š” 50ms
function updateOutputAreaDarkMode() {
const now = Date.now();
if (now - lastUpdateTime < UPDATE_THROTTLE) {
return; // ่ทณ่ฟ‡่ฟ‡ไบŽ้ข‘็น็š„ๆ›ดๆ–ฐ
}
lastUpdateTime = now;
const outputArea = document.getElementById('output-area');
if (!outputArea) return;
// ๆฃ€ๆŸฅๆ˜ฏๅฆๆ˜ฏๆทฑ่‰ฒๆจกๅผ
const isDark = document.documentElement.classList.contains('dark') ||
document.body.classList.contains('dark') ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
if (isDark) {
// ๆทฑ่‰ฒๆจกๅผ๏ผšๆทฑ่‰ฒ่ƒŒๆ™ฏ
outputArea.style.setProperty('background', '#111827', 'important');
outputArea.style.setProperty('border-color', '#374151', 'important');
} else {
// ๆต…่‰ฒๆจกๅผ๏ผšๆต…่‰ฒ่ƒŒๆ™ฏ
outputArea.style.setProperty('background', '#fafafa', 'important');
outputArea.style.setProperty('border-color', '#e5e7eb', 'important');
}
}
// ๅปถ่ฟŸๅˆๅง‹ๅŒ–๏ผŒ้ฟๅ…้กต้ขๅŠ ่ฝฝๆ—ถ้—ช็ƒ
setTimeout(function() {
updateOutputAreaDarkMode();
// ็›‘ๅฌๆทฑ่‰ฒๆจกๅผๅ˜ๅŒ–
const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
darkModeMediaQuery.addEventListener('change', updateOutputAreaDarkMode);
// ็›‘ๅฌ DOM class ๅ˜ๅŒ–
const darkModeObserver = new MutationObserver(updateOutputAreaDarkMode);
darkModeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
darkModeObserver.observe(document.body, {
attributes: true,
attributeFilter: ['class']
});
// ็›‘ๅฌ output-area ็š„ๅ†…ๅฎนๅ˜ๅŒ–
const outputArea = document.getElementById('output-area');
if (outputArea) {
const outputContentObserver = new MutationObserver(function() {
// ๅ†…ๅฎนๅ˜ๅŒ–ๆ—ถ็ซ‹ๅณๅบ”็”จๆทฑ่‰ฒๆจกๅผๆ ทๅผ
requestAnimationFrame(updateOutputAreaDarkMode);
});
outputContentObserver.observe(outputArea, {
childList: true,
subtree: true
});
}
}, 500); // ๅปถ่ฟŸ 500ms ๅŽๅ†ๅฏๅŠจ็›‘ๅฌ
})();
"""
def create_interface():
# Get the directory where this script is located for static files
script_dir = os.path.dirname(os.path.abspath(__file__))
# Helper function to convert image to base64 for embedding in HTML
def image_to_base64(image_path):
"""Convert image file to base64 string for HTML embedding."""
try:
with open(image_path, 'rb') as img_file:
img_data = img_file.read()
b64_string = base64.b64encode(img_data).decode('utf-8')
# Determine MIME type based on file extension
ext = image_path.lower().split('.')[-1]
mime_types = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'svg': 'image/svg+xml',
'gif': 'image/gif'
}
mime_type = mime_types.get(ext, 'image/png')
return f"data:{mime_type};base64,{b64_string}"
except Exception as e:
print(f"Error loading image {image_path}: {e}")
return ""
# Inline CSS - all styles embedded directly
INLINE_CSS = """
/* Global Styles */
.gradio-container {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
}
/* Thinking Styles */
.thinking-collapsed {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 0.75rem;
margin: 0.5rem 0;
}
.thinking-collapsed summary {
cursor: pointer;
font-weight: 500;
color: #6b7280;
font-size: 0.875rem;
}
.thinking-collapsed summary:hover {
color: #374151;
}
.thinking-content {
margin-top: 0.5rem;
color: #374151;
white-space: pre-wrap;
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 0.875rem;
line-height: 1.5;
}
.thinking-streaming {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 0.875rem;
margin: 0.5rem 0;
color: #0c4a6e;
white-space: pre-wrap;
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.875rem;
line-height: 1.5;
}
/* Tool Call Card */
.tool-call-card {
background: #f9fafb;
border-radius: 8px;
padding: 1rem;
margin: 0.75rem 0;
}
.tool-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.tool-name {
font-weight: 600;
color: #374151;
font-size: 0.875rem;
}
.tool-detail {
color: #6b7280;
font-size: 0.8rem;
}
/* Result Card */
.result-card-expanded {
background: white;
border-radius: 8px;
padding: 1.25rem;
margin: 1rem 0;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.result-header-expanded {
font-weight: 600;
color: #374151;
margin-bottom: 1.25rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid #e5e7eb;
font-size: 1rem;
}
.result-content-expanded {
color: #4b5563;
line-height: 1.6;
}
.result-content-expanded p {
margin: 0.5rem 0;
}
.result-content-expanded code {
background: #f3f4f6;
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-family: monospace;
font-size: 0.875em;
}
/* Search Result Card Hover */
.search-result-card {
transition: all 0.2s ease;
}
.search-result-card:hover {
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15) !important;
border-color: #667eea !important;
}
/* Answer Section */
.answer-section {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-left: 4px solid #10a37f;
border-radius: 8px;
padding: 1.5rem;
margin: 1rem 0;
}
.answer-section p {
color: #374151;
line-height: 1.7;
margin: 0.5rem 0;
}
.answer-section strong {
color: #1e293b;
font-weight: 600;
}
.answer-section a {
color: #10a37f;
text-decoration: none;
font-weight: 500;
}
.answer-section a:hover {
text-decoration: underline;
}
/* User Message Bubble - ๆทก่“่‰ฒ่ƒŒๆ™ฏ๏ผŒๅณๅฏน้ฝ */
.user-message-bubble {
background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
color: #0c4a6e;
border-radius: 1rem 1rem 0.25rem 1rem;
padding: 1rem 1.25rem;
margin: 1rem 0 1rem auto;
max-width: 80%;
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.2);
border: 1px solid #7dd3fc;
text-align: left;
}
/* Reference Section Collapsible */
.reference-section {
margin-top: 40px;
border-top: 1px solid #e5e7eb;
padding-top: 20px;
}
.reference-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 16px;
color: #111827;
cursor: pointer;
outline: none;
}
.ref-url {
text-align: left !important;
}
.user-message-content {
line-height: 1.6;
font-size: 0.95rem;
}
/* Output area - ๅ›บๅฎš้ซ˜ๅบฆๅฏๆปšๅŠจ */
/* ๅผบๅˆถๅ›บๅฎš้ซ˜ๅบฆ๏ผŒๅ†…ๅฎนๅœจ้‡Œ้ขๆปšๅŠจ */
#output-area,
#output-area.output-box,
div#output-area {
height: 50vh !important;
min-height: 250px !important;
max-height: 50vh !important;
overflow-y: scroll !important;
overflow-x: hidden !important;
padding: 1rem !important;
border: 1px solid #e5e7eb !important;
border-radius: 8px !important;
background: #fafafa !important;
scroll-behavior: smooth;
flex-shrink: 0 !important;
flex-grow: 0 !important;
}
/* ๅ†…้ƒจๆ‰€ๆœ‰ๅ…ƒ็ด ไธ่ƒฝๆ’‘็ ดๅฎนๅ™จ */
#output-area * {
max-height: none !important;
overflow: visible !important;
}
#output-area > div {
height: auto !important;
max-height: none !important;
overflow: visible !important;
border: none !important;
background: transparent !important;
padding: 0 !important;
margin: 0 !important;
box-shadow: none !important;
}
/* ๅˆๅง‹็Šถๆ€ๅ’Œ็ฉบๅ†…ๅฎนๆ—ถ้š่— output-area */
#output-area:empty,
#output-area.hidden-output,
#output-area:not(.has-content),
.hidden-output#output-area,
div.hidden-output#output-area,
#main-content #output-area.hidden-output,
#main-content .output-box.hidden-output {
display: none !important;
visibility: hidden !important;
height: 0 !important;
min-height: 0 !important;
max-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
opacity: 0 !important;
overflow: hidden !important;
}
/* ้˜ฒๆญขๅ†…้ƒจๅ†…ๅฎนๆ’‘็ ดๅฎนๅ™จ */
#output-area > * {
max-width: 100%;
word-wrap: break-word;
overflow-wrap: break-word;
}
/* ไธปๅ†…ๅฎนๅŒบๅŸŸๅธƒๅฑ€ - ้™ๅˆถๆ•ดไฝ“้ซ˜ๅบฆ */
#main-content {
display: flex !important;
flex-direction: column !important;
height: auto !important;
max-height: none !important;
overflow: visible !important;
}
/* Gradio ๅŒ…่ฃ…ๅฎนๅ™จ้™ๅˆถ */
#main-content > div {
flex-shrink: 0;
}
#output-area::-webkit-scrollbar {
width: 8px;
}
#output-area::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
#output-area::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
#output-area::-webkit-scrollbar-thumb:hover {
background: #a1a1a1;
}
/* ่‡ชๅŠจๆปšๅŠจๆŽงๅˆถๆŒ‰้’ฎๆ ทๅผ */
#auto-scroll-checkbox {
margin-top: 0.5rem;
}
#auto-scroll-checkbox label {
font-size: 0.85rem;
color: #4b5563;
}
/* Completion Message */
.completion-msg {
text-align: center;
color: #10a37f;
font-weight: 600;
padding: 1rem;
margin: 1rem 0;
background: #f0fdf4;
border-radius: 8px;
border: 1px solid #86efac;
}
/* Error Message */
.error-message {
background: #fee2e2;
border-left: 4px solid #dc2626;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
color: #991b1b;
}
.error-message strong {
color: #7f1d1d;
}
.error-message a {
color: #dc2626;
font-weight: 500;
}
/* Round Badge - ๆทก่“่‰ฒ่ƒŒๆ™ฏ */
.round-badge {
display: inline-block;
background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
color: #0369a1;
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
margin: 0.5rem 0;
box-shadow: 0 2px 4px rgba(14, 165, 233, 0.15);
border: 1px solid #7dd3fc;
}
/* Settings Section */
#settings-group {
background: transparent !important;
border: none !important;
padding: 0 !important;
box-shadow: none !important;
gap: 0 !important;
}
#max-rounds-slider, #auto-scroll-checkbox {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.5rem;
}
.settings-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.875rem;
}
.settings-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
}
.settings-api-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.375rem;
}
.settings-label {
font-size: 0.8rem;
font-weight: 500;
color: #4b5563;
}
.settings-help-link {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.7rem;
color: #667eea;
text-decoration: none;
transition: opacity 0.2s;
}
.settings-help-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 0.6rem;
font-weight: bold;
}
/* Tools Section */
.tools-section {
margin-top: 0;
}
.tools-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
margin-top: 0;
}
/* Tool Item */
.tool-item {
background: #f9fafb;
padding: 0.75rem;
border-radius: 6px;
margin-bottom: 0.5rem;
border: 1px solid #e5e7eb;
}
.tool-item strong {
color: #374151;
font-size: 0.85rem;
}
.tool-item span {
color: #6b7280;
font-size: 0.8rem;
}
/* Examples Section */
.examples-section {
margin-top: -0.5rem;
}
.examples-title {
font-size: 0.875rem;
font-weight: 600;
color: #374151;
margin-bottom: 0.5rem;
}
/* Example Carousel */
.example-carousel {
background: white;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e5e7eb;
}
.carousel-container {
position: relative;
min-height: 60px;
margin-bottom: 0.75rem;
}
.carousel-item {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.carousel-item.active {
display: block;
opacity: 1;
}
.carousel-item-text {
background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
padding: 1rem;
border-radius: 6px;
color: #374151;
font-size: 0.875rem;
line-height: 1.5;
border: 1px solid #e0e7ff;
}
.carousel-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.carousel-btn {
cursor: pointer;
width: 32px;
height: 32px;
border-radius: 50%;
background: #f3f4f6;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
color: #6b7280;
transition: all 0.2s ease;
user-select: none;
}
.carousel-btn:hover {
background: #667eea;
color: white;
}
.carousel-indicators {
display: flex;
gap: 0.5rem;
}
.carousel-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #d1d5db;
cursor: pointer;
transition: all 0.2s ease;
}
.carousel-dot.active {
background: #667eea;
width: 24px;
border-radius: 4px;
}
/* Welcome Message */
.welcome-container {
text-align: center;
padding: 3rem 2rem;
}
.welcome-title {
font-size: 2rem;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 1rem;
}
.welcome-subtitle {
color: #6b7280;
font-size: 1.1rem;
margin-bottom: 2rem;
}
/* Footer */
.footer-container {
text-align: center;
padding: 1.5rem;
color: #9ca3af;
font-size: 0.875rem;
border-top: 1px solid #e5e7eb;
margin-top: 2rem;
}
.footer-container a {
color: #667eea;
text-decoration: none;
}
.footer-container a:hover {
text-decoration: underline;
}
/* Disclaimer */
.disclaimer {
text-align: center;
padding: 1rem;
color: #6b7280;
font-size: 0.875rem;
border-top: 1px solid #e5e7eb;
margin-top: 1rem;
}
/* ========== ๆทฑ่‰ฒๆจกๅผ้€‚้… ========== */
@media (prefers-color-scheme: dark) {
/* Settings ๅŒบๅŸŸ */
#settings-group {
background: transparent !important;
border: none !important;
padding: 0 !important;
gap: 0 !important;
}
#max-rounds-slider, #auto-scroll-checkbox {
background: #1f2937 !important;
border: 1px solid #374151 !important;
border-radius: 6px !important;
padding: 0.75rem !important;
}
.settings-title,
.tools-title,
.examples-title {
color: #e5e7eb !important;
}
.settings-label {
color: #9ca3af !important;
}
.settings-help-link {
color: #818cf8 !important;
}
/* Available Tools ๅŒบๅŸŸ */
.tool-item {
background: #1f2937 !important;
border-color: #374151 !important;
}
.tool-item strong {
color: #e5e7eb !important;
}
.tool-item span {
color: #9ca3af !important;
}
/* Example Carousel ๅŒบๅŸŸ */
.example-carousel {
background: #1f2937 !important;
border-color: #374151 !important;
}
.carousel-item-text {
background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
border-color: #3b82f6 !important;
color: #e0f2fe !important;
}
.carousel-btn {
background: #374151 !important;
color: #9ca3af !important;
}
.carousel-btn:hover {
background: #667eea !important;
color: white !important;
}
.carousel-dot {
background: #4b5563 !important;
}
.carousel-dot.active {
background: #667eea !important;
}
/* Output area ๆทฑ่‰ฒๆจกๅผ */
#output-area,
#output-area.output-box,
div#output-area {
background: #111827 !important;
border: 1px solid #374151 !important;
}
#output-area::-webkit-scrollbar-track {
background: #1f2937 !important;
}
#output-area::-webkit-scrollbar-thumb {
background: #4b5563 !important;
}
#output-area::-webkit-scrollbar-thumb:hover {
background: #6b7280 !important;
}
/* Tool call card ๆทฑ่‰ฒๆจกๅผ */
.tool-call-card {
background: #1f2937 !important;
}
.tool-name {
color: #e5e7eb !important;
}
.tool-detail {
color: #9ca3af !important;
}
/* Result card ๆทฑ่‰ฒๆจกๅผ */
.result-card-expanded {
background: #1f2937 !important;
}
.result-header-expanded {
color: #e5e7eb !important;
border-bottom-color: #374151 !important;
}
.result-content-expanded {
color: #d1d5db !important;
}
/* Thinking ๆทฑ่‰ฒๆจกๅผ */
.thinking-collapsed {
background: #1f2937 !important;
border-color: #374151 !important;
}
.thinking-collapsed summary {
color: #9ca3af !important;
}
.thinking-content {
color: #d1d5db !important;
}
.thinking-streaming {
background: #0c4a6e !important;
border-color: #0369a1 !important;
color: #bae6fd !important;
}
/* Answer section ๆทฑ่‰ฒๆจกๅผ */
.answer-section {
background: linear-gradient(135deg, #0c4a6e 0%, #164e63 100%) !important;
}
.answer-section p {
color: #e0f2fe !important;
}
.answer-section strong {
color: #f0f9ff !important;
}
/* ็”จๆˆท้—ฎ้ข˜ๆฐ”ๆณกๆทฑ่‰ฒๆจกๅผ */
.user-message-bubble {
background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
color: #e0f2fe !important;
border-color: #3b82f6 !important;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
}
.user-message-content {
color: #e0f2fe !important;
}
/* Round Badge ๆทฑ่‰ฒๆจกๅผ */
.round-badge {
background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
color: #93c5fd !important;
border-color: #3b82f6 !important;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2) !important;
}
/* ๆœ็ดข็ป“ๆžœๅก็‰‡ๆทฑ่‰ฒๆจกๅผ */
.search-result-card {
background: #1f2937 !important;
border-color: #374151 !important;
}
.search-result-card:hover {
border-color: #667eea !important;
}
/* ๅทฅๅ…ท็ป“ๆžœๆ ‡้ข˜ๅŒบๅŸŸๆทฑ่‰ฒๆจกๅผ */
.result-card-expanded div[style*="background: linear-gradient"] {
background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
border-color: #3b82f6 !important;
}
.result-card-expanded div[style*="background: linear-gradient"] span[style*="color: #1e40af"],
.result-card-expanded div[style*="background: linear-gradient"] a[style*="color: #1e40af"] {
color: #93c5fd !important;
}
.result-card-expanded div[style*="color: #64748b"] {
color: #9ca3af !important;
}
/* ๅฎŒๆˆๆถˆๆฏๆทฑ่‰ฒๆจกๅผ */
.completion-msg {
background: #064e3b !important;
border-color: #059669 !important;
color: #6ee7b7 !important;
}
/* ้”™่ฏฏๆถˆๆฏๆทฑ่‰ฒๆจกๅผ */
.error-message {
background: #450a0a !important;
border-color: #b91c1c !important;
color: #fca5a5 !important;
}
/* ไพง่พนๆ ๆ ‡้ข˜ๆทฑ่‰ฒๆจกๅผ */
div[style*="font-weight: 600"][style*="color: #374151"] {
color: #e5e7eb !important;
}
/* Disclaimer ๆทฑ่‰ฒๆจกๅผ */
.disclaimer {
color: #9ca3af !important;
border-color: #374151 !important;
}
}
/* Gradio ๆทฑ่‰ฒไธป้ข˜็ฑปๅ้€‚้… */
.dark #settings-group,
.dark .tool-item,
.dark .example-carousel {
background: #1f2937 !important;
border-color: #374151 !important;
}
.dark .settings-title,
.dark .tools-title,
.dark .examples-title,
.dark .tool-item strong,
.dark .result-header-expanded,
.dark .tool-name {
color: #e5e7eb !important;
}
.dark .settings-label,
.dark .tool-item span,
.dark .tool-detail,
.dark .thinking-collapsed summary {
color: #9ca3af !important;
}
.dark .settings-help-link {
color: #818cf8 !important;
}
.dark .carousel-item-text {
background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
border-color: #3b82f6 !important;
color: #e0f2fe !important;
}
.dark #output-area,
.dark #output-area.output-box,
.dark div#output-area {
background: #111827 !important;
border: 1px solid #374151 !important;
}
.dark .tool-call-card,
.dark .result-card-expanded,
.dark .thinking-collapsed {
background: #1f2937 !important;
border-color: #374151 !important;
}
.dark .result-content-expanded,
.dark .thinking-content {
color: #d1d5db !important;
}
.dark .answer-section {
background: linear-gradient(135deg, #0c4a6e 0%, #164e63 100%) !important;
}
.dark .answer-section p {
color: #e0f2fe !important;
}
.dark .search-result-card {
background: #1f2937 !important;
border-color: #374151 !important;
}
.dark .completion-msg {
background: #064e3b !important;
border-color: #059669 !important;
color: #6ee7b7 !important;
}
/* ็”จๆˆท้—ฎ้ข˜ๆฐ”ๆณก Gradio dark ๆจกๅผ */
.dark .user-message-bubble {
background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
color: #e0f2fe !important;
border-color: #3b82f6 !important;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
}
.dark .user-message-content {
color: #e0f2fe !important;
}
/* Round Badge Gradio dark ๆจกๅผ */
.dark .round-badge {
background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
color: #93c5fd !important;
border-color: #3b82f6 !important;
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2) !important;
}
/* ๅทฅๅ…ท็ป“ๆžœๆ ‡้ข˜ Gradio dark ๆจกๅผ */
.dark .result-card-expanded div[style*="background: linear-gradient"] {
background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
border-color: #3b82f6 !important;
}
/* Disclaimer Gradio dark ๆจกๅผ */
.dark .disclaimer {
color: #9ca3af !important;
border-color: #374151 !important;
}
/* Reference Section */
.reference-section {
margin-top: 40px;
border-top: 1px solid #e5e7eb;
padding-top: 20px;
}
.reference-title {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 16px;
color: #111827;
}
.reference-item {
display: block;
background-color: #fff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 12px 16px;
margin-bottom: 12px;
text-decoration: none;
color: #374151;
transition: all 0.2s;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.reference-item:hover {
border-color: #3b82f6;
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
transform: translateY(-1px);
text-decoration: none;
}
.ref-number {
display: inline-block;
background-color: #eff6ff;
color: #2563eb;
font-weight: 600;
padding: 2px 6px;
border-radius: 6px;
margin-right: 8px;
font-size: 0.85em;
}
.ref-text {
font-weight: 500;
color: #1f2937;
}
.ref-url {
display: block;
margin-top: 4px;
font-size: 0.8em;
color: #6b7280;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ๆทฑ่‰ฒๆจกๅผ้€‚้… Reference Section */
@media (prefers-color-scheme: dark) {
.reference-title {
color: #e5e7eb !important;
}
.reference-item {
background-color: #1f2937 !important;
border-color: #374151 !important;
color: #d1d5db !important;
}
.reference-item:hover {
border-color: #667eea !important;
}
.ref-number {
background-color: #1e3a5f !important;
color: #93c5fd !important;
}
.ref-text {
color: #e5e7eb !important;
}
.ref-url {
color: #9ca3af !important;
}
}
/* Gradio dark ๆจกๅผ้€‚้… Reference Section */
.dark .reference-title {
color: #e5e7eb !important;
}
.dark .reference-item {
background-color: #1f2937 !important;
border-color: #374151 !important;
color: #d1d5db !important;
}
.dark .reference-item:hover {
border-color: #667eea !important;
}
.dark .ref-number {
background-color: #1e3a5f !important;
color: #93c5fd !important;
}
.dark .ref-text {
color: #e5e7eb !important;
}
.dark .ref-url {
color: #9ca3af !important;
}
"""
with gr.Blocks(css=INLINE_CSS, theme=gr.themes.Soft(), js=CAROUSEL_JS) as demo:
# Header with logo and title images
logo_path = os.path.join(script_dir, "or-logo1.png")
title_path = os.path.join(script_dir, "openresearcher-title.svg")
logo_base64 = image_to_base64(logo_path)
title_base64 = image_to_base64(title_path)
header_html = f"""
<div style="
text-align: center;
padding: 0.5rem 1rem 0.5rem 1rem;
background: transparent;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 1.5rem;
">
"""
if logo_base64:
header_html += f'<img src="{logo_base64}" alt="OpenResearcher Logo" style="height: 84px;">'
if title_base64:
header_html += f'<img src="{title_base64}" alt="OpenResearcher" style="height: 84px;">'
header_html += "</div>"
gr.HTML(header_html)
gr.HTML("""
<div style="display: flex; gap: 0px; justify-content: center; flex-wrap: wrap; margin-top: 0px; margin-bottom: 24px;">
<a href="https://boiled-honeycup-4c7.notion.site/OpenResearcher-A-Fully-Open-Pipeline-for-Long-Horizon-Deep-Research-Trajectory-Synthesis-2f7e290627b5800cb3a0cd7e8d6ec0ea?source=copy_link" target="_blank">
<img src="https://img.shields.io/badge/Blog-4285F4?style=for-the-badge&logo=google-chrome&logoColor=white" alt="Blog" style="height: 28px;">
</a>
<a href="https://github.com/TIGER-AI-Lab/OpenResearcher" target="_blank">
<img src="https://img.shields.io/badge/Github-181717?style=for-the-badge&logo=github&logoColor=white" alt="Github" style="height: 28px;">
</a>
<a href="https://huggingface.co/datasets/OpenResearcher/OpenResearcher-Dataset" target="_blank">
<img src="https://img.shields.io/badge/Dataset-FFB7B2?style=for-the-badge&logo=huggingface&logoColor=ffffff" alt="Dataset" style="height: 28px;">
</a>
<a href="https://huggingface.co/OpenResearcher/Nemotron-3-Nano-30B-A3B" target="_blank">
<img src="https://img.shields.io/badge/Model-FFD966?style=for-the-badge&logo=huggingface&logoColor=ffffff" alt="Model" style="height: 28px;">
</a>
<a href="https://huggingface.co/datasets/OpenResearcher/OpenResearcher-Eval-Logs/tree/main" target="_blank">
<img src="https://img.shields.io/badge/Eval%20Logs-755BB4?style=for-the-badge&logo=google-sheets&logoColor=white" alt="Eval Logs" style="height: 28px;">
</a>
</div>
""")
# Main layout: Left sidebar + Right content
with gr.Row():
# Left Sidebar (settings & tools)
with gr.Column(scale=1, min_width=280):
# API Settings in a unified box
with gr.Group(elem_id="settings-group"):
gr.HTML('''
<div class="settings-header">
<span class="settings-title">โš™๏ธ Settings</span>
</div>
''')
max_rounds_input = gr.Slider(
minimum=1,
maximum=200,
value=50,
step=1,
label="Max Rounds",
elem_id="max-rounds-slider"
)
auto_scroll_checkbox = gr.Checkbox(
label="Auto Scroll",
value=True,
elem_id="auto-scroll-checkbox",
interactive=True
)
# Tools info
gr.HTML("""
<div class="tools-section">
<div class="tools-title">๐Ÿ› ๏ธ Available Tools</div>
<div class="tool-item"><strong>browser.search</strong><br><span>Search the web</span></div>
<div class="tool-item"><strong>browser.open</strong><br><span>Open & read pages</span></div>
<div class="tool-item"><strong>browser.find</strong><br><span>Find text in page</span></div>
</div>
""")
# Example carousel with navigation
gr.HTML("""
<div class="examples-section">
<div class="examples-title">๐Ÿ’ก Try Examples</div>
<div class="example-carousel" id="example-carousel">
<div class="carousel-container">
<div class="carousel-item active" data-index="0" data-text="Who won the Nobel Prize in Physics 2024?">
<div class="carousel-item-text">๐Ÿ† Who won the Nobel Prize in Physics 2024?</div>
</div>
<div class="carousel-item" data-index="1" data-text="What are the latest breakthroughs in quantum computing in 2024?">
<div class="carousel-item-text">๐Ÿ”ฌ What are the latest breakthroughs in quantum computing in 2024?</div>
</div>
<div class="carousel-item" data-index="2" data-text="What are the new features in Python 3.12?">
<div class="carousel-item-text">๐Ÿ What are the new features in Python 3.12?</div>
</div>
</div>
<div class="carousel-controls">
<div class="carousel-btn" id="prev-btn">โ€น</div>
<div class="carousel-indicators">
<div class="carousel-dot active" data-index="0"></div>
<div class="carousel-dot" data-index="1"></div>
<div class="carousel-dot" data-index="2"></div>
</div>
<div class="carousel-btn" id="next-btn">โ€บ</div>
</div>
</div>
</div>
""")
# Main content area (30-70)
with gr.Column(scale=3, elem_id="main-content"):
# Output area (on top, hidden initially)
output_area = gr.HTML(
value="",
elem_classes=["output-box"],
elem_id="output-area",
visible=True
)
# Welcome message (will be hidden after first search)
welcome_html = gr.HTML(
value="""
<div id="welcome-section" class="welcome-section">
<h2>What Would You Like to Research?</h2>
<p>I am OpenResearcher, a leading open-source Deep Research Agent, welcome to try!</p>
<p style="color: red;">Due to high traffic, if your submission has no response, please refresh the page and resubmit. Thank you!</p>
</div>
""",
elem_id="welcome-container"
)
# Input area at bottom
question_input = gr.Textbox(
label="",
placeholder="Ask me anything and I'll handle the rest...",
lines=2,
show_label=False,
elem_id="question-input",
autofocus=True
)
with gr.Row(elem_id="button-row"):
submit_btn = gr.Button(
"๐Ÿ” Start DeepResearch",
variant="primary",
elem_classes=["primary-btn"],
scale=3
)
stop_btn = gr.Button("โน Stop", variant="stop", scale=1)
clear_btn = gr.Button("๐Ÿ—‘ Clear", scale=1)
# Function to hide welcome and show output
async def start_research(question, max_rounds):
# Generator that first hides welcome, then streams results
# Also clears the input box for the next question
# Initial yield to immediately clear welcome, show loading in output, and clear input
# IMPORTANT: Don't use empty string for output, or JS will hide the output area!
yield "", '<div style="text-align: center; padding: 2rem; color: #6b7280;">Delving into it...</div>', ""
async for result in run_agent_streaming(question, max_rounds):
yield "", result, ""
# Event handlers
submit_event = submit_btn.click(
fn=start_research,
inputs=[question_input, max_rounds_input],
outputs=[welcome_html, output_area, question_input],
show_progress="hidden",
concurrency_limit=20
)
question_input.submit(
fn=start_research,
inputs=[question_input, max_rounds_input],
outputs=[welcome_html, output_area, question_input],
show_progress="hidden",
concurrency_limit=20
)
stop_btn.click(fn=None, inputs=None, outputs=None, cancels=[submit_event])
clear_btn.click(
fn=lambda: ("""
<div id="welcome-section" class="welcome-section">
<h2>What would you like to research?</h2>
<p>Ask any question and I'll search the web to find answers</p>
</div>
""", "", ""),
outputs=[welcome_html, output_area, question_input]
)
# Disclaimer
gr.HTML('''
<div class="disclaimer">
โš ๏ธ AI may generate incorrect information or citations. Please double-check important facts.
</div>
''')
return demo
if __name__ == "__main__":
print("="*60)
print("OpenResearcher DeepSearch Agent - Helmholtz Blablador Provider")
print("="*60)
demo = create_interface()
demo.queue(default_concurrency_limit=20).launch()