hsaest commited on
Commit
97f5fd3
Β·
1 Parent(s): 092f9be

feat: integrate PythonInterpreter via SandboxFusion

Browse files
Files changed (2) hide show
  1. app.py +98 -4
  2. requirements.txt +2 -0
app.py CHANGED
@@ -34,6 +34,19 @@ DEFAULT_MAX_SEARCH_RESULTS = 10
34
  MAX_SEARCH_QUERIES_PER_CALL = 10
35
  MAX_VISIT_URLS_PER_CALL = 5
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  PAPER_URL = os.getenv("PAPER_URL", "https://arxiv.org/abs/2605.24218")
38
  CODE_URL = os.getenv("CODE_URL", "https://github.com/OSU-NLP-Group/QUEST")
39
  DATASET_URL = os.getenv("DATASET_URL", "https://huggingface.co/collections/osunlp/quest")
@@ -42,9 +55,8 @@ MODEL_URL = os.getenv("MODEL_URL", "https://huggingface.co/osunlp/QUEST-35B-RL")
42
 
43
  # --- System prompt ---------------------------------------------------------
44
  # Hosted Space prompt (based on inference/prompt.py in the GitHub repo).
45
- # PythonInterpreter is intentionally omitted here: this Space has no Python
46
- # sandbox backend. To try PythonInterpreter, run offline inference from
47
- # https://github.com/OSU-NLP-Group/QUEST with ENABLE_PYTHON_TOOL=true.
48
  QUEST_SYSTEM_PROMPT = """You are a deep research assistant. Your core function is to conduct thorough, multi-source investigations into any topic. You must handle both broad, open-domain inquiries and queries within specialized academic fields. For every request, synthesize information from credible, diverse sources to deliver a comprehensive, accurate, and objective response. When you have gathered sufficient information and are ready to provide the definitive response, you must enclose the entire final answer within <answer></answer> tags.
49
 
50
  # Tools
@@ -56,6 +68,7 @@ You are provided with function signatures within <tools></tools> XML tags:
56
  {"type": "function", "function": {"name": "search", "description": "Perform Google web searches then returns a string of the top search results. Accepts up to 10 queries per call.", "parameters": {"type": "object", "properties": {"query": {"type": "array", "items": {"type": "string", "description": "The search query."}, "minItems": 1, "maxItems": 10, "description": "The list of search queries. Use at most 10 queries per search call."}}, "required": ["query"]}}}
57
  {"type": "function", "function": {"name": "visit", "description": "Visit webpage(s) and return the summary of the content. Accepts up to 5 URLs per call.", "parameters": {"type": "object", "properties": {"url": {"type": "array", "items": {"type": "string"}, "minItems": 1, "maxItems": 5, "description": "The URL(s) of the webpage(s) to visit. Use at most 5 URLs per visit call."}, "goal": {"type": "string", "description": "The specific information goal for visiting webpage(s)."}}, "required": ["url", "goal"]}}}
58
  {"type": "function", "function": {"name": "google_scholar", "description": "Leverage Google Scholar to retrieve relevant information from academic publications. Accepts up to 10 queries per call.", "parameters": {"type": "object", "properties": {"query": {"type": "array", "items": {"type": "string", "description": "The search query."}, "minItems": 1, "maxItems": 10, "description": "The list of search queries for Google Scholar. Use at most 10 queries per call."}}, "required": ["query"]}}}
 
59
  </tools>
60
 
61
  Tool call batching limits: search and google_scholar process at most 10 queries per call; visit processes at most 5 URLs per call. If more sources are needed, make additional tool calls in later turns.
@@ -225,8 +238,32 @@ def build_current_date_context() -> str:
225
  )
226
 
227
 
 
 
 
228
  def build_system_prompt() -> str:
229
- return build_current_date_context() + "\n\n" + QUEST_SYSTEM_PROMPT + current_date_iso()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
 
232
  def build_user_prompt(question: str) -> str:
@@ -385,6 +422,9 @@ def format_tool_response_for_model(tool_name: Optional[str], response: Any) -> s
385
  )
386
  return _format_visit_single_for_model(response)
387
 
 
 
 
388
  if response.get("error"):
389
  return "[Tool Error] " + _clean_tool_text(response.get("error"))
390
  return str(response)
@@ -2445,6 +2485,49 @@ def run_visit(
2445
  return _run_visit_single(str(url or "").strip(), max_chars, goal)
2446
 
2447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2448
  def _build_client_for_model(model: str) -> Tuple[InferenceClient, str, List[str]]:
2449
  """Returns (client, primary_model_id, fallback_model_ids).
2450
 
@@ -2864,6 +2947,17 @@ def build_research_agent(
2864
  f"πŸ“š turn {turn}: scholar {ok_count}/{len(per_q)} ok"
2865
  )
2866
  yield _emit()
 
 
 
 
 
 
 
 
 
 
 
2867
  else:
2868
  tool_response = {"ok": False, "error": f"Unknown tool: {tool_name}"}
2869
  status_lines.append(f"⚠️ turn {turn}: unknown tool `{tool_name}`")
 
34
  MAX_SEARCH_QUERIES_PER_CALL = 10
35
  MAX_VISIT_URLS_PER_CALL = 5
36
 
37
+ # Python Interpreter via SandboxFusion ----------------------------------------
38
+ # Set ENABLE_PYTHON_TOOL=true and SANDBOX_FUSION_ENDPOINTS=http://host:8080,...
39
+ # in Space Secrets to enable the python_interpreter tool.
40
+ ENABLE_PYTHON_TOOL = os.getenv("ENABLE_PYTHON_TOOL", "").strip().lower() in ("1", "true", "yes")
41
+ _SANDBOX_FUSION_ENDPOINTS_RAW = (
42
+ os.getenv("SANDBOX_FUSION_ENDPOINTS") or os.getenv("SANDBOX_FUSION_ENDPOINT") or ""
43
+ ).strip()
44
+
45
+ def _get_python_endpoints() -> List[str]:
46
+ if _SANDBOX_FUSION_ENDPOINTS_RAW:
47
+ return [e.strip() for e in _SANDBOX_FUSION_ENDPOINTS_RAW.split(",") if e.strip()]
48
+ return []
49
+
50
  PAPER_URL = os.getenv("PAPER_URL", "https://arxiv.org/abs/2605.24218")
51
  CODE_URL = os.getenv("CODE_URL", "https://github.com/OSU-NLP-Group/QUEST")
52
  DATASET_URL = os.getenv("DATASET_URL", "https://huggingface.co/collections/osunlp/quest")
 
55
 
56
  # --- System prompt ---------------------------------------------------------
57
  # Hosted Space prompt (based on inference/prompt.py in the GitHub repo).
58
+ # Set ENABLE_PYTHON_TOOL=true and SANDBOX_FUSION_ENDPOINTS in Space Secrets
59
+ # to enable the PythonInterpreter tool.
 
60
  QUEST_SYSTEM_PROMPT = """You are a deep research assistant. Your core function is to conduct thorough, multi-source investigations into any topic. You must handle both broad, open-domain inquiries and queries within specialized academic fields. For every request, synthesize information from credible, diverse sources to deliver a comprehensive, accurate, and objective response. When you have gathered sufficient information and are ready to provide the definitive response, you must enclose the entire final answer within <answer></answer> tags.
61
 
62
  # Tools
 
68
  {"type": "function", "function": {"name": "search", "description": "Perform Google web searches then returns a string of the top search results. Accepts up to 10 queries per call.", "parameters": {"type": "object", "properties": {"query": {"type": "array", "items": {"type": "string", "description": "The search query."}, "minItems": 1, "maxItems": 10, "description": "The list of search queries. Use at most 10 queries per search call."}}, "required": ["query"]}}}
69
  {"type": "function", "function": {"name": "visit", "description": "Visit webpage(s) and return the summary of the content. Accepts up to 5 URLs per call.", "parameters": {"type": "object", "properties": {"url": {"type": "array", "items": {"type": "string"}, "minItems": 1, "maxItems": 5, "description": "The URL(s) of the webpage(s) to visit. Use at most 5 URLs per visit call."}, "goal": {"type": "string", "description": "The specific information goal for visiting webpage(s)."}}, "required": ["url", "goal"]}}}
70
  {"type": "function", "function": {"name": "google_scholar", "description": "Leverage Google Scholar to retrieve relevant information from academic publications. Accepts up to 10 queries per call.", "parameters": {"type": "object", "properties": {"query": {"type": "array", "items": {"type": "string", "description": "The search query."}, "minItems": 1, "maxItems": 10, "description": "The list of search queries for Google Scholar. Use at most 10 queries per call."}}, "required": ["query"]}}}
71
+ {PYTHON_TOOL_DEF}
72
  </tools>
73
 
74
  Tool call batching limits: search and google_scholar process at most 10 queries per call; visit processes at most 5 URLs per call. If more sources are needed, make additional tool calls in later turns.
 
238
  )
239
 
240
 
241
+ _PYTHON_TOOL_SIGNATURE = '{"type": "function", "function": {"name": "python_interpreter", "description": "Execute Python code in a sandboxed environment to perform calculations, data analysis, string processing, or any computational task. Always use print() for any output you want to see in the results.", "parameters": {"type": "object", "properties": {"code": {"type": "string", "description": "The Python code to execute. Use print() for output."}}, "required": ["code"]}}}'
242
+
243
+
244
  def build_system_prompt() -> str:
245
+ if ENABLE_PYTHON_TOOL and _get_python_endpoints():
246
+ python_line = _PYTHON_TOOL_SIGNATURE
247
+ batching_note = (
248
+ "Tool call batching limits: search and google_scholar process at most 10 queries "
249
+ "per call; visit processes at most 5 URLs per call; python_interpreter executes one "
250
+ "code block per call. If more sources are needed, make additional tool calls in later turns."
251
+ )
252
+ else:
253
+ python_line = ""
254
+ batching_note = (
255
+ "Tool call batching limits: search and google_scholar process at most 10 queries "
256
+ "per call; visit processes at most 5 URLs per call. If more sources are needed, "
257
+ "make additional tool calls in later turns."
258
+ )
259
+ prompt = QUEST_SYSTEM_PROMPT.replace("{PYTHON_TOOL_DEF}", python_line)
260
+ # Replace the static batching note with a potentially updated one
261
+ prompt = prompt.replace(
262
+ "Tool call batching limits: search and google_scholar process at most 10 queries per call; "
263
+ "visit processes at most 5 URLs per call. If more sources are needed, make additional tool calls in later turns.",
264
+ batching_note,
265
+ )
266
+ return build_current_date_context() + "\n\n" + prompt + current_date_iso()
267
 
268
 
269
  def build_user_prompt(question: str) -> str:
 
422
  )
423
  return _format_visit_single_for_model(response)
424
 
425
+ if tool_name == "python_interpreter":
426
+ return _clean_tool_text(response.get("result", response.get("error", str(response))))
427
+
428
  if response.get("error"):
429
  return "[Tool Error] " + _clean_tool_text(response.get("error"))
430
  return str(response)
 
2485
  return _run_visit_single(str(url or "").strip(), max_chars, goal)
2486
 
2487
 
2488
+ def _call_python_interpreter(code: str, timeout: int = 50) -> str:
2489
+ """Run Python code on a SandboxFusion endpoint. Mirrors tool_python.py logic."""
2490
+ import random as _random
2491
+ endpoints = _get_python_endpoints()
2492
+ if not endpoints:
2493
+ return "[Python Interpreter Error]: No sandbox endpoints configured. Set SANDBOX_FUSION_ENDPOINTS."
2494
+
2495
+ # Strip markdown code fences if model wrapped the code in them.
2496
+ triple = re.search(r'```[^\n]*\n(.+?)```', code, re.DOTALL)
2497
+ if triple:
2498
+ code = triple.group(1)
2499
+ if not code or not code.strip():
2500
+ return "[Python Interpreter Error]: Empty code."
2501
+
2502
+ try:
2503
+ from sandbox_fusion import run_code, RunCodeRequest # type: ignore
2504
+ except ImportError:
2505
+ return "[Python Interpreter Error]: sandbox_fusion package not installed."
2506
+
2507
+ last_error: Optional[str] = None
2508
+ for attempt in range(8):
2509
+ endpoint = _random.choice(endpoints)
2510
+ try:
2511
+ result = run_code(
2512
+ RunCodeRequest(code=code, language="python", run_timeout=timeout),
2513
+ max_attempts=1,
2514
+ client_timeout=timeout,
2515
+ endpoint=endpoint,
2516
+ )
2517
+ parts: List[str] = []
2518
+ if result.run_result.stdout:
2519
+ parts.append(f"stdout:\n{result.run_result.stdout}")
2520
+ if result.run_result.stderr:
2521
+ parts.append(f"stderr:\n{result.run_result.stderr}")
2522
+ if result.run_result.execution_time >= timeout - 1:
2523
+ parts.append("[PythonInterpreter Error] TimeoutError: Execution timed out.")
2524
+ return "\n".join(parts) if parts else "Finished execution."
2525
+ except Exception as exc:
2526
+ last_error = f"[Python Interpreter Error]: {type(exc).__name__}: {exc} (endpoint: {endpoint})"
2527
+ continue
2528
+ return last_error or "[Python Interpreter Error]: All attempts failed."
2529
+
2530
+
2531
  def _build_client_for_model(model: str) -> Tuple[InferenceClient, str, List[str]]:
2532
  """Returns (client, primary_model_id, fallback_model_ids).
2533
 
 
2947
  f"πŸ“š turn {turn}: scholar {ok_count}/{len(per_q)} ok"
2948
  )
2949
  yield _emit()
2950
+ elif tool_name == "python_interpreter":
2951
+ code = str(tool_args.get("code", "")).strip()
2952
+ code_preview = (code[:60] + "…") if len(code) > 60 else code
2953
+ status_lines.append(f"🐍 turn {turn}: running Python β€” `{code_preview}`")
2954
+ yield _emit()
2955
+ py_result = _call_python_interpreter(code)
2956
+ tool_response = {"ok": not py_result.startswith("[Python Interpreter Error]"), "result": py_result}
2957
+ status_lines.append(
2958
+ f"{'βœ…' if tool_response['ok'] else '⚠️'} turn {turn}: python finished"
2959
+ )
2960
+ yield _emit()
2961
  else:
2962
  tool_response = {"ok": False, "error": f"Unknown tool: {tool_name}"}
2963
  status_lines.append(f"⚠️ turn {turn}: unknown tool `{tool_name}`")
requirements.txt CHANGED
@@ -5,3 +5,5 @@ requests==2.32.3
5
  beautifulsoup4==4.12.3
6
  openai>=1.40.0
7
  tiktoken>=0.7.0
 
 
 
5
  beautifulsoup4==4.12.3
6
  openai>=1.40.0
7
  tiktoken>=0.7.0
8
+ sandbox-fusion>=0.1.0
9
+ json5>=0.9.0