IPF commited on
Commit
baf61c0
·
verified ·
1 Parent(s): af2ca27

Upload 4 files

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. app.py +2342 -0
  3. openresearcher-title.svg +12 -0
  4. or-logo1.png +3 -0
  5. requirements.txt +11 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ or-logo1.png filter=lfs diff=lfs merge=lfs -text
app.py ADDED
@@ -0,0 +1,2342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ OpenResearcher DeepSearch Agent - Hugging Face Space
4
+ Uses ZeroGPU for efficient inference with the Nemotron model
5
+ Aligned with app_local.py frontend and logic
6
+ """
7
+ import os
8
+ import gradio as gr
9
+ import httpx
10
+ import json
11
+ import json5
12
+ import re
13
+ import time
14
+ import html
15
+ import asyncio
16
+ from datetime import datetime
17
+ from typing import List, Dict, Any, Optional, Tuple, Generator
18
+ import traceback
19
+ import base64
20
+ from transformers import AutoTokenizer
21
+
22
+
23
+ # ============================================================
24
+ # Configuration
25
+ # ============================================================
26
+ MODEL_NAME = os.getenv("MODEL_NAME", "OpenResearcher/Nemotron-3-Nano-30B-A3B")
27
+ REMOTE_API_BASE = "https://scrappier-sallie-untarred.ngrok-free.dev/v1"
28
+ SERPER_API_KEY = os.getenv("SERPER_API_KEY", "")
29
+ MAX_NEW_TOKENS = int(os.getenv("MAX_NEW_TOKENS", "4096")) # Safe limit for ZeroGPU
30
+
31
+ # ============================================================
32
+ # System Prompt & Tools
33
+ # ============================================================
34
+ DEVELOPER_CONTENT = """
35
+ You are a helpful assistant and harmless assistant.
36
+
37
+ You will be able to use a set of browsering tools to answer user queries.
38
+
39
+ Tool for browsing.
40
+ The `cursor` appears in brackets before each browsing display: `[{cursor}]`.
41
+ Cite information from the tool using the following format:
42
+ `【{cursor}†L{line_start}(-L{line_end})?】`, for example: `【6†L9-L11】` or `【8†L3】`.
43
+ Do not quote more than 10 words directly from the tool output.
44
+ sources=web
45
+ """.strip()
46
+
47
+ TOOL_CONTENT = """
48
+ [
49
+ {
50
+ "type": "function",
51
+ "function": {
52
+ "name": "browser.search",
53
+ "description": "Searches for information related to a query and displays top N results. Returns a list of search results with titles, URLs, and summaries.",
54
+ "parameters": {
55
+ "type": "object",
56
+ "properties": {
57
+ "query": {
58
+ "type": "string",
59
+ "description": "The search query string"
60
+ },
61
+ "topn": {
62
+ "type": "integer",
63
+ "description": "Number of results to display",
64
+ "default": 10
65
+ }
66
+ },
67
+ "required": [
68
+ "query"
69
+ ]
70
+ }
71
+ }
72
+ },
73
+ {
74
+ "type": "function",
75
+ "function": {
76
+ "name": "browser.open",
77
+ "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}†.*】.",
78
+ "parameters": {
79
+ "type": "object",
80
+ "properties": {
81
+ "id": {
82
+ "type": [
83
+ "integer",
84
+ "string"
85
+ ],
86
+ "description": "Link id from current page (integer) or fully qualified URL (string). Default is -1 (most recent page)",
87
+ "default": -1
88
+ },
89
+ "cursor": {
90
+ "type": "integer",
91
+ "description": "Page cursor to operate on. If not provided, the most recent page is implied",
92
+ "default": -1
93
+ },
94
+ "loc": {
95
+ "type": "integer",
96
+ "description": "Starting line number. If not provided, viewport will be positioned at the beginning or centered on relevant passage",
97
+ "default": -1
98
+ },
99
+ "num_lines": {
100
+ "type": "integer",
101
+ "description": "Number of lines to display",
102
+ "default": -1
103
+ },
104
+ "view_source": {
105
+ "type": "boolean",
106
+ "description": "Whether to view page source",
107
+ "default": false
108
+ },
109
+ "source": {
110
+ "type": "string",
111
+ "description": "The source identifier (e.g., 'web')"
112
+ }
113
+ },
114
+ "required": []
115
+ }
116
+ }
117
+ },
118
+ {
119
+ "type": "function",
120
+ "function": {
121
+ "name": "browser.find",
122
+ "description": "Finds exact matches of a pattern in the current page or a specified page by cursor.",
123
+ "parameters": {
124
+ "type": "object",
125
+ "properties": {
126
+ "pattern": {
127
+ "type": "string",
128
+ "description": "The exact text pattern to search for"
129
+ },
130
+ "cursor": {
131
+ "type": "integer",
132
+ "description": "Page cursor to search in. If not provided, searches in the current page",
133
+ "default": -1
134
+ }
135
+ },
136
+ "required": [
137
+ "pattern"
138
+ ]
139
+ }
140
+ }
141
+ }
142
+ ]
143
+ """.strip()
144
+
145
+ # ============================================================
146
+ # Browser Tool Implementation
147
+ # ============================================================
148
+ class SimpleBrowser:
149
+ """Browser tool using Serper API."""
150
+
151
+ def __init__(self, serper_key: str):
152
+ self.serper_key = serper_key
153
+ self.pages: Dict[str, Dict] = {}
154
+ self.page_stack: List[str] = []
155
+ self.link_map: Dict[int, str] = {}
156
+
157
+ @property
158
+ def current_cursor(self) -> int:
159
+ return len(self.page_stack) - 1
160
+
161
+ def _format_line_numbers(self, text: str, offset: int = 0) -> str:
162
+ lines = text.split('\n')
163
+ return '\n'.join(f"L{i + offset}: {line}" for i, line in enumerate(lines))
164
+
165
+ def _clean_links(self, results: List[Dict], query: str) -> Tuple[str, Dict[int, str]]:
166
+ link_map = {}
167
+ lines = []
168
+
169
+ for i, r in enumerate(results):
170
+ title = html.escape(r.get('title', 'No Title'))
171
+ url = r.get('link', r.get('url', ''))
172
+ snippet = html.escape(r.get('snippet', r.get('summary', '')))
173
+
174
+ try:
175
+ domain = url.split('/')[2] if url else ''
176
+ except:
177
+ domain = ''
178
+
179
+ link_map[i] = url
180
+ link_text = f"【{i}†{title}†{domain}】" if domain else f"【{i}†{title}】"
181
+ lines.append(f"{link_text}")
182
+ lines.append(f" {snippet}")
183
+ lines.append("")
184
+
185
+ return '\n'.join(lines), link_map
186
+
187
+ async def search(self, query: str, topn: int = 10) -> str:
188
+ url = "https://google.serper.dev/search"
189
+ headers = {'X-API-KEY': self.serper_key, 'Content-Type': 'application/json'}
190
+ payload = json.dumps({"q": query, "num": topn})
191
+
192
+ async with httpx.AsyncClient() as client:
193
+ try:
194
+ response = await client.post(url, headers=headers, data=payload, timeout=20.0)
195
+ if response.status_code != 200:
196
+ return f"Error: Search failed with status {response.status_code}"
197
+
198
+ data = response.json()
199
+ results = data.get("organic", [])
200
+ if not results:
201
+ return f"No results found for: '{query}'"
202
+
203
+ content, self.link_map = self._clean_links(results, query)
204
+ pseudo_url = f"web-search://q={query}&ts={int(time.time())}"
205
+ cursor = self.current_cursor + 1
206
+
207
+ page_data = {
208
+ 'url': pseudo_url,
209
+ 'title': f"Search Results: {query}",
210
+ 'text': content,
211
+ 'urls': {str(k): v for k, v in self.link_map.items()}
212
+ }
213
+ self.pages[pseudo_url] = page_data
214
+ self.page_stack.append(pseudo_url)
215
+
216
+ header = f"{page_data['title']} ({pseudo_url})\n**viewing lines [0 - {len(content.split(chr(10)))-1}]**\n\n"
217
+ body = self._format_line_numbers(content)
218
+
219
+ return f"[{cursor}] {header}{body}"
220
+
221
+ except Exception as e:
222
+ return f"Error during search: {str(e)}"
223
+
224
+ async def open(self, id: int | str = -1, cursor: int = -1, loc: int = -1, num_lines: int = -1, **kwargs) -> str:
225
+ target_url = None
226
+
227
+ if isinstance(id, str) and id.startswith("http"):
228
+ target_url = id
229
+ elif isinstance(id, int) and id >= 0:
230
+ target_url = self.link_map.get(id)
231
+ if not target_url:
232
+ return f"Error: Invalid link id '{id}'. Available: {list(self.link_map.keys())}"
233
+ elif cursor >= 0 and cursor < len(self.page_stack):
234
+ page_url = self.page_stack[cursor]
235
+ page = self.pages.get(page_url)
236
+ if page:
237
+ text = page['text']
238
+ lines = text.split('\n')
239
+ start = max(0, loc) if loc >= 0 else 0
240
+ end = min(len(lines), start + num_lines) if num_lines > 0 else len(lines)
241
+
242
+ header = f"{page['title']} ({page['url']})\n**viewing lines [{start} - {end-1}] of {len(lines)-1}**\n\n"
243
+ body = self._format_line_numbers('\n'.join(lines[start:end]), offset=start)
244
+ return f"[{cursor}] {header}{body}"
245
+ else:
246
+ return "Error: No valid target specified"
247
+
248
+ if not target_url:
249
+ return "Error: Could not determine target URL"
250
+
251
+ headers = {'X-API-KEY': self.serper_key, 'Content-Type': 'application/json'}
252
+ payload = json.dumps({"url": target_url})
253
+
254
+ async with httpx.AsyncClient() as client:
255
+ try:
256
+ response = await client.post("https://scrape.serper.dev/", headers=headers, data=payload, timeout=30.0)
257
+ if response.status_code != 200:
258
+ return f"Error fetching URL: {response.status_code}"
259
+
260
+ data = response.json()
261
+ text = data.get("text", "")
262
+ title = data.get("metadata", {}).get("title", "") if isinstance(data.get("metadata"), dict) else ""
263
+
264
+ if not text:
265
+ return f"No content found at URL"
266
+
267
+ lines = text.split('\n')
268
+ content = '\n'.join(lines)
269
+
270
+ max_lines = 150
271
+ if len(lines) > max_lines:
272
+ content = '\n'.join(lines[:max_lines]) + "\n\n...(content truncated)..."
273
+
274
+ new_cursor = self.current_cursor + 1
275
+ page_data = {
276
+ 'url': target_url,
277
+ 'title': title or target_url,
278
+ 'text': content,
279
+ 'urls': {}
280
+ }
281
+ self.pages[target_url] = page_data
282
+ self.page_stack.append(target_url)
283
+
284
+ start = max(0, loc) if loc >= 0 else 0
285
+ display_lines = content.split('\n')
286
+ end = min(len(display_lines), start + num_lines) if num_lines > 0 else len(display_lines)
287
+
288
+ header = f"{title or target_url} ({target_url})\n**viewing lines [{start} - {end-1}] of {len(display_lines)-1}**\n\n"
289
+ body = self._format_line_numbers('\n'.join(display_lines[start:end]), offset=start)
290
+
291
+ return f"[{new_cursor}] {header}{body}"
292
+
293
+ except Exception as e:
294
+ return f"Error fetching URL: {str(e)}"
295
+
296
+ def find(self, pattern: str, cursor: int = -1) -> str:
297
+ if not self.page_stack:
298
+ return "Error: No page open"
299
+
300
+ page_url = self.page_stack[cursor] if cursor >= 0 and cursor < len(self.page_stack) else self.page_stack[-1]
301
+ page = self.pages.get(page_url)
302
+
303
+ if not page:
304
+ return "Error: Page not found"
305
+
306
+ text = page['text']
307
+ lines = text.split('\n')
308
+ matches = []
309
+
310
+ for i, line in enumerate(lines):
311
+ if str(pattern).lower() in line.lower():
312
+ start = max(0, i - 1)
313
+ end = min(len(lines), i + 3)
314
+ context = '\n'.join(f"L{j}: {lines[j]}" for j in range(start, end))
315
+ matches.append(f"# 【{len(matches)}†match at L{i}】\n{context}")
316
+ if len(matches) >= 10:
317
+ break
318
+
319
+ if not matches:
320
+ return f"No matches found for: '{pattern}'"
321
+
322
+ result_url = f"{page_url}/find?pattern={pattern}"
323
+ new_cursor = self.current_cursor + 1
324
+ result_content = '\n\n'.join(matches)
325
+
326
+ page_data = {
327
+ 'url': result_url,
328
+ 'title': f"Find results for: '{pattern}'",
329
+ 'text': result_content,
330
+ 'urls': {}
331
+ }
332
+ self.pages[result_url] = page_data
333
+ self.page_stack.append(result_url)
334
+
335
+ header = f"Find results for text: `{pattern}` in `{page['title']}`\n\n"
336
+ return f"[{new_cursor}] {header}{result_content}"
337
+
338
+ def get_cursor_url(self, cursor: int) -> Optional[str]:
339
+ if cursor >= 0 and cursor < len(self.page_stack):
340
+ return self.page_stack[cursor]
341
+ return None
342
+
343
+
344
+ # ============================================================
345
+ # Tokenizer Loading
346
+ # ============================================================
347
+ tokenizer = None
348
+
349
+ def load_tokenizer():
350
+ global tokenizer
351
+ if tokenizer is None:
352
+ print(f"Loading tokenizer: {MODEL_NAME}")
353
+ try:
354
+ tokenizer = AutoTokenizer.from_pretrained(
355
+ MODEL_NAME,
356
+ trust_remote_code=True
357
+ )
358
+ print("Tokenizer loaded successfully!")
359
+ except Exception as e:
360
+ print(f"Error loading tokenizer: {e}")
361
+ import traceback
362
+ traceback.print_exc()
363
+ raise
364
+ return tokenizer
365
+
366
+ # ============================================================
367
+ # Text Processing
368
+ # ============================================================
369
+ def extract_thinking(text: str) -> Tuple[Optional[str], str]:
370
+ reasoning_content = None
371
+ content = text
372
+
373
+ if '<think>' in content and '</think>' in content:
374
+ match = re.search(r'<think>(.*?)</think>', content, re.DOTALL)
375
+ if match:
376
+ reasoning_content = match.group(1).strip()
377
+ content = content.replace(match.group(0), "").strip()
378
+ elif '</think>' in content:
379
+ match = re.search(r'^(.*?)</think>', content, re.DOTALL)
380
+ if match:
381
+ reasoning_content = match.group(1).strip()
382
+ content = content.replace(match.group(0), "").strip()
383
+
384
+ return reasoning_content, content
385
+
386
+
387
+ def parse_tool_call(text: str) -> Tuple[Optional[Dict], str]:
388
+ tool_call_text = None
389
+ content = text
390
+
391
+ if '<tool_call>' in content and '</tool_call>' in content:
392
+ match = re.search(r'<tool_call>(.*?)</tool_call>', content, re.DOTALL)
393
+ if match:
394
+ tool_call_text = match.group(1).strip()
395
+ content = content.replace(match.group(0), "").strip()
396
+ elif '</tool_call>' in content:
397
+ match = re.search(r'^(.*?)</tool_call>', content, re.DOTALL)
398
+ if match:
399
+ tool_call_text = match.group(1).strip()
400
+ content = content.replace(match.group(0), "").strip()
401
+
402
+ if tool_call_text:
403
+ try:
404
+ if "```json" in tool_call_text:
405
+ tool_call_text = tool_call_text.split("```json")[1].split("```")[0].strip()
406
+ elif "```" in tool_call_text:
407
+ tool_call_text = tool_call_text.split("```")[1].split("```")[0].strip()
408
+
409
+ parsed = json5.loads(tool_call_text)
410
+ return parsed, content
411
+ except:
412
+ pass
413
+
414
+ func_match = re.search(r'<function=([\w.]+)>', tool_call_text)
415
+ if func_match:
416
+ tool_name = func_match.group(1)
417
+ tool_args = {}
418
+ params = re.finditer(r'<parameter=([\w]+)>\s*(.*?)\s*</parameter>', tool_call_text, re.DOTALL)
419
+ for p in params:
420
+ param_name = p.group(1)
421
+ param_value = p.group(2).strip()
422
+ if param_value.startswith('"') and param_value.endswith('"'):
423
+ param_value = param_value[1:-1]
424
+ try:
425
+ if param_value.isdigit():
426
+ param_value = int(param_value)
427
+ except:
428
+ pass
429
+ tool_args[param_name] = param_value
430
+
431
+ return {"name": tool_name, "arguments": tool_args}, content
432
+
433
+ return None, content
434
+
435
+
436
+ def is_final_answer(text: str) -> bool:
437
+ t = text.lower()
438
+ return (
439
+ ('<answer>' in t and '</answer>' in t) or
440
+ 'final answer:' in t or
441
+ ('exact answer:' in t and 'confidence:' in t)
442
+ )
443
+
444
+ # ============================================================
445
+ # HTML Rendering Helpers (From app_local.py)
446
+ # ============================================================
447
+ def render_citations(text: str, browser: SimpleBrowser) -> str:
448
+ """Convert citation markers to clickable HTML links."""
449
+ def replace_citation(m):
450
+ cursor_str = m.group(1)
451
+ l1 = m.group(2)
452
+ l2 = m.group(3)
453
+
454
+ try:
455
+ cursor = int(cursor_str)
456
+ url = browser.get_cursor_url(cursor)
457
+ if url:
458
+ ref = f"L{l1}-L{l2}" if l2 else f"L{l1}"
459
+ # Return HTML link
460
+ escaped_url = html.escape(url)
461
+ return f'<a href="{escaped_url}" target="_blank" style="color:#10a37f;text-decoration:none;font-size:0.85em;">[{cursor_str}†{ref}]</a>'
462
+ except:
463
+ pass
464
+ return m.group(0)
465
+
466
+ result = re.sub(r'【(\d+)†L(\d+)(?:-L(\d+))?】', replace_citation, text)
467
+
468
+ # Convert basic markdown to HTML
469
+ result = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', result)
470
+ result = re.sub(r'\*(.+?)\*', r'<em>\1</em>', result)
471
+ result = re.sub(r'`(.+?)`', r'<code>\1</code>', result)
472
+ result = result.replace('\n\n', '</p><p>').replace('\n', '<br>')
473
+ if not result.startswith('<p>'):
474
+ result = f'<p>{result}</p>'
475
+
476
+ return result
477
+
478
+ def render_thinking_streaming(text: str) -> str:
479
+ """Render thinking content in streaming mode (visible, with animation)."""
480
+ escaped = html.escape(text)
481
+ return f'<div class="thinking-streaming">{escaped}</div>'
482
+
483
+ def render_thinking_collapsed(text: str) -> str:
484
+ """Render thinking content in collapsed mode after completion."""
485
+ escaped = html.escape(text)
486
+ preview = text[:100] + "..." if len(text) > 100 else text
487
+ preview_escaped = html.escape(preview)
488
+ return f'''<details class="thinking-collapsed">
489
+ <summary>Thought process: "{preview_escaped}"</summary>
490
+ <div class="thinking-content">{escaped}</div>
491
+ </details>'''
492
+
493
+ def render_tool_call(fn_name: str, args: dict, browser: SimpleBrowser = None) -> str:
494
+ """Render a tool call card with unified format and subtle distinction."""
495
+ border_colors = {
496
+ "browser.search": "#667eea",
497
+ "browser.open": "#4facfe",
498
+ "browser.find": "#fa709a"
499
+ }
500
+ border_color = border_colors.get(fn_name, "#9ca3af")
501
+
502
+ if fn_name == "browser.search":
503
+ query = str(args.get('query', ''))
504
+ return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
505
+ <div class="tool-info">
506
+ <div class="tool-name">Searching the web</div>
507
+ <div class="tool-detail">Query: "{html.escape(query)}"</div>
508
+ </div>
509
+ </div>'''
510
+ elif fn_name == "browser.open":
511
+ link_id = args.get('id', '')
512
+ url_info = ""
513
+ if browser and isinstance(link_id, int) and link_id >= 0:
514
+ url = browser.link_map.get(link_id, "")
515
+ if url:
516
+ try:
517
+ domain = url.split('/')[2]
518
+ url_info = f" → {domain}"
519
+ except:
520
+ url_info = ""
521
+ return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
522
+ <div class="tool-info">
523
+ <div class="tool-name">Opening page</div>
524
+ <div class="tool-detail">Link #{link_id}{url_info}</div>
525
+ </div>
526
+ </div>'''
527
+ elif fn_name == "browser.find":
528
+ pattern = str(args.get('pattern', ''))
529
+ return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
530
+ <div class="tool-info">
531
+ <div class="tool-name">Finding in page</div>
532
+ <div class="tool-detail">Pattern: "{html.escape(pattern)}"</div>
533
+ </div>
534
+ </div>'''
535
+ else:
536
+ return f'''<div class="tool-call-card" style="border-left: 3px solid {border_color};">
537
+ <div class="tool-info">
538
+ <div class="tool-name">{html.escape(str(fn_name))}</div>
539
+ <div class="tool-detail">{html.escape(json.dumps(args))}</div>
540
+ </div>
541
+ </div>'''
542
+
543
+ def render_tool_result(result: str, fn_name: str) -> str:
544
+ """Render tool result in an expanded card with direct HTML rendering."""
545
+ import uuid
546
+ tool_label = {
547
+ "browser.search": "🔍 Search Results",
548
+ "browser.open": "📄 Page Content",
549
+ "browser.find": "🔎 Find Results"
550
+ }.get(fn_name, "📋 Result")
551
+
552
+ border_colors = {
553
+ "browser.search": "#667eea",
554
+ "browser.open": "#4facfe",
555
+ "browser.find": "#86efac"
556
+ }
557
+ border_color = border_colors.get(fn_name, "#9ca3af")
558
+
559
+ # ===== SEARCH RESULTS =====
560
+ if fn_name == "browser.search" and "<html>" in result and "<ul>" in result:
561
+ ul_match = re.search(r'<ul>(.*?)</ul>', result, re.DOTALL)
562
+ if ul_match:
563
+ ul_content = ul_match.group(1)
564
+ items = re.findall(r"<li><a href='([^']+)'>([^<]+)</a>\s*([^<]*)</li>", ul_content)
565
+
566
+ if items:
567
+ lines = result.split('\n')
568
+ search_title = ""
569
+ if lines and re.match(r'^\[\d+\]\s+Search Results:', lines[0]):
570
+ match = re.match(r'^\[(\d+)\]\s+(.+?)\s+\(web-search://', lines[0])
571
+ if match:
572
+ ref_num, title = match.groups()
573
+ title = re.sub(r'\s+\(web-search://.*$', '', lines[0])
574
+ title = re.sub(r'^\[\d+\]\s+', '', title)
575
+ search_title = f'''
576
+ <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;">
577
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
578
+ <span style="background: #667eea; color: white; padding: 0.125rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600;">【{ref_num}】</span>
579
+ <span style="color: #1e40af; font-weight: 600; font-size: 0.95rem;">{html.escape(title)}</span>
580
+ </div>
581
+ </div>
582
+ '''
583
+
584
+ result_html = '<div style="display: flex; flex-direction: column; gap: 0.75rem;">'
585
+ for idx, (url, title, summary) in enumerate(items, 1):
586
+ card_id = f"search-card-{uuid.uuid4().hex[:8]}"
587
+ result_html += f'''
588
+ <div class="search-result-card" style="background: white; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; transition: all 0.2s ease;">
589
+ <div style="display: flex; align-items: center; gap: 0.5rem; padding: 0.875rem;">
590
+ <a href="{html.escape(url)}" target="_blank"
591
+ style="color: #667eea; font-weight: 600; font-size: 0.75rem; min-width: 30px; text-decoration: none;">【{idx}】</a>
592
+ <a href="{html.escape(url)}" target="_blank"
593
+ style="color: #1f2937; font-weight: 600; font-size: 0.9rem; text-decoration: none; flex: 1;">
594
+ {html.escape(title)}
595
+ </a>
596
+ </div>
597
+ <div style="padding: 0 0.875rem 0.875rem 0.875rem; border-top: 1px solid #f3f4f6;">
598
+ <div style="color: #6b7280; font-size: 0.85rem; line-height: 1.5; margin-top: 0.75rem;">
599
+ {html.escape(summary)}
600
+ </div>
601
+ <div style="color: #9ca3af; font-size: 0.75rem; margin-top: 0.5rem; font-family: monospace; word-break: break-all;">
602
+ {html.escape(url)}
603
+ </div>
604
+ </div>
605
+ </div>
606
+ '''
607
+ result_html += '</div>'
608
+ return f'''<div class="result-card-expanded" style="border-left: 3px solid {border_color};">
609
+ <div class="result-header-expanded">{tool_label}</div>
610
+ <div class="result-content-expanded" style="font-family: inherit;">{search_title}{result_html}</div>
611
+ </div>'''
612
+
613
+ # ===== BROWSER.OPEN and BROWSER.FIND =====
614
+ lines = result.split('\n')
615
+ title_html = ""
616
+ content_start_idx = 0
617
+ pattern_to_highlight = None
618
+
619
+ if lines and re.match(r'^\[\d+\]\s+.+\s+\(.+\)$', lines[0]):
620
+ first_line = lines[0]
621
+ match = re.match(r'^\[(\d+)\]\s+(.+?)\s+\((.+)\)$', first_line)
622
+ if match:
623
+ ref_num, title, url = match.groups()
624
+
625
+ if fn_name == "browser.find":
626
+ pattern_match = re.search(r'Find Results:\s*(.+)', title)
627
+ if pattern_match:
628
+ pattern_to_highlight = pattern_match.group(1).strip()
629
+
630
+ is_clickable = not url.startswith('web-search://')
631
+
632
+ if is_clickable:
633
+ title_html = f'''
634
+ <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;">
635
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
636
+ <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>
637
+ <a href="{html.escape(url)}" target="_blank"
638
+ style="color: #1e40af; font-weight: 600; font-size: 0.95rem; text-decoration: none; flex: 1;">
639
+ {html.escape(title)}
640
+ </a>
641
+ </div>
642
+ <div style="color: #64748b; font-size: 0.75rem; margin-top: 0.25rem; font-family: monospace;">
643
+ {html.escape(url)}
644
+ </div>
645
+ </div>
646
+ '''
647
+ else:
648
+ title_html = f'''
649
+ <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;">
650
+ <div style="display: flex; align-items: center; gap: 0.5rem;">
651
+ <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>
652
+ <span style="color: #1e40af; font-weight: 600; font-size: 0.95rem;">{html.escape(title)}</span>
653
+ </div>
654
+ </div>
655
+ '''
656
+
657
+ content_start_idx = 1
658
+ if content_start_idx < len(lines) and lines[content_start_idx].startswith('**viewing lines'):
659
+ content_start_idx += 1
660
+ if content_start_idx < len(lines) and lines[content_start_idx].strip() == '':
661
+ content_start_idx += 1
662
+
663
+ cleaned_lines = []
664
+ for line in lines[content_start_idx:]:
665
+ cleaned_line = re.sub(r'^L\d+:\s*', '', line)
666
+ cleaned_lines.append(cleaned_line)
667
+
668
+ cleaned_content = '\n'.join(cleaned_lines)
669
+ formatted_result = html.escape(cleaned_content)
670
+
671
+ if pattern_to_highlight and fn_name == "browser.find":
672
+ escaped_pattern = re.escape(pattern_to_highlight)
673
+ def highlight_match(match):
674
+ return f'<mark style="background: #86efac; padding: 0.125rem 0.25rem; border-radius: 2px; font-weight: 600; color: #064e3b;">{match.group(0)}</mark>'
675
+ formatted_result = re.sub(
676
+ escaped_pattern,
677
+ highlight_match,
678
+ formatted_result,
679
+ flags=re.IGNORECASE
680
+ )
681
+
682
+ def make_citation_clickable(match):
683
+ full_text = match.group(0)
684
+ parts_match = re.match(r'【(\d+)†([^†]+)†([^】]+)】', full_text)
685
+ if parts_match:
686
+ ref_num = parts_match.group(1)
687
+ title = parts_match.group(2)
688
+ domain = parts_match.group(3)
689
+ url = f"https://{domain}" if not domain.startswith('http') else domain
690
+ 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>'
691
+ else:
692
+ simple_match = re.match(r'【(\d+)†([^】]+)】', full_text)
693
+ if simple_match:
694
+ ref_num = simple_match.group(1)
695
+ text = simple_match.group(2)
696
+ 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>'
697
+ return full_text
698
+
699
+ formatted_result = re.sub(r'【\d+†[^】]+】', make_citation_clickable, formatted_result)
700
+ formatted_result = formatted_result.replace('\n\n', '</p><p style="margin: 0.75rem 0;">')
701
+ formatted_result = formatted_result.replace('\n', '<br>')
702
+
703
+ if not formatted_result.startswith('<p'):
704
+ formatted_result = f'<p style="margin: 0.75rem 0;">{formatted_result}</p>'
705
+
706
+ max_length = 5000
707
+ if len(result) > max_length:
708
+ formatted_result = formatted_result[:max_length] + '<br><br><em style="color: #9ca3af;">...(content truncated for display)...</em>'
709
+
710
+ return f'''<div class="result-card-expanded" style="border-left: 3px solid {border_color};">
711
+ <div class="result-header-expanded">{tool_label}</div>
712
+ <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>
713
+ </div>'''
714
+
715
+ def render_round_badge(round_num: int, max_rounds: int) -> str:
716
+ return f'<div class="round-badge">Round {round_num}/{max_rounds}</div>'
717
+
718
+ def render_answer(text: str, browser: SimpleBrowser) -> str:
719
+ rendered = render_citations(text, browser)
720
+ return f'<div class="answer-section">{rendered}</div>'
721
+
722
+ def render_completion() -> str:
723
+ return '<div class="completion-msg">Research Complete</div>'
724
+
725
+ def render_user_message(question: str) -> str:
726
+ escaped = html.escape(question)
727
+ return f'''<div class="user-message-bubble">
728
+ <div class="user-message-content">{escaped}</div>
729
+ </div>'''
730
+
731
+
732
+
733
+ # ============================================================
734
+ # Remote API Generation (via vLLM-compatible endpoint)
735
+ # ============================================================
736
+
737
+ def generate_response(prompt: str, max_new_tokens: int = MAX_NEW_TOKENS) -> str:
738
+ """Generate response using vLLM OpenAI-compatible API."""
739
+ # Use /completions endpoint for raw prompt
740
+ url = f"{REMOTE_API_BASE}/completions"
741
+ headers = {"Content-Type": "application/json"}
742
+ payload = {
743
+ "model": MODEL_NAME,
744
+ "prompt": prompt,
745
+ "max_tokens": max_new_tokens,
746
+ "temperature": 0.7,
747
+ "top_p": 0.9,
748
+ "stop": ["\n<tool_response>", "<tool_response>"],
749
+ }
750
+
751
+ response = httpx.post(url, json=payload, headers=headers, timeout=300.0)
752
+ if response.status_code != 200:
753
+ raise Exception(f"vLLM API error {response.status_code}: {response.text}")
754
+
755
+ data = response.json()
756
+ return data["choices"][0]["text"]
757
+
758
+
759
+ # ============================================================
760
+ # Streaming Agent Runner
761
+ # ============================================================
762
+ def run_agent_streaming(
763
+ question: str,
764
+ serper_key: str,
765
+ max_rounds: int
766
+ ) -> Generator[str, None, None]:
767
+ global tokenizer
768
+
769
+ if not question.strip():
770
+ yield "<p style='color: var(--body-text-color-subdued); text-align: center; padding: 2rem;'>Please enter a question to begin.</p>"
771
+ return
772
+
773
+ if not serper_key:
774
+ yield """<div class="error-message">
775
+ <p><strong>Serper API Key Required</strong></p>
776
+ <p>Please configure your Serper API Key in the left sidebar under <strong>Settings</strong>.</p>
777
+ <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>
778
+ </div>"""
779
+ return
780
+
781
+ # Load tokenizer for prompt formatting
782
+ try:
783
+ load_tokenizer()
784
+ except Exception as e:
785
+ yield f"<p style='color:#dc2626;'>Error loading tokenizer: {html.escape(str(e))}</p>"
786
+ return
787
+
788
+ browser = SimpleBrowser(serper_key)
789
+ tools = json.loads(TOOL_CONTENT)
790
+
791
+ system_prompt = DEVELOPER_CONTENT + f"\n\nToday's date: {datetime.now().strftime('%Y-%m-%d')}"
792
+ messages = [
793
+ {"role": "system", "content": system_prompt},
794
+ {"role": "user", "content": question}
795
+ ]
796
+
797
+ stop_strings = ["\n<tool_response>", "<tool_response>"]
798
+
799
+ html_parts = [render_user_message(question)]
800
+ yield ''.join(html_parts)
801
+
802
+ round_num = 0
803
+
804
+ try:
805
+ while round_num < max_rounds:
806
+ round_num += 1
807
+ html_parts.append(render_round_badge(round_num, max_rounds))
808
+ yield ''.join(html_parts)
809
+
810
+ prompt = tokenizer.apply_chat_template(
811
+ messages,
812
+ tools=tools,
813
+ tokenize=False,
814
+ add_generation_prompt=True
815
+ )
816
+
817
+ try:
818
+ print(f"\n{'='*60}")
819
+ print(f"Round {round_num}")
820
+ print(f"{'='*60}")
821
+
822
+ html_parts.append('<div class="thinking-streaming">Processing...</div>')
823
+ yield ''.join(html_parts)
824
+
825
+ # Call ZeroGPU function
826
+ generated = generate_response(prompt, max_new_tokens=MAX_NEW_TOKENS)
827
+
828
+ # Remove placeholder
829
+ html_parts.pop()
830
+
831
+ except Exception as e:
832
+ html_parts.pop() # Remove placeholder
833
+ html_parts.append(f"<p style='color:#dc2626;'>Generation Error: {html.escape(str(e))}</p>")
834
+ yield ''.join(html_parts)
835
+ return
836
+
837
+ for stop_str in stop_strings:
838
+ if stop_str in generated:
839
+ generated = generated[:generated.find(stop_str)]
840
+
841
+ reasoning, content = extract_thinking(generated)
842
+ tool_call, clean_content = parse_tool_call(content)
843
+
844
+ if reasoning:
845
+ html_parts.append(render_thinking_collapsed(reasoning))
846
+ yield ''.join(html_parts)
847
+
848
+ if tool_call:
849
+ fn_name = tool_call.get("name", "unknown")
850
+ args = tool_call.get("arguments", {})
851
+ html_parts.append(render_tool_call(fn_name, args, browser))
852
+ yield ''.join(html_parts)
853
+
854
+ if clean_content.strip() and not tool_call:
855
+ rendered = render_citations(clean_content, browser)
856
+ html_parts.append(f'<div class="answer-section">{rendered}</div>')
857
+ yield ''.join(html_parts)
858
+
859
+ non_thinking = generated.split('</think>', 1)[1].strip() if '</think>' in generated else generated.strip()
860
+ messages.append({
861
+ "role": "assistant",
862
+ "content": non_thinking if tool_call is None else "",
863
+ "reasoning_content": reasoning,
864
+ "tool_calls": [{
865
+ "id": str(round_num),
866
+ "type": "function",
867
+ "function": {
868
+ "name": tool_call.get("name", ""),
869
+ "arguments": tool_call.get("arguments", {})
870
+ }
871
+ }] if tool_call else None
872
+ })
873
+
874
+ if tool_call:
875
+ fn_name = tool_call.get("name", "")
876
+ args = tool_call.get("arguments", {})
877
+
878
+ if fn_name.startswith("browser."):
879
+ actual_fn = fn_name.split(".", 1)[1]
880
+ else:
881
+ actual_fn = fn_name
882
+
883
+ result = ""
884
+ try:
885
+ if actual_fn == "search":
886
+ result = asyncio.run(browser.search(args.get("query", ""), args.get("topn", 10)))
887
+ elif actual_fn == "open":
888
+ result = asyncio.run(browser.open(**args))
889
+ elif actual_fn == "find":
890
+ result = browser.find(args.get("pattern", ""), args.get("cursor", -1))
891
+ else:
892
+ result = f"Unknown tool: {fn_name}"
893
+ except Exception as e:
894
+ result = f"Tool error: {str(e)}\n{traceback.format_exc()}"
895
+
896
+ html_parts.append(render_tool_result(result, fn_name))
897
+ yield ''.join(html_parts)
898
+
899
+ messages.append({
900
+ "role": "tool",
901
+ "tool_call_id": str(round_num),
902
+ "content": result
903
+ })
904
+ continue
905
+
906
+ if is_final_answer(generated):
907
+ html_parts.append(render_completion())
908
+ yield ''.join(html_parts)
909
+ break
910
+
911
+ if round_num >= max_rounds:
912
+ html_parts.append('<div class="completion-msg" style="background:#fef3c7;border-color:#f59e0b;color:#92400e;">Maximum rounds reached</div>')
913
+ yield ''.join(html_parts)
914
+
915
+ except Exception as e:
916
+ tb = traceback.format_exc()
917
+ html_parts.append(f'<div style="color:#dc2626;"><p>Error: {html.escape(str(e))}</p><pre>{html.escape(tb)}</pre></div>')
918
+ yield ''.join(html_parts)
919
+
920
+
921
+ # ============================================================
922
+ # Gradio Interface
923
+ # ============================================================
924
+ CAROUSEL_JS = r"""
925
+ (function() {
926
+ let currentExample = 0;
927
+ const totalExamples = 3;
928
+ let carouselInitialized = false;
929
+ let layoutInitialized = false;
930
+
931
+ function updateCarousel() {
932
+ const items = document.querySelectorAll('.carousel-item');
933
+ const dots = document.querySelectorAll('.carousel-dot');
934
+
935
+ items.forEach((item, index) => {
936
+ if (index === currentExample) {
937
+ item.classList.add('active');
938
+ } else {
939
+ item.classList.remove('active');
940
+ }
941
+ });
942
+
943
+ dots.forEach((dot, index) => {
944
+ if (index === currentExample) {
945
+ dot.classList.add('active');
946
+ } else {
947
+ dot.classList.remove('active');
948
+ }
949
+ });
950
+ }
951
+
952
+ function setExample(text) {
953
+ const container = document.querySelector('#question-input');
954
+ if (container) {
955
+ const textbox = container.querySelector('textarea');
956
+ if (textbox) {
957
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
958
+ nativeInputValueSetter.call(textbox, text);
959
+ textbox.dispatchEvent(new Event('input', { bubbles: true }));
960
+ textbox.focus();
961
+ }
962
+ }
963
+ }
964
+
965
+ function initCarousel() {
966
+ if (carouselInitialized) return;
967
+
968
+ const prevBtn = document.getElementById('prev-btn');
969
+ const nextBtn = document.getElementById('next-btn');
970
+ const items = document.querySelectorAll('.carousel-item');
971
+ const dots = document.querySelectorAll('.carousel-dot');
972
+
973
+ if (!prevBtn || !nextBtn || items.length === 0) return;
974
+
975
+ carouselInitialized = true;
976
+
977
+ prevBtn.onclick = function(e) {
978
+ e.preventDefault();
979
+ e.stopPropagation();
980
+ currentExample = (currentExample - 1 + totalExamples) % totalExamples;
981
+ updateCarousel();
982
+ };
983
+
984
+ nextBtn.onclick = function(e) {
985
+ e.preventDefault();
986
+ e.stopPropagation();
987
+ currentExample = (currentExample + 1) % totalExamples;
988
+ updateCarousel();
989
+ };
990
+
991
+ dots.forEach((dot, index) => {
992
+ dot.onclick = function(e) {
993
+ e.preventDefault();
994
+ e.stopPropagation();
995
+ currentExample = index;
996
+ updateCarousel();
997
+ };
998
+ });
999
+
1000
+ items.forEach((item, index) => {
1001
+ item.onclick = function(e) {
1002
+ e.preventDefault();
1003
+ e.stopPropagation();
1004
+ const text = this.getAttribute('data-text');
1005
+ if (text) {
1006
+ setExample(text);
1007
+ }
1008
+ };
1009
+ });
1010
+ }
1011
+
1012
+ function isAutoScrollEnabled() {
1013
+ const checkbox = document.querySelector('#auto-scroll-checkbox input[type="checkbox"]');
1014
+ return checkbox ? checkbox.checked : true;
1015
+ }
1016
+
1017
+ function scrollToBottom() {
1018
+ if (!isAutoScrollEnabled()) return;
1019
+ const outputArea = document.querySelector('#output-area');
1020
+ if (outputArea) {
1021
+ // 直接滚动 #output-area
1022
+ outputArea.scrollTop = outputArea.scrollHeight;
1023
+ }
1024
+ }
1025
+
1026
+ // 监听输出区域的内容变化,自动滚动
1027
+ function setupAutoScroll() {
1028
+ const outputArea = document.querySelector('#output-area');
1029
+ if (outputArea) {
1030
+ const observer = new MutationObserver(function() {
1031
+ // 延迟滚动以确保 DOM 已更新
1032
+ requestAnimationFrame(function() {
1033
+ setTimeout(scrollToBottom, 50);
1034
+ });
1035
+ });
1036
+ observer.observe(outputArea, { childList: true, subtree: true, characterData: true });
1037
+ }
1038
+ }
1039
+
1040
+ function updateOutputVisibility() {
1041
+ const outputArea = document.getElementById('output-area');
1042
+ if (outputArea) {
1043
+ const content = outputArea.innerHTML.trim();
1044
+ // 检查是否有实际内容(不只是空的 div 或空白)
1045
+ const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
1046
+ if (hasContent) {
1047
+ outputArea.classList.remove('hidden-output');
1048
+ outputArea.classList.add('has-content');
1049
+ 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;';
1050
+ } else {
1051
+ outputArea.classList.add('hidden-output');
1052
+ outputArea.classList.remove('has-content');
1053
+ outputArea.style.cssText = 'display: none !important; visibility: hidden !important; opacity: 0 !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important;';
1054
+ }
1055
+ }
1056
+ }
1057
+
1058
+ function initLayout() {
1059
+ if (layoutInitialized) return;
1060
+
1061
+ const mainContent = document.getElementById('main-content');
1062
+ const outputArea = document.getElementById('output-area');
1063
+
1064
+ if (!mainContent) return;
1065
+
1066
+ layoutInitialized = true;
1067
+ mainContent.classList.add('initial-state');
1068
+
1069
+ // 初始化时立即隐藏空的输出区域 - 使用内联样式确保生效
1070
+ if (outputArea) {
1071
+ const content = outputArea.innerHTML.trim();
1072
+ const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
1073
+ if (!hasContent) {
1074
+ 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;';
1075
+ outputArea.classList.add('hidden-output');
1076
+ outputArea.classList.remove('has-content');
1077
+ }
1078
+ }
1079
+
1080
+ // 设置自动滚动监听
1081
+ setupAutoScroll();
1082
+
1083
+ const outputObserver = new MutationObserver(function() {
1084
+ const content = outputArea ? outputArea.innerHTML.trim() : '';
1085
+ const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
1086
+
1087
+ if (hasContent) {
1088
+ mainContent.classList.remove('initial-state');
1089
+ outputArea.classList.remove('hidden-output');
1090
+ outputArea.classList.add('has-content');
1091
+ 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;';
1092
+ setTimeout(scrollToBottom, 100);
1093
+ } else {
1094
+ mainContent.classList.add('initial-state');
1095
+ if (outputArea) {
1096
+ outputArea.classList.add('hidden-output');
1097
+ outputArea.classList.remove('has-content');
1098
+ outputArea.style.cssText = 'display: none !important; visibility: hidden !important; opacity: 0 !important; height: 0 !important; min-height: 0 !important; max-height: 0 !important;';
1099
+ }
1100
+ }
1101
+ });
1102
+
1103
+ if (outputArea) {
1104
+ outputObserver.observe(outputArea, { childList: true, subtree: true, characterData: true });
1105
+ }
1106
+
1107
+ const questionInput = document.querySelector('#question-input textarea');
1108
+ if (questionInput) {
1109
+ questionInput.focus();
1110
+ }
1111
+ }
1112
+
1113
+ const observer = new MutationObserver(function(mutations, obs) {
1114
+ initCarousel();
1115
+ initLayout();
1116
+ if (carouselInitialized && layoutInitialized) {
1117
+ obs.disconnect();
1118
+ }
1119
+ });
1120
+
1121
+ observer.observe(document.body, {
1122
+ childList: true,
1123
+ subtree: true
1124
+ });
1125
+
1126
+ // 立即尝试隐藏输出区域(在 DOM 完全加载之前)
1127
+ function hideOutputAreaEarly() {
1128
+ const outputArea = document.getElementById('output-area');
1129
+ if (outputArea) {
1130
+ const content = outputArea.innerHTML.trim();
1131
+ const hasContent = content !== '' && content !== '<div></div>' && !/^<div[^>]*>\s*<\/div>$/.test(content);
1132
+ if (!hasContent) {
1133
+ 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;';
1134
+ outputArea.classList.add('hidden-output');
1135
+ outputArea.classList.remove('has-content');
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ // 多次尝试隐藏,确保在各种时机都能生效
1141
+ hideOutputAreaEarly();
1142
+ document.addEventListener('DOMContentLoaded', hideOutputAreaEarly);
1143
+ setTimeout(hideOutputAreaEarly, 0);
1144
+ setTimeout(hideOutputAreaEarly, 100);
1145
+ setTimeout(hideOutputAreaEarly, 300);
1146
+ setTimeout(hideOutputAreaEarly, 500);
1147
+ setTimeout(function() { initCarousel(); initLayout(); }, 1000);
1148
+ setTimeout(function() { initCarousel(); initLayout(); }, 2000);
1149
+
1150
+ // 深色模式适配 - 动态更新 output-area 背景色(防闪烁优化版)
1151
+ let lastUpdateTime = 0;
1152
+ const UPDATE_THROTTLE = 50; // 最小更新间隔 50ms
1153
+
1154
+ function updateOutputAreaDarkMode() {
1155
+ const now = Date.now();
1156
+ if (now - lastUpdateTime < UPDATE_THROTTLE) {
1157
+ return; // 跳过过于频繁的更新
1158
+ }
1159
+ lastUpdateTime = now;
1160
+
1161
+ const outputArea = document.getElementById('output-area');
1162
+ if (!outputArea) return;
1163
+
1164
+ // 检查是否是深色模式
1165
+ const isDark = document.documentElement.classList.contains('dark') ||
1166
+ document.body.classList.contains('dark') ||
1167
+ window.matchMedia('(prefers-color-scheme: dark)').matches;
1168
+
1169
+ if (isDark) {
1170
+ // 深色模式:深色背景
1171
+ outputArea.style.setProperty('background', '#111827', 'important');
1172
+ outputArea.style.setProperty('border-color', '#374151', 'important');
1173
+ } else {
1174
+ // 浅色模式:浅色背景
1175
+ outputArea.style.setProperty('background', '#fafafa', 'important');
1176
+ outputArea.style.setProperty('border-color', '#e5e7eb', 'important');
1177
+ }
1178
+ }
1179
+
1180
+ // 延迟初始化,避免页面加载时闪烁
1181
+ setTimeout(function() {
1182
+ updateOutputAreaDarkMode();
1183
+
1184
+ // 监听深色模式变化
1185
+ const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
1186
+ darkModeMediaQuery.addEventListener('change', updateOutputAreaDarkMode);
1187
+
1188
+ // 监听 DOM class 变化
1189
+ const darkModeObserver = new MutationObserver(updateOutputAreaDarkMode);
1190
+ darkModeObserver.observe(document.documentElement, {
1191
+ attributes: true,
1192
+ attributeFilter: ['class']
1193
+ });
1194
+ darkModeObserver.observe(document.body, {
1195
+ attributes: true,
1196
+ attributeFilter: ['class']
1197
+ });
1198
+
1199
+ // 监听 output-area 的内容变化
1200
+ const outputArea = document.getElementById('output-area');
1201
+ if (outputArea) {
1202
+ const outputContentObserver = new MutationObserver(function() {
1203
+ // 内容变化时立即应用深色模式样式
1204
+ requestAnimationFrame(updateOutputAreaDarkMode);
1205
+ });
1206
+
1207
+ outputContentObserver.observe(outputArea, {
1208
+ childList: true,
1209
+ subtree: true
1210
+ });
1211
+ }
1212
+ }, 500); // 延迟 500ms 后再启动监听
1213
+ })();
1214
+ """
1215
+
1216
+ def create_interface():
1217
+ # Get the directory where this script is located for static files
1218
+ script_dir = os.path.dirname(os.path.abspath(__file__))
1219
+
1220
+ # Helper function to convert image to base64 for embedding in HTML
1221
+ def image_to_base64(image_path):
1222
+ """Convert image file to base64 string for HTML embedding."""
1223
+ try:
1224
+ with open(image_path, 'rb') as img_file:
1225
+ img_data = img_file.read()
1226
+ b64_string = base64.b64encode(img_data).decode('utf-8')
1227
+ # Determine MIME type based on file extension
1228
+ ext = image_path.lower().split('.')[-1]
1229
+ mime_types = {
1230
+ 'png': 'image/png',
1231
+ 'jpg': 'image/jpeg',
1232
+ 'jpeg': 'image/jpeg',
1233
+ 'svg': 'image/svg+xml',
1234
+ 'gif': 'image/gif'
1235
+ }
1236
+ mime_type = mime_types.get(ext, 'image/png')
1237
+ return f"data:{mime_type};base64,{b64_string}"
1238
+ except Exception as e:
1239
+ print(f"Error loading image {image_path}: {e}")
1240
+ return ""
1241
+
1242
+ # Inline CSS - all styles embedded directly
1243
+ INLINE_CSS = """
1244
+ /* Global Styles */
1245
+ .gradio-container {
1246
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
1247
+ }
1248
+
1249
+ /* Thinking Styles */
1250
+ .thinking-collapsed {
1251
+ background: #f9fafb;
1252
+ border: 1px solid #e5e7eb;
1253
+ border-radius: 8px;
1254
+ padding: 0.75rem;
1255
+ margin: 0.5rem 0;
1256
+ }
1257
+
1258
+ .thinking-collapsed summary {
1259
+ cursor: pointer;
1260
+ font-weight: 500;
1261
+ color: #6b7280;
1262
+ font-size: 0.875rem;
1263
+ }
1264
+
1265
+ .thinking-collapsed summary:hover {
1266
+ color: #374151;
1267
+ }
1268
+
1269
+ .thinking-content {
1270
+ margin-top: 0.5rem;
1271
+ color: #374151;
1272
+ white-space: pre-wrap;
1273
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
1274
+ font-size: 0.875rem;
1275
+ line-height: 1.5;
1276
+ }
1277
+
1278
+ .thinking-streaming {
1279
+ background: #f0f9ff;
1280
+ border: 1px solid #bae6fd;
1281
+ border-radius: 8px;
1282
+ padding: 0.875rem;
1283
+ margin: 0.5rem 0;
1284
+ color: #0c4a6e;
1285
+ white-space: pre-wrap;
1286
+ font-family: 'SF Mono', Monaco, monospace;
1287
+ font-size: 0.875rem;
1288
+ line-height: 1.5;
1289
+ }
1290
+
1291
+ /* Tool Call Card */
1292
+ .tool-call-card {
1293
+ background: #f9fafb;
1294
+ border-radius: 8px;
1295
+ padding: 1rem;
1296
+ margin: 0.75rem 0;
1297
+ }
1298
+
1299
+ .tool-info {
1300
+ display: flex;
1301
+ flex-direction: column;
1302
+ gap: 0.25rem;
1303
+ }
1304
+
1305
+ .tool-name {
1306
+ font-weight: 600;
1307
+ color: #374151;
1308
+ font-size: 0.875rem;
1309
+ }
1310
+
1311
+ .tool-detail {
1312
+ color: #6b7280;
1313
+ font-size: 0.8rem;
1314
+ }
1315
+
1316
+ /* Result Card */
1317
+ .result-card-expanded {
1318
+ background: white;
1319
+ border-radius: 8px;
1320
+ padding: 1.25rem;
1321
+ margin: 1rem 0;
1322
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
1323
+ }
1324
+
1325
+ .result-header-expanded {
1326
+ font-weight: 600;
1327
+ color: #374151;
1328
+ margin-bottom: 1.25rem;
1329
+ padding-bottom: 0.5rem;
1330
+ border-bottom: 1px solid #e5e7eb;
1331
+ font-size: 1rem;
1332
+ }
1333
+
1334
+ .result-content-expanded {
1335
+ color: #4b5563;
1336
+ line-height: 1.6;
1337
+ }
1338
+
1339
+ .result-content-expanded p {
1340
+ margin: 0.5rem 0;
1341
+ }
1342
+
1343
+ .result-content-expanded code {
1344
+ background: #f3f4f6;
1345
+ padding: 0.125rem 0.375rem;
1346
+ border-radius: 3px;
1347
+ font-family: monospace;
1348
+ font-size: 0.875em;
1349
+ }
1350
+
1351
+ /* Search Result Card Hover */
1352
+ .search-result-card {
1353
+ transition: all 0.2s ease;
1354
+ }
1355
+
1356
+ .search-result-card:hover {
1357
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15) !important;
1358
+ border-color: #667eea !important;
1359
+ }
1360
+
1361
+ /* Answer Section */
1362
+ .answer-section {
1363
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
1364
+ border-left: 4px solid #10a37f;
1365
+ border-radius: 8px;
1366
+ padding: 1.5rem;
1367
+ margin: 1rem 0;
1368
+ }
1369
+
1370
+ .answer-section p {
1371
+ color: #374151;
1372
+ line-height: 1.7;
1373
+ margin: 0.5rem 0;
1374
+ }
1375
+
1376
+ .answer-section strong {
1377
+ color: #1e293b;
1378
+ font-weight: 600;
1379
+ }
1380
+
1381
+ .answer-section a {
1382
+ color: #10a37f;
1383
+ text-decoration: none;
1384
+ font-weight: 500;
1385
+ }
1386
+
1387
+ .answer-section a:hover {
1388
+ text-decoration: underline;
1389
+ }
1390
+
1391
+ /* User Message Bubble - 淡蓝色背景,右对齐 */
1392
+ .user-message-bubble {
1393
+ background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
1394
+ color: #0c4a6e;
1395
+ border-radius: 1rem 1rem 0.25rem 1rem;
1396
+ padding: 1rem 1.25rem;
1397
+ margin: 1rem 0 1rem auto;
1398
+ max-width: 80%;
1399
+ box-shadow: 0 2px 8px rgba(14, 165, 233, 0.2);
1400
+ border: 1px solid #7dd3fc;
1401
+ text-align: right;
1402
+ }
1403
+
1404
+ .user-message-content {
1405
+ line-height: 1.6;
1406
+ font-size: 0.95rem;
1407
+ }
1408
+
1409
+ /* Output area - 固定高度可滚动 */
1410
+ /* 强��固定高度,内容在里面滚动 */
1411
+ #output-area,
1412
+ #output-area.output-box,
1413
+ div#output-area {
1414
+ height: 50vh !important;
1415
+ min-height: 250px !important;
1416
+ max-height: 50vh !important;
1417
+ overflow-y: scroll !important;
1418
+ overflow-x: hidden !important;
1419
+ padding: 1rem !important;
1420
+ border: 1px solid #e5e7eb !important;
1421
+ border-radius: 8px !important;
1422
+ background: #fafafa !important;
1423
+ scroll-behavior: smooth;
1424
+ flex-shrink: 0 !important;
1425
+ flex-grow: 0 !important;
1426
+ }
1427
+
1428
+ /* 内部所有元素不能撑破容器 */
1429
+ #output-area * {
1430
+ max-height: none !important;
1431
+ overflow: visible !important;
1432
+ }
1433
+
1434
+ #output-area > div {
1435
+ height: auto !important;
1436
+ max-height: none !important;
1437
+ overflow: visible !important;
1438
+ border: none !important;
1439
+ background: transparent !important;
1440
+ padding: 0 !important;
1441
+ margin: 0 !important;
1442
+ box-shadow: none !important;
1443
+ }
1444
+
1445
+ /* 初始状态和空内容时隐藏 output-area */
1446
+ #output-area:empty,
1447
+ #output-area.hidden-output,
1448
+ #output-area:not(.has-content),
1449
+ .hidden-output#output-area,
1450
+ div.hidden-output#output-area,
1451
+ #main-content #output-area.hidden-output,
1452
+ #main-content .output-box.hidden-output {
1453
+ display: none !important;
1454
+ visibility: hidden !important;
1455
+ height: 0 !important;
1456
+ min-height: 0 !important;
1457
+ max-height: 0 !important;
1458
+ padding: 0 !important;
1459
+ margin: 0 !important;
1460
+ border: none !important;
1461
+ opacity: 0 !important;
1462
+ overflow: hidden !important;
1463
+ }
1464
+
1465
+ /* 防止内部内容撑破容器 */
1466
+ #output-area > * {
1467
+ max-width: 100%;
1468
+ word-wrap: break-word;
1469
+ overflow-wrap: break-word;
1470
+ }
1471
+
1472
+ /* 主内容区域布局 - 限制整体高度 */
1473
+ #main-content {
1474
+ display: flex !important;
1475
+ flex-direction: column !important;
1476
+ height: auto !important;
1477
+ max-height: none !important;
1478
+ overflow: visible !important;
1479
+ }
1480
+
1481
+ /* Gradio 包装容器限制 */
1482
+ #main-content > div {
1483
+ flex-shrink: 0;
1484
+ }
1485
+
1486
+ #output-area::-webkit-scrollbar {
1487
+ width: 8px;
1488
+ }
1489
+
1490
+ #output-area::-webkit-scrollbar-track {
1491
+ background: #f1f1f1;
1492
+ border-radius: 4px;
1493
+ }
1494
+
1495
+ #output-area::-webkit-scrollbar-thumb {
1496
+ background: #c1c1c1;
1497
+ border-radius: 4px;
1498
+ }
1499
+
1500
+ #output-area::-webkit-scrollbar-thumb:hover {
1501
+ background: #a1a1a1;
1502
+ }
1503
+
1504
+ /* 自动滚动控制按钮样式 */
1505
+ #auto-scroll-checkbox {
1506
+ margin-top: 0.5rem;
1507
+ }
1508
+
1509
+ #auto-scroll-checkbox label {
1510
+ font-size: 0.85rem;
1511
+ color: #4b5563;
1512
+ }
1513
+
1514
+ /* Completion Message */
1515
+ .completion-msg {
1516
+ text-align: center;
1517
+ color: #10a37f;
1518
+ font-weight: 600;
1519
+ padding: 1rem;
1520
+ margin: 1rem 0;
1521
+ background: #f0fdf4;
1522
+ border-radius: 8px;
1523
+ border: 1px solid #86efac;
1524
+ }
1525
+
1526
+ /* Error Message */
1527
+ .error-message {
1528
+ background: #fee2e2;
1529
+ border-left: 4px solid #dc2626;
1530
+ border-radius: 8px;
1531
+ padding: 1rem;
1532
+ margin: 1rem 0;
1533
+ color: #991b1b;
1534
+ }
1535
+
1536
+ .error-message strong {
1537
+ color: #7f1d1d;
1538
+ }
1539
+
1540
+ .error-message a {
1541
+ color: #dc2626;
1542
+ font-weight: 500;
1543
+ }
1544
+
1545
+ /* Round Badge - 淡蓝色背景 */
1546
+ .round-badge {
1547
+ display: inline-block;
1548
+ background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
1549
+ color: #0369a1;
1550
+ padding: 0.25rem 0.75rem;
1551
+ border-radius: 999px;
1552
+ font-size: 0.75rem;
1553
+ font-weight: 600;
1554
+ margin: 0.5rem 0;
1555
+ box-shadow: 0 2px 4px rgba(14, 165, 233, 0.15);
1556
+ border: 1px solid #7dd3fc;
1557
+ }
1558
+
1559
+ /* Settings Section */
1560
+ .settings-header {
1561
+ display: flex;
1562
+ align-items: center;
1563
+ gap: 0.5rem;
1564
+ margin-bottom: 0.875rem;
1565
+ }
1566
+
1567
+ .settings-title {
1568
+ font-size: 0.875rem;
1569
+ font-weight: 600;
1570
+ color: #374151;
1571
+ }
1572
+
1573
+ .settings-api-row {
1574
+ display: flex;
1575
+ align-items: center;
1576
+ justify-content: space-between;
1577
+ margin-bottom: 0.375rem;
1578
+ }
1579
+
1580
+ .settings-label {
1581
+ font-size: 0.8rem;
1582
+ font-weight: 500;
1583
+ color: #4b5563;
1584
+ }
1585
+
1586
+ .settings-help-link {
1587
+ display: inline-flex;
1588
+ align-items: center;
1589
+ gap: 0.25rem;
1590
+ font-size: 0.7rem;
1591
+ color: #667eea;
1592
+ text-decoration: none;
1593
+ transition: opacity 0.2s;
1594
+ }
1595
+
1596
+ .settings-help-icon {
1597
+ display: inline-flex;
1598
+ align-items: center;
1599
+ justify-content: center;
1600
+ width: 14px;
1601
+ height: 14px;
1602
+ border-radius: 50%;
1603
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1604
+ color: white;
1605
+ font-size: 0.6rem;
1606
+ font-weight: bold;
1607
+ }
1608
+
1609
+ /* Tools Section */
1610
+ .tools-section {
1611
+ margin-top: 1rem;
1612
+ display: grid;
1613
+ grid-template-columns: 1fr 1fr;
1614
+ gap: 0.5rem;
1615
+ }
1616
+
1617
+ .tools-title {
1618
+ grid-column: 1 / -1;
1619
+ font-size: 0.875rem;
1620
+ font-weight: 600;
1621
+ color: #374151;
1622
+ margin-bottom: 0.25rem;
1623
+ margin-top: 0.5rem;
1624
+ }
1625
+
1626
+ /* Tool Item */
1627
+ .tool-item {
1628
+ background: #f9fafb;
1629
+ padding: 0.75rem;
1630
+ border-radius: 6px;
1631
+ margin-bottom: 0;
1632
+ border: 1px solid #e5e7eb;
1633
+ height: 100%;
1634
+ display: flex;
1635
+ flex-direction: row;
1636
+ align-items: center;
1637
+ gap: 0.5rem;
1638
+ }
1639
+
1640
+ .tool-item strong {
1641
+ color: #374151;
1642
+ font-size: 0.85rem;
1643
+ }
1644
+
1645
+ .tool-item span {
1646
+ color: #6b7280;
1647
+ font-size: 0.8rem;
1648
+ }
1649
+
1650
+ /* Examples Section */
1651
+ .examples-section {
1652
+ margin-top: -0.5rem;
1653
+ }
1654
+
1655
+ .examples-title {
1656
+ font-size: 0.875rem;
1657
+ font-weight: 600;
1658
+ color: #374151;
1659
+ margin-bottom: 0.5rem;
1660
+ }
1661
+
1662
+ /* Example Carousel */
1663
+ .example-carousel {
1664
+ background: white;
1665
+ border-radius: 8px;
1666
+ padding: 1rem;
1667
+ border: 1px solid #e5e7eb;
1668
+ }
1669
+
1670
+ .carousel-container {
1671
+ position: relative;
1672
+ min-height: 60px;
1673
+ margin-bottom: 0.75rem;
1674
+ }
1675
+
1676
+ .carousel-item {
1677
+ display: none;
1678
+ opacity: 0;
1679
+ transition: opacity 0.3s ease;
1680
+ }
1681
+
1682
+ .carousel-item.active {
1683
+ display: block;
1684
+ opacity: 1;
1685
+ }
1686
+
1687
+ .carousel-item-text {
1688
+ background: linear-gradient(135deg, #f0f4ff 0%, #e8eeff 100%);
1689
+ padding: 1rem;
1690
+ border-radius: 6px;
1691
+ color: #374151;
1692
+ font-size: 0.875rem;
1693
+ line-height: 1.5;
1694
+ border: 1px solid #e0e7ff;
1695
+ }
1696
+
1697
+ .carousel-controls {
1698
+ display: flex;
1699
+ align-items: center;
1700
+ justify-content: center;
1701
+ gap: 1rem;
1702
+ }
1703
+
1704
+ .carousel-btn {
1705
+ cursor: pointer;
1706
+ width: 32px;
1707
+ height: 32px;
1708
+ border-radius: 50%;
1709
+ background: #f3f4f6;
1710
+ display: flex;
1711
+ align-items: center;
1712
+ justify-content: center;
1713
+ font-size: 1.25rem;
1714
+ color: #6b7280;
1715
+ transition: all 0.2s ease;
1716
+ user-select: none;
1717
+ }
1718
+
1719
+ .carousel-btn:hover {
1720
+ background: #667eea;
1721
+ color: white;
1722
+ }
1723
+
1724
+ .carousel-indicators {
1725
+ display: flex;
1726
+ gap: 0.5rem;
1727
+ }
1728
+
1729
+ .carousel-dot {
1730
+ width: 8px;
1731
+ height: 8px;
1732
+ border-radius: 50%;
1733
+ background: #d1d5db;
1734
+ cursor: pointer;
1735
+ transition: all 0.2s ease;
1736
+ }
1737
+
1738
+ .carousel-dot.active {
1739
+ background: #667eea;
1740
+ width: 24px;
1741
+ border-radius: 4px;
1742
+ }
1743
+
1744
+ /* Welcome Message */
1745
+ .welcome-container {
1746
+ text-align: center;
1747
+ padding: 3rem 2rem;
1748
+ }
1749
+
1750
+ .welcome-title {
1751
+ font-size: 2rem;
1752
+ font-weight: 700;
1753
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1754
+ -webkit-background-clip: text;
1755
+ -webkit-text-fill-color: transparent;
1756
+ background-clip: text;
1757
+ margin-bottom: 1rem;
1758
+ }
1759
+
1760
+ .welcome-subtitle {
1761
+ color: #6b7280;
1762
+ font-size: 1.1rem;
1763
+ margin-bottom: 2rem;
1764
+ }
1765
+
1766
+ /* Footer */
1767
+ .footer-container {
1768
+ text-align: center;
1769
+ padding: 1.5rem;
1770
+ color: #9ca3af;
1771
+ font-size: 0.875rem;
1772
+ border-top: 1px solid #e5e7eb;
1773
+ margin-top: 2rem;
1774
+ }
1775
+
1776
+ .footer-container a {
1777
+ color: #667eea;
1778
+ text-decoration: none;
1779
+ }
1780
+
1781
+ .footer-container a:hover {
1782
+ text-decoration: underline;
1783
+ }
1784
+
1785
+ /* Disclaimer */
1786
+ .disclaimer {
1787
+ text-align: center;
1788
+ padding: 1rem;
1789
+ color: #6b7280;
1790
+ font-size: 0.875rem;
1791
+ border-top: 1px solid #e5e7eb;
1792
+ margin-top: 1rem;
1793
+ }
1794
+
1795
+ /* ========== 深色模式适配 ========== */
1796
+ @media (prefers-color-scheme: dark) {
1797
+ /* Settings 区域 */
1798
+ #settings-group {
1799
+ background: #1f2937 !important;
1800
+ border-color: #374151 !important;
1801
+ }
1802
+
1803
+ .settings-title,
1804
+ .tools-title,
1805
+ .examples-title {
1806
+ color: #e5e7eb !important;
1807
+ }
1808
+
1809
+ .settings-label {
1810
+ color: #9ca3af !important;
1811
+ }
1812
+
1813
+ .settings-help-link {
1814
+ color: #818cf8 !important;
1815
+ }
1816
+
1817
+ /* Available Tools 区域 */
1818
+ .tool-item {
1819
+ background: #1f2937 !important;
1820
+ border-color: #374151 !important;
1821
+ }
1822
+
1823
+ .tool-item strong {
1824
+ color: #e5e7eb !important;
1825
+ }
1826
+
1827
+ .tool-item span {
1828
+ color: #9ca3af !important;
1829
+ }
1830
+
1831
+ /* Example Carousel 区域 */
1832
+ .example-carousel {
1833
+ background: #1f2937 !important;
1834
+ border-color: #374151 !important;
1835
+ }
1836
+
1837
+ .carousel-item-text {
1838
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
1839
+ border-color: #3b82f6 !important;
1840
+ color: #e0f2fe !important;
1841
+ }
1842
+
1843
+ .carousel-btn {
1844
+ background: #374151 !important;
1845
+ color: #9ca3af !important;
1846
+ }
1847
+
1848
+ .carousel-btn:hover {
1849
+ background: #667eea !important;
1850
+ color: white !important;
1851
+ }
1852
+
1853
+ .carousel-dot {
1854
+ background: #4b5563 !important;
1855
+ }
1856
+
1857
+ .carousel-dot.active {
1858
+ background: #667eea !important;
1859
+ }
1860
+
1861
+ /* Output area 深色模式 */
1862
+ #output-area,
1863
+ #output-area.output-box,
1864
+ div#output-area {
1865
+ background: #111827 !important;
1866
+ border: 1px solid #374151 !important;
1867
+ }
1868
+
1869
+ #output-area::-webkit-scrollbar-track {
1870
+ background: #1f2937 !important;
1871
+ }
1872
+
1873
+ #output-area::-webkit-scrollbar-thumb {
1874
+ background: #4b5563 !important;
1875
+ }
1876
+
1877
+ #output-area::-webkit-scrollbar-thumb:hover {
1878
+ background: #6b7280 !important;
1879
+ }
1880
+
1881
+ /* Tool call card 深色模式 */
1882
+ .tool-call-card {
1883
+ background: #1f2937 !important;
1884
+ }
1885
+
1886
+ .tool-name {
1887
+ color: #e5e7eb !important;
1888
+ }
1889
+
1890
+ .tool-detail {
1891
+ color: #9ca3af !important;
1892
+ }
1893
+
1894
+ /* Result card 深色模式 */
1895
+ .result-card-expanded {
1896
+ background: #1f2937 !important;
1897
+ }
1898
+
1899
+ .result-header-expanded {
1900
+ color: #e5e7eb !important;
1901
+ border-bottom-color: #374151 !important;
1902
+ }
1903
+
1904
+ .result-content-expanded {
1905
+ color: #d1d5db !important;
1906
+ }
1907
+
1908
+ /* Thinking 深色模式 */
1909
+ .thinking-collapsed {
1910
+ background: #1f2937 !important;
1911
+ border-color: #374151 !important;
1912
+ }
1913
+
1914
+ .thinking-collapsed summary {
1915
+ color: #9ca3af !important;
1916
+ }
1917
+
1918
+ .thinking-content {
1919
+ color: #d1d5db !important;
1920
+ }
1921
+
1922
+ .thinking-streaming {
1923
+ background: #0c4a6e !important;
1924
+ border-color: #0369a1 !important;
1925
+ color: #bae6fd !important;
1926
+ }
1927
+
1928
+ /* Answer section 深色模式 */
1929
+ .answer-section {
1930
+ background: linear-gradient(135deg, #0c4a6e 0%, #164e63 100%) !important;
1931
+ }
1932
+
1933
+ .answer-section p {
1934
+ color: #e0f2fe !important;
1935
+ }
1936
+
1937
+ .answer-section strong {
1938
+ color: #f0f9ff !important;
1939
+ }
1940
+
1941
+ /* 用户问题气泡深色模式 */
1942
+ .user-message-bubble {
1943
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
1944
+ color: #e0f2fe !important;
1945
+ border-color: #3b82f6 !important;
1946
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
1947
+ }
1948
+
1949
+ .user-message-content {
1950
+ color: #e0f2fe !important;
1951
+ }
1952
+
1953
+ /* Round Badge 深色模式 */
1954
+ .round-badge {
1955
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
1956
+ color: #93c5fd !important;
1957
+ border-color: #3b82f6 !important;
1958
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2) !important;
1959
+ }
1960
+
1961
+ /* 搜索结果卡片深色模式 */
1962
+ .search-result-card {
1963
+ background: #1f2937 !important;
1964
+ border-color: #374151 !important;
1965
+ }
1966
+
1967
+ .search-result-card:hover {
1968
+ border-color: #667eea !important;
1969
+ }
1970
+
1971
+ /* 工具结果标题区域深色模式 */
1972
+ .result-card-expanded div[style*="background: linear-gradient"] {
1973
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
1974
+ border-color: #3b82f6 !important;
1975
+ }
1976
+
1977
+ .result-card-expanded div[style*="background: linear-gradient"] span[style*="color: #1e40af"],
1978
+ .result-card-expanded div[style*="background: linear-gradient"] a[style*="color: #1e40af"] {
1979
+ color: #93c5fd !important;
1980
+ }
1981
+
1982
+ .result-card-expanded div[style*="color: #64748b"] {
1983
+ color: #9ca3af !important;
1984
+ }
1985
+
1986
+ /* 完成消息深色模式 */
1987
+ .completion-msg {
1988
+ background: #064e3b !important;
1989
+ border-color: #059669 !important;
1990
+ color: #6ee7b7 !important;
1991
+ }
1992
+
1993
+ /* 错误消息深色模式 */
1994
+ .error-message {
1995
+ background: #450a0a !important;
1996
+ border-color: #b91c1c !important;
1997
+ color: #fca5a5 !important;
1998
+ }
1999
+
2000
+ /* 侧边栏标题深色模式 */
2001
+ div[style*="font-weight: 600"][style*="color: #374151"] {
2002
+ color: #e5e7eb !important;
2003
+ }
2004
+
2005
+ /* Disclaimer 深色模式 */
2006
+ .disclaimer {
2007
+ color: #9ca3af !important;
2008
+ border-color: #374151 !important;
2009
+ }
2010
+ }
2011
+
2012
+ /* Gradio 深色主题类名适配 */
2013
+ .dark #settings-group,
2014
+ .dark .tool-item,
2015
+ .dark .example-carousel {
2016
+ background: #1f2937 !important;
2017
+ border-color: #374151 !important;
2018
+ }
2019
+
2020
+ .dark .settings-title,
2021
+ .dark .tools-title,
2022
+ .dark .examples-title,
2023
+ .dark .tool-item strong,
2024
+ .dark .result-header-expanded,
2025
+ .dark .tool-name {
2026
+ color: #e5e7eb !important;
2027
+ }
2028
+
2029
+ .dark .settings-label,
2030
+ .dark .tool-item span,
2031
+ .dark .tool-detail,
2032
+ .dark .thinking-collapsed summary {
2033
+ color: #9ca3af !important;
2034
+ }
2035
+
2036
+ .dark .settings-help-link {
2037
+ color: #818cf8 !important;
2038
+ }
2039
+
2040
+ .dark .carousel-item-text {
2041
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2042
+ border-color: #3b82f6 !important;
2043
+ color: #e0f2fe !important;
2044
+ }
2045
+
2046
+ .dark #output-area,
2047
+ .dark #output-area.output-box,
2048
+ .dark div#output-area {
2049
+ background: #111827 !important;
2050
+ border: 1px solid #374151 !important;
2051
+ }
2052
+
2053
+ .dark .tool-call-card,
2054
+ .dark .result-card-expanded,
2055
+ .dark .thinking-collapsed {
2056
+ background: #1f2937 !important;
2057
+ border-color: #374151 !important;
2058
+ }
2059
+
2060
+ .dark .result-content-expanded,
2061
+ .dark .thinking-content {
2062
+ color: #d1d5db !important;
2063
+ }
2064
+
2065
+ .dark .answer-section {
2066
+ background: linear-gradient(135deg, #0c4a6e 0%, #164e63 100%) !important;
2067
+ }
2068
+
2069
+ .dark .answer-section p {
2070
+ color: #e0f2fe !important;
2071
+ }
2072
+
2073
+ .dark .search-result-card {
2074
+ background: #1f2937 !important;
2075
+ border-color: #374151 !important;
2076
+ }
2077
+
2078
+ .dark .completion-msg {
2079
+ background: #064e3b !important;
2080
+ border-color: #059669 !important;
2081
+ color: #6ee7b7 !important;
2082
+ }
2083
+
2084
+ /* 用户问题气泡 Gradio dark 模式 */
2085
+ .dark .user-message-bubble {
2086
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2087
+ color: #e0f2fe !important;
2088
+ border-color: #3b82f6 !important;
2089
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3) !important;
2090
+ }
2091
+
2092
+ .dark .user-message-content {
2093
+ color: #e0f2fe !important;
2094
+ }
2095
+
2096
+ /* Round Badge Gradio dark 模式 */
2097
+ .dark .round-badge {
2098
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2099
+ color: #93c5fd !important;
2100
+ border-color: #3b82f6 !important;
2101
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2) !important;
2102
+ }
2103
+
2104
+ /* 工具结果标题 Gradio dark 模式 */
2105
+ .dark .result-card-expanded div[style*="background: linear-gradient"] {
2106
+ background: linear-gradient(135deg, #1e3a5f 0%, #1e40af 100%) !important;
2107
+ border-color: #3b82f6 !important;
2108
+ }
2109
+
2110
+ /* Disclaimer Gradio dark 模式 */
2111
+ .dark .disclaimer {
2112
+ color: #9ca3af !important;
2113
+ border-color: #374151 !important;
2114
+ }
2115
+ """
2116
+
2117
+ with gr.Blocks(css=INLINE_CSS, theme=gr.themes.Soft(), js=CAROUSEL_JS) as demo:
2118
+ # Header with logo and title images - convert to base64 for proper rendering
2119
+ logo_path = os.path.join(os.path.dirname(script_dir), "assets", "imgs", "or-logo1.png")
2120
+ title_path = os.path.join(os.path.dirname(script_dir), "assets", "imgs", "openresearcher-title.svg")
2121
+
2122
+ logo_base64 = image_to_base64(logo_path)
2123
+ title_base64 = image_to_base64(title_path)
2124
+
2125
+ # Build header HTML with base64 images
2126
+ header_html = f"""
2127
+ <div style="
2128
+ text-align: center;
2129
+ padding: 0.5rem 2rem 0rem 2rem;
2130
+ background: transparent;
2131
+ display: flex;
2132
+ flex-direction: row;
2133
+ align-items: center;
2134
+ justify-content: center;
2135
+ gap: 1.5rem;
2136
+ ">
2137
+ """
2138
+
2139
+ if logo_base64:
2140
+ header_html += f'<img src="{logo_base64}" alt="OpenResearcher Logo" style="height: 84px;">'
2141
+ if title_base64:
2142
+ header_html += f'<img src="{title_base64}" alt="OpenResearcher" style="height: 84px;">'
2143
+
2144
+ header_html += "</div>"
2145
+
2146
+ gr.HTML(header_html)
2147
+
2148
+ gr.HTML("""
2149
+ <div style="display: flex; gap: 5px; justify-content: center; flex-wrap: wrap; margin-top: 0px; margin-bottom: 24px;">
2150
+ <a href="https://huggingface.co/datasets/OpenResearcher/OpenResearcher-Dataset" target="_blank">
2151
+ <img src="https://img.shields.io/badge/Dataset-FFB7B2?style=for-the-badge&logo=huggingface&logoColor=ffffff" alt="Dataset" style="height: 28px;">
2152
+ </a>
2153
+ <a href="https://huggingface.co/OpenResearcher/Nemotron-3-Nano-30B-A3B" target="_blank">
2154
+ <img src="https://img.shields.io/badge/Model-FFD966?style=for-the-badge&logo=huggingface&logoColor=ffffff" alt="Model" style="height: 28px;">
2155
+ </a>
2156
+ <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">
2157
+ <img src="https://img.shields.io/badge/Blog-4285F4?style=for-the-badge&logo=google-chrome&logoColor=white" alt="Blog" style="height: 28px;">
2158
+ </a>
2159
+ <a href="https://wandb.ai/dongfu/nano-v3-sft-search" target="_blank">
2160
+ <img src="https://img.shields.io/badge/WandB%20Logs-48B5A3?style=for-the-badge&logo=weightsandbiases&logoColor=white" alt="WandB Logs" style="height: 28px;">
2161
+ </a>
2162
+ <a href="https://huggingface.co/datasets/OpenResearcher/OpenResearcher-Eval-Logs/tree/main" target="_blank">
2163
+ <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;">
2164
+ </a>
2165
+ </div>
2166
+ """)
2167
+
2168
+ # Main layout: Left sidebar + Right content
2169
+ with gr.Row():
2170
+ # Left Sidebar (settings & tools)
2171
+ with gr.Column(scale=1, min_width=280):
2172
+ # API Settings in a unified box
2173
+ with gr.Group(elem_id="settings-group"):
2174
+ gr.HTML('''
2175
+ <div class="settings-header">
2176
+ <span class="settings-title">⚙️ Settings</span>
2177
+ </div>
2178
+ <div class="settings-api-row">
2179
+ <span class="settings-label">Serper API Key</span>
2180
+ <a href="https://serper.dev/" target="_blank" class="settings-help-link" title="How to get Serper API Key">
2181
+ <span class="settings-help-icon">?</span>
2182
+ <span>How to get</span>
2183
+ </a>
2184
+ </div>
2185
+ ''')
2186
+ serper_input = gr.Textbox(
2187
+ label="",
2188
+ value=SERPER_API_KEY,
2189
+ type="password",
2190
+ placeholder="Enter your Serper API key...",
2191
+ show_label=False,
2192
+ elem_id="serper-api-input",
2193
+ container=False
2194
+ )
2195
+ max_rounds_input = gr.Slider(
2196
+ minimum=1,
2197
+ maximum=200,
2198
+ value=50,
2199
+ step=1,
2200
+ label="Max Rounds",
2201
+ elem_id="max-rounds-slider"
2202
+ )
2203
+ auto_scroll_checkbox = gr.Checkbox(
2204
+ label="Auto Scroll",
2205
+ value=True,
2206
+ elem_id="auto-scroll-checkbox",
2207
+ interactive=True
2208
+ )
2209
+
2210
+ # Tools info
2211
+ gr.HTML("""
2212
+ <div class="tools-section">
2213
+ <div class="tools-title">🛠️ Available Tools</div>
2214
+ <div class="tool-item"><strong>browser.search</strong> <span>Search the web</span></div>
2215
+ <div class="tool-item"><strong>browser.open</strong> <span>Open & read pages</span></div>
2216
+ <div class="tool-item"><strong>browser.find</strong> <span>Find text in page</span></div>
2217
+ </div>
2218
+ """)
2219
+
2220
+ # Example carousel with navigation
2221
+ gr.HTML("""
2222
+ <div class="examples-section">
2223
+ <div class="examples-title">💡 Try Examples</div>
2224
+ <div class="example-carousel" id="example-carousel">
2225
+ <div class="carousel-container">
2226
+ <div class="carousel-item active" data-index="0" data-text="Who won the Nobel Prize in Physics 2024?">
2227
+ <div class="carousel-item-text">🏆 Who won the Nobel Prize in Physics 2024?</div>
2228
+ </div>
2229
+ <div class="carousel-item" data-index="1" data-text="What are the latest breakthroughs in quantum computing in 2024?">
2230
+ <div class="carousel-item-text">🔬 What are the latest breakthroughs in quantum computing in 2024?</div>
2231
+ </div>
2232
+ <div class="carousel-item" data-index="2" data-text="What are the new features in Python 3.12?">
2233
+ <div class="carousel-item-text">🐍 What are the new features in Python 3.12?</div>
2234
+ </div>
2235
+ </div>
2236
+ <div class="carousel-controls">
2237
+ <div class="carousel-btn" id="prev-btn">‹</div>
2238
+ <div class="carousel-indicators">
2239
+ <div class="carousel-dot active" data-index="0"></div>
2240
+ <div class="carousel-dot" data-index="1"></div>
2241
+ <div class="carousel-dot" data-index="2"></div>
2242
+ </div>
2243
+ <div class="carousel-btn" id="next-btn">›</div>
2244
+ </div>
2245
+ </div>
2246
+ </div>
2247
+ """)
2248
+
2249
+ # Main content area (30-70)
2250
+ with gr.Column(scale=3, elem_id="main-content"):
2251
+ # Output area (on top, hidden initially)
2252
+ output_area = gr.HTML(
2253
+ value="",
2254
+ elem_classes=["output-box"],
2255
+ elem_id="output-area",
2256
+ visible=True
2257
+ )
2258
+
2259
+ # Welcome message (will be hidden after first search)
2260
+ welcome_html = gr.HTML(
2261
+ value="""
2262
+ <div id="welcome-section" class="welcome-section">
2263
+ <h2>What Would You Like to Research?</h2>
2264
+ <p>I am OpenResearcher, a leading open-source Deep Research Agent, welcome to try!</p>
2265
+ </div>
2266
+ """,
2267
+ elem_id="welcome-container"
2268
+ )
2269
+
2270
+ # Input area at bottom
2271
+ question_input = gr.Textbox(
2272
+ label="",
2273
+ placeholder="Ask me anything and I'll handle the rest...",
2274
+ lines=2,
2275
+ show_label=False,
2276
+ elem_id="question-input",
2277
+ autofocus=True
2278
+ )
2279
+
2280
+ with gr.Row(elem_id="button-row"):
2281
+ submit_btn = gr.Button(
2282
+ "🔍 Start DeepResearch",
2283
+ variant="primary",
2284
+ elem_classes=["primary-btn"],
2285
+ scale=3
2286
+ )
2287
+ stop_btn = gr.Button("⏹ Stop", variant="stop", scale=1)
2288
+ clear_btn = gr.Button("🗑 Clear", scale=1)
2289
+
2290
+ # Function to hide welcome and show output
2291
+ def start_research(question, serper_key, max_rounds):
2292
+ # Generator that first hides welcome, then streams results
2293
+ # Also clears the input box for the next question
2294
+
2295
+ # Initial yield to immediately clear welcome, show loading in output, and clear input
2296
+ # IMPORTANT: Don't use empty string for output, or JS will hide the output area!
2297
+ yield "", '<div style="text-align: center; padding: 2rem; color: #6b7280;">Delving into it...</div>', ""
2298
+
2299
+ for result in run_agent_streaming(question, serper_key, max_rounds):
2300
+ yield "", result, ""
2301
+
2302
+ # Event handlers
2303
+ submit_event = submit_btn.click(
2304
+ fn=start_research,
2305
+ inputs=[question_input, serper_input, max_rounds_input],
2306
+ outputs=[welcome_html, output_area, question_input],
2307
+ show_progress="hidden"
2308
+ )
2309
+
2310
+ question_input.submit(
2311
+ fn=start_research,
2312
+ inputs=[question_input, serper_input, max_rounds_input],
2313
+ outputs=[welcome_html, output_area, question_input],
2314
+ show_progress="hidden"
2315
+ )
2316
+
2317
+ stop_btn.click(fn=None, inputs=None, outputs=None, cancels=[submit_event])
2318
+ clear_btn.click(
2319
+ fn=lambda: ("""
2320
+ <div id="welcome-section" class="welcome-section">
2321
+ <h2>What would you like to research?</h2>
2322
+ <p>Ask any question and I'll search the web to find answers</p>
2323
+ </div>
2324
+ """, "", ""),
2325
+ outputs=[welcome_html, output_area, question_input]
2326
+ )
2327
+
2328
+ # Disclaimer
2329
+ gr.HTML('''
2330
+ <div class="disclaimer">
2331
+ ⚠️ AI may generate incorrect information or citations. Please double-check important facts.
2332
+ </div>
2333
+ ''')
2334
+
2335
+ return demo
2336
+
2337
+ if __name__ == "__main__":
2338
+ print("="*60)
2339
+ print("OpenResearcher DeepSearch Agent - ZeroGPU Space")
2340
+ print("="*60)
2341
+ demo = create_interface()
2342
+ demo.queue().launch()
openresearcher-title.svg ADDED
or-logo1.png ADDED

Git LFS Details

  • SHA256: 4d6288904dea6e09ad8ec22bb342309c3bb774a96fc94698798f496649558418
  • Pointer size: 131 Bytes
  • Size of remote file: 618 kB
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio>=4.0.0
2
+ transformers<5.0.0
3
+ torch>=2.0.0
4
+ httpx
5
+ spaces
6
+ # OpenResearcher DeepSearch Agent - Hugging Face Space
7
+ bitsandbytes
8
+ sentencepiece
9
+ protobuf
10
+ json5
11
+ accelerate