Spaces:
Running
Running
feat: integrate PythonInterpreter via SandboxFusion
Browse files- app.py +98 -4
- 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 |
-
#
|
| 46 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|