| import json |
| import os |
| import re |
| import time |
| from dataclasses import dataclass, field |
| from datetime import date |
| from typing import Any, Dict, List, Optional, Set, Tuple, Union |
|
|
| import gradio as gr |
| import requests |
| from bs4 import BeautifulSoup |
| from duckduckgo_search import DDGS |
| from huggingface_hub import InferenceClient |
|
|
|
|
| |
| |
| |
| |
| QUEST_MODEL_ID = "osunlp/QUEST-35B" |
| QUEST_BASE_URL = os.getenv("QUEST_BASE_URL", "").strip() |
| |
| |
| |
| QUEST_ENDPOINT_MODEL = os.getenv("QUEST_ENDPOINT_MODEL", "tgi").strip() or "tgi" |
|
|
| |
| |
| |
| DEFAULT_MODEL = QUEST_MODEL_ID |
|
|
| |
| DEFAULT_MAX_SEARCH_RESULTS = 10 |
|
|
| PAPER_URL = os.getenv("PAPER_URL", "https://osu-nlp-group.github.io/quest-gh-test/") |
| CODE_URL = os.getenv("CODE_URL", "https://github.com/OSU-NLP-Group/QUEST") |
| DATASET_URL = os.getenv("DATASET_URL", "https://huggingface.co/collections/osunlp/quest") |
| MODEL_URL = os.getenv("MODEL_URL", "https://huggingface.co/osunlp/QUEST-35B-RL") |
|
|
|
|
| |
| |
| |
| |
| 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. |
| |
| # Tools |
| |
| You may call one or more functions to assist with the user query. |
| |
| You are provided with function signatures within <tools></tools> XML tags: |
| <tools> |
| {"type": "function", "function": {"name": "search", "description": "Perform Google web searches then returns a string of the top search results. Accepts multiple queries.", "parameters": {"type": "object", "properties": {"query": {"type": "array", "items": {"type": "string", "description": "The search query."}, "minItems": 1, "description": "The list of search queries."}}, "required": ["query"]}}} |
| {"type": "function", "function": {"name": "visit", "description": "Visit webpage(s) and return the summary of the content.", "parameters": {"type": "object", "properties": {"url": {"type": "array", "items": {"type": "string"}, "description": "The URL(s) of the webpage(s) to visit. Can be a single URL or an array of URLs."}, "goal": {"type": "string", "description": "The specific information goal for visiting webpage(s)."}}, "required": ["url", "goal"]}}} |
| </tools> |
| |
| # Using prev_state (Research State Summary) |
| |
| If you see a "RESEARCH STATE SUMMARY (prev_state)" section in the user message, it contains a compressed summary of previous research progress. Use it to avoid repeating searches/visits that have already been executed, use verified information directly in your answer, and follow up on uncertain claims only when needed. |
| |
| For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags: |
| <tool_call> |
| {"name": <function-name>, "arguments": <args-json-object>} |
| </tool_call> |
| |
| Current date: """ |
|
|
|
|
| def build_system_prompt() -> str: |
| return QUEST_SYSTEM_PROMPT + date.today().isoformat() |
|
|
|
|
| TOOL_RESPONSE_TEMPLATE = """<tool_response> |
| {payload} |
| </tool_response>""" |
|
|
| SEARCH_CACHE: Dict[str, Dict[str, Any]] = {} |
| VISIT_CACHE: Dict[str, Dict[str, Any]] = {} |
| |
| |
| |
| APP_THEME = gr.themes.Base( |
| primary_hue=gr.themes.colors.orange, |
| secondary_hue=gr.themes.colors.teal, |
| neutral_hue=gr.themes.colors.slate, |
| font=[ |
| gr.themes.GoogleFont("Manrope"), |
| "ui-sans-serif", |
| "system-ui", |
| "sans-serif", |
| ], |
| font_mono=[ |
| gr.themes.GoogleFont("JetBrains Mono"), |
| "ui-monospace", |
| "monospace", |
| ], |
| ).set( |
| body_background_fill="#F2F4F8", |
| body_text_color="#0D1117", |
| body_text_color_subdued="#64748B", |
| color_accent="#BE5B2B", |
| color_accent_soft="rgba(190,91,43,0.09)", |
| background_fill_primary="#FFFFFF", |
| background_fill_secondary="#EEF1F7", |
| border_color_primary="rgba(10,15,40,0.08)", |
| border_color_accent="#BE5B2B", |
| block_background_fill="#FFFFFF", |
| block_border_width="1px", |
| block_border_color="rgba(10,15,40,0.08)", |
| block_shadow="0 1px 2px rgba(10,15,40,0.05), 0 2px 10px rgba(10,15,40,0.06)", |
| block_radius="16px", |
| block_label_background_fill="transparent", |
| block_label_border_width="0px", |
| block_label_text_color="#64748B", |
| block_label_text_weight="700", |
| block_title_text_color="#0D1117", |
| block_title_text_weight="700", |
| block_title_border_width="0px", |
| panel_background_fill="transparent", |
| panel_border_width="0px", |
| panel_border_color="transparent", |
| input_background_fill="#FFFFFF", |
| input_background_fill_focus="#FFFFFF", |
| input_border_color="rgba(10,15,40,0.12)", |
| input_border_color_focus="#BE5B2B", |
| input_border_width="1px", |
| input_radius="12px", |
| input_shadow="none", |
| input_shadow_focus="0 0 0 3px rgba(190,91,43,0.15)", |
| code_background_fill="#EEF1F7", |
| slider_color="#BE5B2B", |
| button_primary_background_fill="#0D1117", |
| button_primary_background_fill_hover="#1F2A37", |
| button_primary_text_color="#FFFFFF", |
| button_primary_border_color="transparent", |
| button_primary_shadow="0 1px 2px rgba(10,15,40,0.08), 0 6px 18px rgba(10,15,40,0.12)", |
| button_secondary_background_fill="#FFFFFF", |
| button_secondary_background_fill_hover="rgba(190,91,43,0.09)", |
| button_secondary_text_color="#BE5B2B", |
| button_secondary_border_color="rgba(10,15,40,0.16)", |
| button_cancel_background_fill="#FFFFFF", |
| button_cancel_background_fill_hover="#FEE2E2", |
| button_cancel_text_color="#DC2626", |
| button_cancel_border_color="#FCA5A5", |
| table_border_color="rgba(10,15,40,0.08)", |
| table_even_background_fill="#FAFBFD", |
| table_odd_background_fill="#FFFFFF", |
| ) |
|
|
| CUSTOM_CSS = """ |
| /* === Quest paper palette applied to the Gradio shell ==================== */ |
| /* Brings the OSU-NLP Quest microsite aesthetic into the live Space: soft |
| off-white background, paper-white cards with subtle 1px borders and |
| low-opacity shadows, terracotta accent, Source Serif 4 for display |
| headings, Manrope for everything else. */ |
| |
| :root { |
| --q-bg: #F2F4F8; |
| --q-paper: #FFFFFF; |
| --q-surface-alt: #EEF1F7; |
| --q-line: rgba(10, 15, 40, 0.08); |
| --q-line-strong: rgba(10, 15, 40, 0.16); |
| --q-text: #0D1117; |
| --q-muted: #64748B; |
| --q-accent: #BE5B2B; |
| --q-accent-soft: rgba(190, 91, 43, 0.09); |
| --q-accent-line: rgba(190, 91, 43, 0.55); |
| --q-mint: #0B9E8A; |
| --q-mint-deep: #0A8070; |
| --q-cover-bg: #0D1117; |
| --q-shadow: 0 1px 3px rgba(10,15,40,0.04), 0 8px 32px rgba(10,15,40,0.08); |
| --q-shadow-card: 0 1px 2px rgba(10,15,40,0.05), 0 2px 10px rgba(10,15,40,0.06); |
| --q-radius-xl: 20px; |
| --q-radius-lg: 16px; |
| --q-radius-md: 12px; |
| } |
| |
| html, body, gradio-app, [class*="gradio-container"] { |
| background: var(--q-bg) !important; |
| } |
| |
| /* Full-height shell ------------------------------------------------------- */ |
| html, body { width: 100% !important; min-height: 100vh !important; margin: 0 !important; font-size: 17px !important; } |
| gradio-app { |
| display: block !important; |
| width: 100% !important; |
| min-height: 100vh !important; |
| margin-left: auto !important; |
| margin-right: auto !important; |
| } |
| gradio-app > .gradio-container, |
| gradio-app > div { |
| display: block !important; |
| width: 100% !important; |
| margin-left: auto !important; |
| margin-right: auto !important; |
| } |
| |
| [class*="gradio-container"] { |
| max-width: 1700px !important; |
| width: 100% !important; |
| min-width: 320px !important; |
| margin-left: auto !important; |
| margin-right: auto !important; |
| padding: 28px 36px 72px !important; |
| color: var(--q-text); |
| box-sizing: border-box !important; |
| font-family: "Manrope", ui-sans-serif, system-ui, sans-serif; |
| font-size: 1rem !important; |
| } |
| |
| [class*="gradio-container"] *::selection { background: rgba(190,91,43,0.18); } |
| |
| /* Prevent inner wrappers from collapsing when streaming content first arrives. */ |
| [class*="gradio-container"] .layout-gap { width: 100% !important; } |
| [class*="gradio-container"] .layout-gap > .gr-column, |
| [class*="gradio-container"] .layout-gap > div { min-width: 0 !important; } |
| |
| [class*="gradio-container"] .gradio-markdown, |
| [class*="gradio-container"] [data-testid="markdown"] { min-height: 220px !important; } |
| [class*="gradio-container"] .codemirror-wrapper, |
| [class*="gradio-container"] .cm-editor { min-height: 220px !important; } |
| |
| /* Long code / markdown cannot push the layout sideways. */ |
| [class*="gradio-container"] .gradio-code, |
| [class*="gradio-container"] .gradio-markdown, |
| [class*="gradio-container"] .prose, |
| [class*="gradio-container"] .markdown, |
| [class*="gradio-container"] [data-testid="markdown"], |
| [class*="gradio-container"] .tabs, |
| [class*="gradio-container"] .tabitem, |
| [class*="gradio-container"] .tab-content { |
| max-width: 100% !important; |
| width: 100% !important; |
| min-width: 0 !important; |
| word-wrap: break-word !important; |
| overflow-wrap: anywhere !important; |
| } |
| [class*="gradio-container"] .codemirror-wrapper { |
| max-width: 100% !important; |
| border-radius: 14px !important; |
| overflow: hidden !important; |
| } |
| [class*="gradio-container"] .cm-editor { max-width: 100% !important; overflow: hidden !important; } |
| [class*="gradio-container"] .cm-scroller { max-width: 100% !important; overflow-x: auto !important; } |
| [class*="gradio-container"] .cm-content, |
| [class*="gradio-container"] .cm-line { |
| max-width: 100% !important; |
| white-space: pre-wrap !important; |
| word-break: break-word !important; |
| } |
| [class*="gradio-container"] .prose pre, |
| [class*="gradio-container"] .markdown pre { |
| max-width: 100% !important; |
| overflow-x: auto !important; |
| white-space: pre-wrap !important; |
| } |
| |
| /* === Quest-style header ================================================= */ |
| .quest-header { |
| display: flex; |
| align-items: center; |
| gap: 18px; |
| padding: 18px 22px; |
| margin: 8px 0 24px; |
| border: 1px solid var(--q-line); |
| border-radius: var(--q-radius-lg); |
| background: var(--q-paper); |
| box-shadow: var(--q-shadow-card); |
| } |
| .quest-header-mark { |
| display: grid; |
| place-items: center; |
| width: 48px; |
| height: 48px; |
| flex-shrink: 0; |
| border-radius: 12px; |
| background: var(--q-text); |
| color: #FFFFFF; |
| font-family: "Source Serif 4", "Source Serif Pro", ui-serif, Georgia, serif; |
| font-weight: 700; |
| font-size: 1.55rem; |
| } |
| .quest-header-text { |
| display: flex; |
| flex-direction: column; |
| gap: 4px; |
| min-width: 0; |
| } |
| .quest-header-title { |
| margin: 0; |
| font-family: "Source Serif 4", "Source Serif Pro", ui-serif, Georgia, serif; |
| font-weight: 600; |
| font-size: clamp(1.25rem, 2vw, 1.75rem); |
| line-height: 1.2; |
| letter-spacing: -0.01em; |
| color: var(--q-text); |
| } |
| .quest-header-byline { |
| color: var(--q-muted); |
| font-size: 0.9rem; |
| font-weight: 500; |
| text-decoration: underline; |
| text-decoration-color: rgba(100,116,139,0.45); |
| text-underline-offset: 3px; |
| text-decoration-thickness: 1px; |
| width: fit-content; |
| transition: color 140ms ease, text-decoration-color 140ms ease; |
| } |
| .quest-header-byline:hover { |
| color: var(--q-accent); |
| text-decoration-color: var(--q-accent); |
| } |
| |
| /* === Cards (section-card) =============================================== */ |
| .section-card { |
| background: var(--q-paper) !important; |
| border: 1px solid var(--q-line) !important; |
| border-radius: var(--q-radius-xl) !important; |
| box-shadow: var(--q-shadow-card) !important; |
| padding: 22px !important; |
| } |
| .no-frame { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| } |
| |
| /* Section kicker + hero heading follow the paper treatment. */ |
| .section-heading { |
| font-size: 0.7rem; |
| font-weight: 800; |
| letter-spacing: 0.14em; |
| text-transform: uppercase; |
| color: var(--q-accent); |
| margin: 0 0 14px 0; |
| } |
| .hero-heading { |
| font-family: "Source Serif 4", "Source Serif Pro", ui-serif, Georgia, serif !important; |
| font-weight: 600 !important; |
| font-size: 1.6rem !important; |
| letter-spacing: -0.01em !important; |
| text-transform: none !important; |
| color: var(--q-text) !important; |
| } |
| /* Match the .brand mark from the Quest microsite (github-page branch). */ |
| .quest-name { |
| font-family: "Source Serif 4", "Source Serif Pro", ui-serif, Georgia, serif !important; |
| font-style: italic !important; |
| font-weight: 700 !important; |
| color: inherit !important; |
| letter-spacing: -0.005em; |
| margin: 4px 0 14px 0 !important; |
| } |
| .hero-subtitle { |
| color: var(--q-muted); |
| font-size: 0.95rem; |
| line-height: 1.6; |
| margin: -6px 0 16px 0; |
| } |
| |
| /* Layout gap: mirror the paper's column rhythm. */ |
| .layout-gap { gap: 24px !important; align-items: flex-start; } |
| .right-stack > * { margin-bottom: 14px; } |
| .action-row { gap: 10px !important; margin-top: 14px; } |
| .action-row button { min-width: 0; flex: 1; } |
| |
| /* === Icon grid (Paper / Code / Dataset / Model) ========================= */ |
| .icon-grid { |
| display: grid; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| gap: 10px; |
| width: 100%; |
| } |
| .icon-link { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 8px; |
| padding: 11px 14px; |
| border-radius: 999px; |
| text-decoration: none !important; |
| color: var(--q-text) !important; |
| background: var(--q-paper); |
| font-weight: 600; |
| font-size: 0.88rem; |
| white-space: nowrap; |
| border: 1px solid var(--q-line-strong); |
| transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease; |
| } |
| .icon-link:hover { |
| background: var(--q-accent-soft); |
| border-color: var(--q-accent-line); |
| color: var(--q-accent) !important; |
| transform: translateY(-1px); |
| } |
| |
| /* Resource cards (paper / code / data / model) β icon + label, eye-catching */ |
| .resource-grid { |
| display: grid; |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| gap: 10px; |
| width: 100%; |
| } |
| .resource-card { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| padding: 12px 14px; |
| border-radius: 14px; |
| text-decoration: none !important; |
| color: var(--q-text) !important; |
| background: var(--q-paper); |
| border: 1px solid var(--q-line-strong); |
| transition: background 140ms ease, border-color 140ms ease, color 140ms ease, transform 140ms ease; |
| } |
| .resource-card:hover { |
| background: var(--q-accent-soft); |
| border-color: var(--q-accent-line); |
| color: var(--q-accent) !important; |
| transform: translateY(-1px); |
| } |
| .resource-card-icon { |
| display: inline-flex; |
| align-items: center; |
| justify-content: center; |
| width: 30px; |
| height: 30px; |
| flex-shrink: 0; |
| border-radius: 8px; |
| background: var(--q-surface-alt); |
| color: var(--q-text); |
| } |
| .resource-card-icon svg { |
| width: 18px; |
| height: 18px; |
| fill: currentColor; |
| } |
| .resource-card-icon.resource-card-emoji { |
| background: transparent; |
| font-size: 22px; |
| line-height: 1; |
| } |
| .resource-card-text { |
| display: flex; |
| flex-direction: column; |
| line-height: 1.15; |
| min-width: 0; |
| } |
| .resource-card-text strong { |
| font-weight: 700; |
| font-size: 0.92rem; |
| } |
| .resource-card-text small { |
| font-size: 0.72rem; |
| color: var(--q-muted); |
| margin-top: 2px; |
| } |
| |
| /* === Buttons ============================================================ */ |
| [class*="gradio-container"] button.primary, |
| [class*="gradio-container"] .gr-button-primary { |
| background: var(--q-text) !important; |
| color: #ffffff !important; |
| border: 1px solid var(--q-text) !important; |
| box-shadow: 0 1px 2px rgba(10,15,40,0.08), 0 6px 18px rgba(10,15,40,0.12) !important; |
| font-weight: 700 !important; |
| letter-spacing: 0.01em !important; |
| } |
| [class*="gradio-container"] button.primary:hover, |
| [class*="gradio-container"] .gr-button-primary:hover { |
| background: #1F2A37 !important; |
| border-color: #1F2A37 !important; |
| } |
| [class*="gradio-container"] button.secondary, |
| [class*="gradio-container"] .gr-button-secondary { |
| background: var(--q-paper) !important; |
| color: var(--q-text) !important; |
| border: 1px solid var(--q-line-strong) !important; |
| box-shadow: none !important; |
| font-weight: 600 !important; |
| } |
| [class*="gradio-container"] button.secondary:hover, |
| [class*="gradio-container"] .gr-button-secondary:hover { |
| background: var(--q-accent-soft) !important; |
| border-color: var(--q-accent-line) !important; |
| color: var(--q-accent) !important; |
| } |
| [class*="gradio-container"] button.stop, |
| [class*="gradio-container"] .gr-button-stop { |
| background: var(--q-paper) !important; |
| color: #DC2626 !important; |
| border: 1px solid #FCA5A5 !important; |
| box-shadow: none !important; |
| font-weight: 600 !important; |
| } |
| [class*="gradio-container"] button.stop:hover, |
| [class*="gradio-container"] .gr-button-stop:hover { |
| background: #FEE2E2 !important; |
| color: #B91C1C !important; |
| } |
| |
| /* Flatten every grey block Gradio drops inside our cards. */ |
| [class*="gradio-container"] .gr-group, |
| [class*="gradio-container"] fieldset, |
| [class*="gradio-container"] .gr-box, |
| [class*="gradio-container"] .gr-panel, |
| [class*="gradio-container"] .form, |
| [class*="gradio-container"] .gr-form, |
| [class*="gradio-container"] .container { |
| background: transparent !important; |
| } |
| .section-card { |
| --block-shadow: none; |
| --block-shadow-dark: none; |
| --block-background-fill: transparent; |
| --block-border-color: transparent; |
| --block-border-width: 0px; |
| --panel-background-fill: transparent; |
| --panel-border-width: 0px; |
| --background-fill-secondary: transparent; |
| --border-color-primary: transparent; |
| overflow: visible !important; |
| } |
| .section-card > div, |
| .section-card > div > div, |
| .section-card > div > div > div { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| overflow: visible !important; |
| } |
| .section-card .block, |
| .section-card .form, |
| .section-card .gr-form, |
| .section-card .gr-block, |
| .section-card .gr-panel, |
| .section-card .gr-group, |
| .section-card .gradio-dropdown, |
| .section-card .gradio-slider, |
| .section-card .gradio-textbox, |
| .section-card .gradio-markdown, |
| .section-card .gradio-code { |
| background: transparent !important; |
| border: none !important; |
| box-shadow: none !important; |
| overflow: visible !important; |
| } |
| .section-card .form, |
| .section-card .gr-form { |
| display: flex !important; |
| flex-direction: column !important; |
| gap: 14px !important; |
| } |
| [class*="gradio-container"] .section-card .row, |
| [class*="gradio-container"] .section-card [class*="row"] { |
| display: flex !important; |
| flex-direction: row !important; |
| flex-wrap: wrap !important; |
| gap: 10px !important; |
| } |
| .action-row { |
| display: flex !important; |
| flex-direction: row !important; |
| gap: 10px !important; |
| margin-top: 14px; |
| } |
| .action-row > * { flex: 1 1 0; min-width: 0; } |
| .section-card > * + * { margin-top: 14px; } |
| |
| /* === Inputs ============================================================= */ |
| [class*="gradio-container"] textarea, |
| [class*="gradio-container"] input:not([type="checkbox"]):not([type="radio"]):not([type="range"]) { |
| background: var(--q-paper) !important; |
| border: 1px solid var(--q-line-strong) !important; |
| box-shadow: none !important; |
| border-radius: var(--q-radius-md) !important; |
| color: var(--q-text) !important; |
| font-family: "Manrope", ui-sans-serif, system-ui, sans-serif !important; |
| } |
| /* Make the Model Textbox match the Memory Strategy Dropdown's height (46px outer = 44px content + 2*1px border). */ |
| .section-card [data-testid="textbox"] textarea, |
| .section-card [data-testid="textbox"] input { |
| min-height: 44px !important; |
| padding: 11px 14px !important; |
| line-height: 1.4 !important; |
| box-sizing: border-box !important; |
| } |
| [class*="gradio-container"] textarea::placeholder, |
| [class*="gradio-container"] input::placeholder { color: #94A3B8 !important; } |
| [class*="gradio-container"] textarea:focus, |
| [class*="gradio-container"] input:focus { |
| border-color: var(--q-accent) !important; |
| box-shadow: 0 0 0 3px rgba(190,91,43,0.15) !important; |
| outline: none !important; |
| } |
| |
| /* === Dropdown =========================================================== */ |
| [class*="gradio-container"] [data-testid="dropdown"], |
| [class*="gradio-container"] .gradio-dropdown { |
| background: var(--q-paper) !important; |
| border: 1px solid var(--q-line-strong) !important; |
| border-radius: var(--q-radius-md) !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| min-height: 46px !important; |
| width: 100% !important; |
| box-sizing: border-box !important; |
| } |
| [class*="gradio-container"] [data-testid="dropdown"] > .wrap, |
| [class*="gradio-container"] [data-testid="dropdown"] .secondary-wrap, |
| [class*="gradio-container"] [data-testid="dropdown"] .wrap-inner, |
| [class*="gradio-container"] [data-testid="dropdown"] .input-container, |
| [class*="gradio-container"] [data-testid="dropdown"] .single-select, |
| [class*="gradio-container"] .gradio-dropdown .wrap, |
| [class*="gradio-container"] .gradio-dropdown .wrap-inner, |
| [class*="gradio-container"] .gradio-dropdown .secondary-wrap, |
| [class*="gradio-container"] .gradio-dropdown .input-container, |
| [class*="gradio-container"] .gradio-dropdown .single-select, |
| [class*="gradio-container"] [class*="dropdown"] .wrap { |
| background: transparent !important; |
| border: 0 !important; |
| outline: 0 !important; |
| box-shadow: none !important; |
| border-radius: 0 !important; |
| width: 100% !important; |
| min-height: 44px !important; |
| padding: 0 14px !important; |
| display: flex !important; |
| align-items: center !important; |
| box-sizing: border-box !important; |
| } |
| [class*="gradio-container"] [data-testid="dropdown"] input, |
| [class*="gradio-container"] .gradio-dropdown input, |
| [class*="gradio-container"] [data-testid="dropdown"] select, |
| [class*="gradio-container"] .gradio-dropdown select { |
| background: transparent !important; |
| border: 0 !important; |
| outline: 0 !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| height: 44px !important; |
| line-height: 44px !important; |
| font-size: 0.95rem !important; |
| width: 100% !important; |
| border-radius: 0 !important; |
| } |
| /* Force-remove any nested pill/rounded background that makes the dropdown |
| look like it has two concentric frames. */ |
| [class*="gradio-container"] [data-testid="dropdown"] .container, |
| [class*="gradio-container"] [data-testid="dropdown"] .wrap > .wrap, |
| [class*="gradio-container"] .gradio-dropdown .container, |
| [class*="gradio-container"] .gradio-dropdown .wrap > .wrap { |
| border: 0 !important; |
| outline: 0 !important; |
| box-shadow: none !important; |
| background: transparent !important; |
| border-radius: 0 !important; |
| padding: 0 !important; |
| } |
| /* The little caret/arrow icon container β vertically center it */ |
| [class*="gradio-container"] [data-testid="dropdown"] .icon-wrap, |
| [class*="gradio-container"] .gradio-dropdown .icon-wrap { |
| top: 50% !important; |
| transform: translateY(-50%) !important; |
| right: 14px !important; |
| } |
| [class*="gradio-container"] .options ul, |
| [class*="gradio-container"] .options { |
| background: var(--q-paper) !important; |
| border: 1px solid var(--q-line) !important; |
| border-radius: var(--q-radius-md) !important; |
| box-shadow: 0 10px 30px rgba(10,15,40,0.12) !important; |
| } |
| [class*="gradio-container"] .options li[aria-selected="true"], |
| [class*="gradio-container"] .options li:hover { |
| background: var(--q-accent-soft) !important; |
| color: var(--q-accent) !important; |
| } |
| |
| /* Info hint text under inputs */ |
| [class*="gradio-container"] .info, |
| [class*="gradio-container"] [data-testid*="info"], |
| [class*="gradio-container"] .gr-info { |
| color: var(--q-muted) !important; |
| background: transparent !important; |
| font-size: 12px !important; |
| } |
| |
| /* === Sliders ============================================================ */ |
| /* Flatten the Slider's outer wrapper β Gradio paints a rectangular block |
| around the label + track + value-input by default; remove it. */ |
| .section-card .gradio-slider, |
| .section-card .gradio-slider > div, |
| .section-card .gradio-slider .form, |
| .section-card .gradio-slider .gr-form, |
| .section-card .gradio-slider .wrap, |
| .section-card .gradio-slider .container, |
| .section-card .gradio-slider .head { |
| background: transparent !important; |
| border: 0 !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| } |
| |
| /* === Per-component flatteners (id-based; max specificity vs Gradio defaults) === */ |
| /* The Memory Strategy dropdown and the two sliders ship with an outer block |
| wrapper that paints a small rectangle. Flatten the wrapper AND any nested |
| div Gradio inserts (form/container/wrap/etc), keeping label + interactive |
| element visible. */ |
| #quest-memory-strategy, |
| #quest-memory-strategy > div, |
| #quest-memory-strategy .form, |
| #quest-memory-strategy .gr-form, |
| #quest-memory-strategy .container, |
| #quest-memory-strategy .wrap-inner, |
| #quest-memory-strategy .head, |
| #quest-max-turns, |
| #quest-max-turns > div, |
| #quest-max-turns .form, |
| #quest-max-turns .gr-form, |
| #quest-max-turns .container, |
| #quest-max-turns .wrap-inner, |
| #quest-max-turns .head, |
| #quest-temperature, |
| #quest-temperature > div, |
| #quest-temperature .form, |
| #quest-temperature .gr-form, |
| #quest-temperature .container, |
| #quest-temperature .wrap-inner, |
| #quest-temperature .head, |
| #quest-model, |
| #quest-model > div, |
| #quest-model .form, |
| #quest-model .gr-form, |
| #quest-model .container, |
| #quest-model .wrap-inner, |
| #quest-model .head { |
| background: transparent !important; |
| border: 0 !important; |
| outline: 0 !important; |
| box-shadow: none !important; |
| padding: 0 !important; |
| border-radius: 0 !important; |
| } |
| /* Memory Strategy radio: stack vertically, terracotta-tinted check state. */ |
| #quest-memory-strategy .wrap, |
| #quest-memory-strategy fieldset, |
| #quest-memory-strategy [data-testid="radio"] { |
| display: flex !important; |
| flex-direction: column !important; |
| gap: 6px !important; |
| background: transparent !important; |
| border: 0 !important; |
| padding: 0 !important; |
| } |
| #quest-memory-strategy label { |
| background: transparent !important; |
| border: 1px solid var(--q-line) !important; |
| border-radius: 8px !important; |
| padding: 8px 12px !important; |
| cursor: pointer !important; |
| font-weight: 500 !important; |
| font-size: 0.95rem !important; |
| color: var(--q-text) !important; |
| text-transform: none !important; |
| letter-spacing: 0 !important; |
| display: flex !important; |
| align-items: center !important; |
| gap: 10px !important; |
| transition: border-color 120ms ease, background 120ms ease; |
| } |
| #quest-memory-strategy label:hover { |
| border-color: var(--q-line-strong) !important; |
| } |
| #quest-memory-strategy input[type="radio"] { |
| accent-color: var(--q-accent) !important; |
| width: 16px !important; |
| height: 16px !important; |
| } |
| |
| /* Slider head input (the "[6 βΊ]" / "[1 βΊ]" pill next to the slider track): |
| the global input rule paints a 1px border on it, which looks like a stray |
| rectangle. Flatten it AND hide the reset button (it's redundant β the |
| slider's range already shows the default value). */ |
| #quest-max-turns input[type="number"], |
| #quest-temperature input[type="number"] { |
| border: 0 !important; |
| background: transparent !important; |
| box-shadow: none !important; |
| border-radius: 0 !important; |
| padding: 0 !important; |
| min-height: 0 !important; |
| height: auto !important; |
| text-align: center !important; |
| width: 3.5em !important; |
| font-weight: 600 !important; |
| color: var(--q-text) !important; |
| } |
| #quest-max-turns button, |
| #quest-temperature button { |
| display: none !important; |
| } |
| [class*="gradio-container"] input[type="range"] { |
| -webkit-appearance: none; |
| appearance: none; |
| width: 100%; |
| height: 6px; |
| background: var(--q-surface-alt); |
| border-radius: 999px; |
| outline: none; |
| box-shadow: none !important; |
| border: none !important; |
| } |
| [class*="gradio-container"] input[type="range"]::-webkit-slider-runnable-track { |
| height: 6px; |
| background: linear-gradient(90deg,var(--q-accent) var(--val,50%), var(--q-surface-alt) var(--val,50%)); |
| border-radius: 999px; |
| } |
| [class*="gradio-container"] input[type="range"]::-webkit-slider-thumb { |
| -webkit-appearance: none; |
| appearance: none; |
| width: 18px; |
| height: 18px; |
| border-radius: 50%; |
| background: #ffffff; |
| border: 2px solid var(--q-accent); |
| box-shadow: 0 2px 6px rgba(190,91,43,0.25); |
| margin-top: -6px; |
| cursor: pointer; |
| } |
| [class*="gradio-container"] input[type="range"]::-moz-range-track { |
| height: 6px; |
| background: var(--q-surface-alt); |
| border-radius: 999px; |
| } |
| [class*="gradio-container"] input[type="range"]::-moz-range-progress { |
| height: 6px; |
| background: var(--q-accent); |
| border-radius: 999px; |
| } |
| [class*="gradio-container"] input[type="range"]::-moz-range-thumb { |
| width: 16px; |
| height: 16px; |
| border-radius: 50%; |
| background: #ffffff; |
| border: 2px solid var(--q-accent); |
| box-shadow: 0 2px 6px rgba(190,91,43,0.25); |
| } |
| |
| /* === Tabs =============================================================== */ |
| [class*="gradio-container"] .tabs, |
| [class*="gradio-container"] .tab-container, |
| [class*="gradio-container"] .tab-wrapper { background: transparent !important; } |
| [class*="gradio-container"] .tab-container::after { background: var(--q-line) !important; } |
| [class*="gradio-container"] .tab-wrapper button { |
| color: var(--q-muted) !important; |
| font-weight: 700 !important; |
| letter-spacing: 0.04em !important; |
| text-transform: uppercase !important; |
| font-size: 0.78rem !important; |
| } |
| [class*="gradio-container"] .tab-wrapper button.selected { color: var(--q-accent) !important; } |
| [class*="gradio-container"] .tab-wrapper button.selected::after { background: var(--q-accent) !important; } |
| /* Hide the orange streaming-progress bar that Gradio paints at the top of |
| the Markdown/Code panel while a run is in flight. */ |
| [class*="gradio-container"] .progress, |
| [class*="gradio-container"] .progress-level, |
| [class*="gradio-container"] .progress-level-inner, |
| [class*="gradio-container"] .progress-bar, |
| [class*="gradio-container"] .progress-text, |
| [class*="gradio-container"] [class*="progress-level"], |
| [class*="gradio-container"] .generating, |
| [class*="gradio-container"] div[class*="progress-bar"] { |
| display: none !important; |
| background: transparent !important; |
| border: 0 !important; |
| height: 0 !important; |
| } |
| /* Kill any stray orange/thick separator that Gradio paints above the tab |
| panel content (border-top or ::before on the tab content wrapper). */ |
| [class*="gradio-container"] .tabitem, |
| [class*="gradio-container"] .tab-content, |
| [class*="gradio-container"] .gradio-tabitem, |
| [class*="gradio-container"] .tabs > div.tabitem { |
| border-top: 0 !important; |
| box-shadow: none !important; |
| background: transparent !important; |
| } |
| [class*="gradio-container"] .tabitem::before, |
| [class*="gradio-container"] .tab-content::before, |
| [class*="gradio-container"] .gradio-tabitem::before { content: none !important; } |
| [class*="gradio-container"] .tab-nav, |
| [class*="gradio-container"] .tab-wrapper { |
| border-bottom: 1px solid var(--q-line) !important; |
| border-top: 0 !important; |
| } |
| [class*="gradio-container"] .tab-nav::before, |
| [class*="gradio-container"] .tab-wrapper::before { content: none !important; } |
| |
| /* Block labels above components */ |
| [class*="gradio-container"] .gr-block label, |
| [class*="gradio-container"] .gradio-slider label, |
| [class*="gradio-container"] .gradio-dropdown label, |
| [class*="gradio-container"] .gradio-textbox label { |
| color: var(--q-muted) !important; |
| font-weight: 700 !important; |
| font-size: 0.74rem !important; |
| letter-spacing: 0.08em !important; |
| text-transform: uppercase !important; |
| } |
| |
| /* === Markdown / prose =================================================== */ |
| [class*="gradio-container"] .gr-markdown, |
| [class*="gradio-container"] .prose, |
| [class*="gradio-container"] .markdown { |
| color: var(--q-text) !important; |
| font-family: "Manrope", ui-sans-serif, system-ui, sans-serif !important; |
| line-height: 1.75; |
| } |
| [class*="gradio-container"] .gr-markdown a, |
| [class*="gradio-container"] .prose a { color: var(--q-accent) !important; text-decoration: underline; text-decoration-color: rgba(190,91,43,0.35); } |
| [class*="gradio-container"] .gr-markdown a:hover, |
| [class*="gradio-container"] .prose a:hover { text-decoration-color: var(--q-accent); } |
| [class*="gradio-container"] .gr-markdown h1, |
| [class*="gradio-container"] .gr-markdown h2, |
| [class*="gradio-container"] .gr-markdown h3, |
| [class*="gradio-container"] .prose h1, |
| [class*="gradio-container"] .prose h2, |
| [class*="gradio-container"] .prose h3 { |
| font-family: "Source Serif 4", "Source Serif Pro", ui-serif, Georgia, serif !important; |
| font-weight: 600 !important; |
| letter-spacing: -0.01em !important; |
| color: var(--q-text) !important; |
| } |
| [class*="gradio-container"] .gr-markdown code, |
| [class*="gradio-container"] .prose code { |
| background: var(--q-surface-alt); |
| border: 1px solid var(--q-line); |
| padding: 1px 6px; |
| border-radius: 6px; |
| font-size: 0.9em; |
| } |
| |
| /* === Code block (Record tab) ============================================ */ |
| [class*="gradio-container"] .codemirror-wrapper, |
| [class*="gradio-container"] .cm-editor, |
| [class*="gradio-container"] .cm-scroller, |
| [class*="gradio-container"] .cm-gutters, |
| [class*="gradio-container"] .cm-content { |
| background: var(--q-surface-alt) !important; |
| color: var(--q-text) !important; |
| border: none !important; |
| font-family: "JetBrains Mono", ui-monospace, monospace !important; |
| } |
| [class*="gradio-container"] .cm-gutters { |
| border-right: 1px solid var(--q-line) !important; |
| color: var(--q-muted) !important; |
| } |
| |
| /* === Rounded corners on everything ====================================== */ |
| [class*="gradio-container"] .block, |
| [class*="gradio-container"] .form, |
| [class*="gradio-container"] .gr-box, |
| [class*="gradio-container"] .gr-panel, |
| [class*="gradio-container"] .gr-group, |
| [class*="gradio-container"] [data-testid="textbox"], |
| [class*="gradio-container"] [data-testid="dropdown"], |
| [class*="gradio-container"] .tabitem, |
| [class*="gradio-container"] .tab-content, |
| [class*="gradio-container"] .gradio-markdown, |
| [class*="gradio-container"] .gradio-code { border-radius: var(--q-radius-md) !important; } |
| [class*="gradio-container"] button { border-radius: 999px !important; } |
| |
| /* === Example buttons ==================================================== */ |
| .example-note { color: var(--q-muted); font-size: 13px; margin: 0 0 12px 0; line-height: 1.5; } |
| .memory-help { |
| color: var(--q-muted); |
| font-size: 12.5px; |
| line-height: 1.55; |
| margin: 6px 0 0 0; |
| padding: 10px 12px; |
| background: var(--q-surface-alt); |
| border: 1px solid var(--q-line); |
| border-radius: 8px; |
| } |
| .memory-help b { color: var(--q-text); font-weight: 600; } |
| .example-buttons { display: grid; gap: 10px; margin-top: 4px; } |
| |
| [class*="gradio-container"] .example-btn { |
| text-align: left !important; |
| justify-content: flex-start !important; |
| white-space: normal !important; |
| line-height: 1.5 !important; |
| padding: 14px 16px !important; |
| font-size: 14px !important; |
| color: var(--q-text) !important; |
| background: var(--q-paper) !important; |
| border: 1px solid var(--q-line) !important; |
| border-radius: var(--q-radius-md) !important; |
| box-shadow: none !important; |
| font-weight: 500 !important; |
| letter-spacing: normal !important; |
| text-transform: none !important; |
| } |
| [class*="gradio-container"] .example-btn:hover { |
| background: var(--q-accent-soft) !important; |
| border-color: var(--q-accent-line) !important; |
| color: var(--q-accent) !important; |
| } |
| [class*="gradio-container"] .example-btn > * { |
| color: inherit !important; |
| white-space: normal !important; |
| display: inline !important; |
| } |
| |
| /* Footer tagline block */ |
| .quest-footer { |
| margin-top: 28px; |
| padding: 18px 24px; |
| border: 1px solid var(--q-line); |
| border-radius: var(--q-radius-xl); |
| background: var(--q-paper); |
| box-shadow: var(--q-shadow-card); |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| gap: 20px; |
| color: var(--q-muted); |
| font-size: 0.86rem; |
| line-height: 1.65; |
| } |
| .quest-footer a { color: var(--q-muted); text-decoration: none; } |
| .quest-footer a:hover { color: var(--q-text); } |
| .quest-footer-links { display: flex; gap: 16px; flex-wrap: wrap; } |
| |
| /* Tiny mark that replaces the HF watermark block. */ |
| footer { display: none !important; } |
| |
| /* === Responsive ========================================================= */ |
| @media (max-width: 1100px) { |
| .quest-cover-inner { grid-template-columns: 1fr; } |
| .quest-cover-panel.wide { grid-column: auto; min-height: 180px; } |
| } |
| @media (max-width: 760px) { |
| [class*="gradio-container"] { padding: 16px !important; } |
| .quest-footer { flex-direction: column; align-items: flex-start; } |
| } |
| """ |
|
|
|
|
| @dataclass |
| class AgentState: |
| searched_queries: List[str] = field(default_factory=list) |
| visited_urls: List[str] = field(default_factory=list) |
| searched_query_set: Set[str] = field(default_factory=set) |
| visited_url_set: Set[str] = field(default_factory=set) |
| trusted_notes: List[str] = field(default_factory=list) |
| trace: List[Dict[str, Any]] = field(default_factory=list) |
|
|
|
|
| |
| |
| |
| |
| _PLACEHOLDER_ANSWER_RE = re.compile(r"^[\s.\u2026\u00b7]*$") |
|
|
| |
| |
| _TABLE_SEPARATOR_RE = re.compile( |
| r"^\s*\|?\s*:?-{2,}:?(?:\s*\|\s*:?-{2,}:?)+\s*\|?\s*$" |
| ) |
|
|
|
|
| def strip_think_blocks(text: str) -> str: |
| """Remove any <think>...</think> reasoning blocks. |
| |
| QUEST-35B (Qwen3 family) emits `<think>` reasoning before the final |
| answer. When the endpoint is deployed without a reasoning parser, the raw |
| tags leak into chat completion `content`; stripping them here keeps the |
| extracted answer clean for Markdown rendering. |
| """ |
| return re.sub( |
| r"<think>.*?</think>", "", text, flags=re.DOTALL | re.IGNORECASE |
| ) |
|
|
|
|
| def decode_escaped_whitespace(text: str) -> str: |
| """Decode literal `\\n`/`\\t`/`\\r` sequences back to real whitespace. |
| |
| Some OpenAI-compatible servers (and some vLLM builds when a tokenizer's |
| chat template escapes control characters) return `choices[0].message.content` |
| with newlines stored as the two-character backslash+n sequence rather than |
| as a real newline. That breaks Markdown rendering because a pipe table on |
| a single line is not a table β it is just a sentence with `|` in it, which |
| is exactly the symptom we saw with: |
| |
| \\n| Color | Hex |\\n|---|---|\\n| Red | #FF0000 |... |
| |
| We only decode when the escapes dominate (at least 3 of them, and at |
| least as many as the real newlines in the text). That keeps us from |
| corrupting legitimate backslash-n pairs that happen to appear in a code |
| sample the model produced. |
| """ |
| if not text: |
| return text |
| escaped_newlines = text.count("\\n") |
| if escaped_newlines == 0 and "\\t" not in text and "\\r" not in text: |
| return text |
| real_newlines = text.count("\n") |
| if escaped_newlines < max(3, real_newlines + 1): |
| return text |
| |
| |
| sentinel = "\x00__BS__\x00" |
| out = text.replace("\\\\", sentinel) |
| out = out.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", "\t") |
| out = out.replace(sentinel, "\\") |
| return out |
|
|
|
|
| def _is_placeholder_answer(text: str) -> bool: |
| return bool(_PLACEHOLDER_ANSWER_RE.match(text or "")) |
|
|
|
|
| def ensure_markdown_table_blank_lines(text: str) -> str: |
| """Insert a blank line before any pipe-table header row. |
| |
| GitHub-Flavored Markdown requires a pipe table to be preceded by a |
| paragraph break; otherwise the header row is folded into the previous |
| paragraph and the whole table renders as raw text. Models sometimes glue |
| the table directly under a sentence (e.g. "Here's the comparison: | Col |
| ..."), so we fix that up defensively. |
| """ |
| lines = text.split("\n") |
| out: List[str] = [] |
| for idx, line in enumerate(lines): |
| is_header = ( |
| "|" in line |
| and idx + 1 < len(lines) |
| and _TABLE_SEPARATOR_RE.match(lines[idx + 1]) is not None |
| ) |
| if is_header and out and out[-1].strip() != "": |
| out.append("") |
| out.append(line) |
| return "\n".join(out) |
|
|
|
|
| def extract_answer(text: str) -> Optional[str]: |
| """Return the content of the first `<answer>...</answer>` block. |
| |
| Tries two strategies, in order, and discards placeholder-only content |
| (bare ellipses) that the model sometimes echoes from the prompt: |
| |
| 1. Well-formed `<answer>...</answer>` block. |
| 2. Truncated `<answer>...` with no closing tag (tokens ran out); |
| in that case we take everything after the opening tag. |
| """ |
| |
| |
| decoded = decode_escaped_whitespace(text or "") |
| cleaned = strip_think_blocks(decoded) |
|
|
| full_match = re.search( |
| r"<answer>\s*(.*?)\s*</answer>", |
| cleaned, |
| flags=re.DOTALL | re.IGNORECASE, |
| ) |
| if full_match is not None: |
| candidate = decode_escaped_whitespace(full_match.group(1).strip()) |
| if candidate and not _is_placeholder_answer(candidate): |
| return candidate |
| |
| |
| |
| return None |
|
|
| open_match = re.search( |
| r"<answer>\s*(.*)$", cleaned, flags=re.DOTALL | re.IGNORECASE |
| ) |
| if open_match is not None: |
| candidate = decode_escaped_whitespace(open_match.group(1).strip()) |
| if candidate and not _is_placeholder_answer(candidate): |
| return candidate |
|
|
| return None |
|
|
|
|
| def parse_tool_call(text: str) -> Tuple[Optional[str], Optional[Dict[str, Any]], Optional[str]]: |
| cleaned = strip_think_blocks(text or "") |
| match = re.search(r"<tool_call>\s*(.*?)\s*</tool_call>", cleaned, flags=re.DOTALL | re.IGNORECASE) |
| if not match: |
| return None, None, None |
| payload = match.group(1).strip() |
| try: |
| data = json.loads(payload) |
| except json.JSONDecodeError: |
| return None, None, "Invalid JSON in <tool_call> block." |
|
|
| name = data.get("name") |
| arguments = data.get("arguments", {}) |
| if not isinstance(name, str) or not isinstance(arguments, dict): |
| return None, None, "Invalid tool format. Expect name(str) and arguments(dict)." |
| return name, arguments, None |
|
|
|
|
| _SEARCH_UNAVAILABLE_HINT = ( |
| "The web-search backend is currently rate-limited or unreachable. " |
| "If this question can be answered confidently from your own training " |
| "knowledge (e.g. common product specs, historical facts, definitions), " |
| "please produce your best answer now inside <answer>...</answer>, and " |
| "mention any value that might be out of date. Only ask the user to " |
| "retry later if the question truly requires a fresh web lookup." |
| ) |
|
|
| |
| |
| SERPER_API_KEY = ( |
| os.getenv("SERPER_API_KEY") or os.getenv("SERPER_KEY_ID") or "" |
| ).strip() |
| SERPER_ENDPOINT = os.getenv("SERPER_ENDPOINT", "https://google.serper.dev/search") |
|
|
|
|
| def _serper_search(query: str, max_results: int) -> Dict[str, Any]: |
| """Hit the Google Serper API. Returns the same shape as `_ddg_search`. |
| |
| Serper responds in well under a second and is not subject to the 202 |
| Ratelimit we get from html.duckduckgo.com, so preferring it when the |
| key is set cuts latency dramatically and eliminates most search |
| failures on shared Space IPs. |
| """ |
| try: |
| resp = requests.post( |
| SERPER_ENDPOINT, |
| headers={ |
| "X-API-KEY": SERPER_API_KEY, |
| "Content-Type": "application/json", |
| }, |
| json={"q": query, "num": max_results}, |
| timeout=15, |
| ) |
| resp.raise_for_status() |
| data = resp.json() |
| except Exception as exc: |
| return { |
| "ok": False, |
| "query": query, |
| "error": f"Serper error: {type(exc).__name__}: {exc}", |
| "results": [], |
| "backend": "serper", |
| } |
|
|
| rows: List[Dict[str, str]] = [] |
| for item in (data.get("organic") or [])[:max_results]: |
| rows.append( |
| { |
| "title": item.get("title", ""), |
| "href": item.get("link", ""), |
| "body": item.get("snippet", ""), |
| } |
| ) |
| |
| |
| answer_box = data.get("answerBox") or {} |
| if answer_box: |
| rows.insert( |
| 0, |
| { |
| "title": answer_box.get("title", "Answer box"), |
| "href": answer_box.get("link", ""), |
| "body": answer_box.get("snippet") |
| or answer_box.get("answer") |
| or "", |
| }, |
| ) |
| if not rows: |
| return { |
| "ok": False, |
| "query": query, |
| "error": "Serper returned no organic results", |
| "results": [], |
| "backend": "serper", |
| } |
| return { |
| "ok": True, |
| "query": query, |
| "results": rows, |
| "cached": False, |
| "backend": "serper", |
| } |
|
|
|
|
| def _ddg_search(query: str, max_results: int) -> Dict[str, Any]: |
| """Fallback path: scrape DuckDuckGo. Rate-limits on shared IPs.""" |
| last_exc: Optional[BaseException] = None |
| for attempt in range(2): |
| try: |
| rows: List[Dict[str, str]] = [] |
| with DDGS() as ddgs: |
| for item in ddgs.text(query, max_results=max_results): |
| rows.append( |
| { |
| "title": item.get("title", ""), |
| "href": item.get("href", ""), |
| "body": item.get("body", ""), |
| } |
| ) |
| return { |
| "ok": True, |
| "query": query, |
| "results": rows, |
| "cached": False, |
| "backend": "duckduckgo", |
| } |
| except Exception as exc: |
| last_exc = exc |
| if attempt == 0: |
| time.sleep(1.5) |
| continue |
|
|
| err = f"{type(last_exc).__name__}: {last_exc}" if last_exc else "unknown error" |
| return { |
| "ok": False, |
| "query": query, |
| "error": f"DuckDuckGo unavailable ({err}).", |
| "results": [], |
| "backend": "duckduckgo", |
| } |
|
|
|
|
| def _run_search_single(query: str, max_results: int) -> Dict[str, Any]: |
| """Run one search query, preferring Serper when the key is set. |
| |
| Returns a structured dict on both success and failure; never raises. |
| Order of preference: |
| |
| 1. Google Serper (fast, no scraping, requires `SERPER_API_KEY` / |
| `SERPER_KEY_ID`). |
| 2. DuckDuckGo HTML backend (free, but rate-limits on shared Space IPs). |
| 3. Graceful `ok: False` payload with a hint that tells the agent to |
| answer from its own knowledge if it reasonably can. |
| """ |
| if not query.strip(): |
| return {"ok": False, "error": "Search query cannot be empty."} |
| cache_key = f"{query.strip().lower()}::{max_results}" |
| if cache_key in SEARCH_CACHE: |
| return {**SEARCH_CACHE[cache_key], "cached": True} |
|
|
| tried: List[Dict[str, Any]] = [] |
| if SERPER_API_KEY: |
| serper_result = _serper_search(query, max_results) |
| if serper_result.get("ok"): |
| SEARCH_CACHE[cache_key] = serper_result |
| return serper_result |
| tried.append(serper_result) |
|
|
| ddg_result = _ddg_search(query, max_results) |
| if ddg_result.get("ok"): |
| SEARCH_CACHE[cache_key] = ddg_result |
| return ddg_result |
| tried.append(ddg_result) |
|
|
| |
| errors = "; ".join( |
| f"{r.get('backend', 'unknown')}: {r.get('error', 'no results')}" |
| for r in tried |
| ) |
| return { |
| "ok": False, |
| "query": query, |
| "error": f"All search backends failed ({errors}).", |
| "results": [], |
| "hint": _SEARCH_UNAVAILABLE_HINT, |
| } |
|
|
|
|
| def run_search(query: Union[str, List[str]], max_results: int = 5) -> Dict[str, Any]: |
| """Runs one or more queries through DuckDuckGo. |
| |
| QUEST's schema passes `query` as an array of strings, while the simpler |
| starter schema used a single string. We accept both shapes. |
| """ |
| if isinstance(query, list): |
| sub_results: List[Dict[str, Any]] = [] |
| for q in query: |
| if not isinstance(q, str) or not q.strip(): |
| continue |
| sub_results.append(_run_search_single(q, max_results)) |
| return {"ok": True, "queries": query, "results": sub_results} |
| return _run_search_single(str(query or "").strip(), max_results) |
|
|
|
|
| def _clean_html_to_text(html: str, max_chars: int) -> str: |
| soup = BeautifulSoup(html, "html.parser") |
| for tag in soup(["script", "style", "noscript"]): |
| tag.decompose() |
| text = soup.get_text(separator=" ", strip=True) |
| text = re.sub(r"\s+", " ", text) |
| return text[:max_chars] |
|
|
|
|
| def _run_visit_single(url: str, max_chars: int, goal: str = "") -> Dict[str, Any]: |
| if not url.strip(): |
| return {"ok": False, "error": "URL cannot be empty."} |
| cache_key = f"{url.strip()}::{max_chars}" |
| if cache_key in VISIT_CACHE: |
| return {**VISIT_CACHE[cache_key], "cached": True, "goal": goal} |
| try: |
| resp = requests.get( |
| url, |
| timeout=20, |
| headers={"User-Agent": "Mozilla/5.0 (compatible; DeepResearchSpace/1.0)"}, |
| ) |
| resp.raise_for_status() |
| content_type = resp.headers.get("content-type", "") |
| if "text/html" in content_type or "<html" in resp.text[:200].lower(): |
| text = _clean_html_to_text(resp.text, max_chars=max_chars) |
| else: |
| text = resp.text[:max_chars] |
| payload = {"ok": True, "url": url, "content": text, "cached": False, "goal": goal} |
| VISIT_CACHE[cache_key] = payload |
| return payload |
| except Exception as exc: |
| return {"ok": False, "url": url, "error": str(exc), "goal": goal} |
|
|
|
|
| def run_visit( |
| url: Union[str, List[str]], |
| max_chars: int = 6000, |
| goal: str = "", |
| ) -> Dict[str, Any]: |
| """Fetches one or more URLs. Accepts string or list (QUEST schema).""" |
| if isinstance(url, list): |
| sub_results: List[Dict[str, Any]] = [] |
| for u in url: |
| if not isinstance(u, str) or not u.strip(): |
| continue |
| sub_results.append(_run_visit_single(u, max_chars, goal)) |
| return {"ok": True, "goal": goal, "results": sub_results} |
| return _run_visit_single(str(url or "").strip(), max_chars, goal) |
|
|
|
|
| def _build_client_for_model(model: str) -> Tuple[InferenceClient, str, List[str]]: |
| """Returns (client, primary_model_id, fallback_model_ids). |
| |
| When the user picks the Quest model and QUEST_BASE_URL is configured, the |
| InferenceClient is pointed at the dedicated endpoint; otherwise we hit the |
| shared HF Inference API and let the starter fall back across free models. |
| """ |
| token = os.getenv("HF_TOKEN") |
| quest_timeout = int(os.getenv("QUEST_REQUEST_TIMEOUT", "600")) |
| if model == QUEST_MODEL_ID and QUEST_BASE_URL: |
| |
| |
| endpoint_token = os.getenv("QUEST_API_KEY") or token |
| client = InferenceClient( |
| base_url=QUEST_BASE_URL, |
| token=endpoint_token, |
| timeout=quest_timeout, |
| ) |
| return client, QUEST_ENDPOINT_MODEL, [] |
| client = InferenceClient(token=token, timeout=quest_timeout) |
| return client, model, [] |
|
|
|
|
| def call_model( |
| client: InferenceClient, |
| messages: List[Dict[str, str]], |
| preferred_model: str, |
| candidate_models: List[str], |
| temperature: float, |
| max_new_tokens: int, |
| ) -> Tuple[str, str]: |
| model_order: List[str] = [] |
| for m in [preferred_model] + candidate_models: |
| if m and m not in model_order: |
| model_order.append(m) |
|
|
| last_error = None |
| for model_name in model_order: |
| try: |
| completion = client.chat_completion( |
| model=model_name, |
| messages=messages, |
| temperature=temperature, |
| max_tokens=max_new_tokens, |
| ) |
| return completion.choices[0].message.content or "", model_name |
| except Exception as exc: |
| last_error = exc |
| continue |
| raise RuntimeError(f"All model candidates failed. Last error: {last_error}") |
|
|
|
|
| def _render_progress( |
| lines: List[str], |
| used_model: str, |
| question: str, |
| ) -> str: |
| """Render the in-progress status view that replaces the Markdown panel |
| while the agent is still running, so the user is not staring at a blank |
| box for the 20-60 seconds a full QUEST-35B research run can take.""" |
| header = ( |
| f"### β³ Researchingβ¦\n\n" |
| f"**Model:** `{used_model}` \n" |
| f"**Question:** {question.strip()[:200]}" |
| ) |
| if not lines: |
| body = "_Starting agentβ¦_" |
| else: |
| body = "\n".join(f"- {line}" for line in lines) |
| return f"{header}\n\n{body}" |
|
|
|
|
| def _trace_to_json(state: "AgentState", used_model: str) -> str: |
| return json.dumps( |
| { |
| "used_model": used_model, |
| "searched_queries": state.searched_queries, |
| "visited_urls": state.visited_urls, |
| "trusted_notes": state.trusted_notes[-10:], |
| "trace": state.trace, |
| }, |
| ensure_ascii=False, |
| indent=2, |
| ) |
|
|
|
|
| MEMORY_STRATEGIES = ("condenser", "vanilla", "discard_all", "hide_tool_result") |
|
|
|
|
| def _normalize_memory_strategy(strategy: str) -> str: |
| s = (strategy or "condenser").strip().lower().replace("-", "_") |
| if s == "hide_tool_results": |
| s = "hide_tool_result" |
| return s if s in MEMORY_STRATEGIES else "condenser" |
|
|
|
|
| def _apply_memory_strategy(messages: List[Dict[str, str]], strategy: str, turn: int) -> None: |
| """Lightweight port of the strategies defined in the Quest inference |
| code (`inference/react_agent.py`). Upstream is token-threshold-driven; |
| this Space approximates each strategy on a turn-count basis for demo |
| purposes. |
| |
| - vanilla: no-op (matches MEMORY_ENABLED=false upstream). |
| - condenser: no-op here; the main loop injects a compact research-state |
| summary every few turns (a poor-man's stand-in for the upstream |
| State Summarizer LLM that emits a structured trusted/untrusted/ |
| uncertain JSON when the token threshold is hit). |
| - discard_all: every 8 turns, reset history to [system, user question] |
| (upstream resets when token_count crosses the threshold). |
| - hide_tool_result: keep only the most recent tool-response user |
| message; older ones get their content replaced with a stub |
| (mirrors upstream behavior). |
| """ |
| if strategy == "discard_all": |
| if turn > 1 and turn % 8 == 0 and len(messages) > 2: |
| system_msg = messages[0] |
| question_msg = messages[1] |
| messages.clear() |
| messages.append(system_msg) |
| messages.append(question_msg) |
| messages.append( |
| { |
| "role": "user", |
| "content": "[memory discarded at turn " |
| f"{turn} β continue the research from the original question]", |
| } |
| ) |
| elif strategy == "hide_tool_result": |
| keep_tail = 1 |
| tool_indices = [ |
| i for i, m in enumerate(messages) |
| if m.get("role") == "user" and str(m.get("content", "")).startswith("<tool_response>") |
| ] |
| if len(tool_indices) > keep_tail: |
| for i in tool_indices[:-keep_tail]: |
| if messages[i]["content"] != "<tool_response>[hidden]</tool_response>": |
| messages[i] = { |
| "role": "user", |
| "content": "<tool_response>[hidden]</tool_response>", |
| } |
|
|
|
|
| def build_research_agent( |
| question: str, |
| model: str, |
| max_turns: int, |
| temperature: float, |
| memory_strategy: str = "condenser", |
| ): |
| """Run the ReAct research loop as a generator. |
| |
| Each `yield` emits a `(markdown_for_answer_panel, json_for_record_panel)` |
| tuple. Intermediate yields show progress so that Gradio streams the |
| status lines into the UI as work happens. The last yield contains the |
| final answer and the final trace. |
| """ |
| client, primary_model, fallback_models = _build_client_for_model(model) |
| |
| display_primary = model if (model == QUEST_MODEL_ID) else primary_model |
| state = AgentState() |
| used_model = display_primary |
| status_lines: List[str] = [] |
|
|
| def _emit(): |
| """Yield the current progress snapshot to Gradio.""" |
| return ( |
| _render_progress(status_lines, used_model, question), |
| _trace_to_json(state, used_model), |
| ) |
|
|
| messages: List[Dict[str, str]] = [ |
| {"role": "system", "content": build_system_prompt()}, |
| {"role": "user", "content": question}, |
| ] |
|
|
| final_answer: Optional[str] = None |
|
|
| status_lines.append("π Starting research agent") |
| yield _emit() |
|
|
| strategy = _normalize_memory_strategy(memory_strategy) |
| os.environ["MEMORY_STRATEGY"] = strategy |
|
|
| for turn in range(1, max_turns + 1): |
| _apply_memory_strategy(messages, strategy, turn) |
| if strategy == "condenser" and state.trusted_notes and turn > 1 and turn % 3 == 0: |
| summary_lines = "\n".join(f"- {n}" for n in state.trusted_notes[-6:]) |
| messages.append( |
| { |
| "role": "user", |
| "content": f"RESEARCH STATE SUMMARY\n{summary_lines}\nUse this summary to avoid repeating work.", |
| } |
| ) |
|
|
| status_lines.append(f"π§ turn {turn}: thinkingβ¦") |
| yield _emit() |
|
|
| t0 = time.time() |
| raw_output, endpoint_model = call_model( |
| client=client, |
| messages=messages, |
| preferred_model=primary_model, |
| candidate_models=fallback_models, |
| temperature=temperature, |
| max_new_tokens=int(os.getenv("QUEST_MAX_NEW_TOKENS", "4096")), |
| ) |
| dt = time.time() - t0 |
| model_output = raw_output |
| |
| |
| used_model = display_primary if endpoint_model == primary_model == QUEST_ENDPOINT_MODEL else endpoint_model |
| messages.append({"role": "assistant", "content": model_output}) |
| state.trace.append({"turn": turn, "assistant": model_output, "elapsed_s": round(dt, 2)}) |
| status_lines[-1] = f"π§ turn {turn}: model reply in {dt:.1f}s" |
| yield _emit() |
|
|
| extracted_answer = extract_answer(model_output) |
| if extracted_answer: |
| final_answer = extracted_answer |
| status_lines.append("βοΈ writing final answer") |
| yield _emit() |
| break |
|
|
| tool_name, tool_args, tool_err = parse_tool_call(model_output) |
| if tool_err: |
| tool_response = {"ok": False, "error": tool_err} |
| status_lines.append(f"β οΈ turn {turn}: malformed tool call β {tool_err}") |
| yield _emit() |
| elif not tool_name: |
| |
| |
| |
| |
| |
| |
| messages.append( |
| { |
| "role": "user", |
| "content": ( |
| "You did not call a tool and did not produce a final " |
| "answer. Please now write your best final answer, " |
| "wrapped between an opening <answer> tag and a " |
| "closing </answer> tag. Put the real answer text " |
| "between those tags; do not write a literal ellipsis " |
| "or other placeholder. If the question asks for " |
| "tabular data, use GitHub-Flavored Markdown pipe " |
| "tables (`| col1 | col2 |` + `|---|---|`) and put a " |
| "blank line before the first row so the table renders." |
| ), |
| } |
| ) |
| status_lines.append(f"π turn {turn}: model stalled; asking for an answer") |
| yield _emit() |
| continue |
| else: |
| if tool_name == "search": |
| raw_query = tool_args.get("query", "") |
| queries: List[str] |
| if isinstance(raw_query, list): |
| queries = [str(q).strip() for q in raw_query if str(q).strip()] |
| else: |
| queries = [str(raw_query).strip()] if str(raw_query).strip() else [] |
| max_results = int(tool_args.get("max_results", DEFAULT_MAX_SEARCH_RESULTS)) |
| max_results = max(1, min(max_results, DEFAULT_MAX_SEARCH_RESULTS)) |
|
|
| queries_preview = ", ".join(f"`{q}`" for q in queries) or "_(empty)_" |
| status_lines.append(f"π turn {turn}: searching {queries_preview}") |
| yield _emit() |
|
|
| per_query: List[Dict[str, Any]] = [] |
| backend_labels: List[str] = [] |
| hits_total = 0 |
| for q in queries: |
| if q in state.searched_query_set: |
| per_query.append({ |
| "ok": True, |
| "query": q, |
| "cached": True, |
| "note": "Already searched; reusing cached result.", |
| "results": [], |
| }) |
| backend_labels.append("cache") |
| continue |
| state.searched_queries.append(q) |
| state.searched_query_set.add(q) |
| single = _run_search_single(q, max_results) |
| per_query.append(single) |
| backend_labels.append(single.get("backend", "unknown")) |
| if single.get("ok"): |
| hits_total += len(single.get("results", [])) |
| first_titles = [r.get("title", "") for r in single.get("results", [])[:2]] |
| if first_titles: |
| state.trusted_notes.append( |
| f"Searched '{q}' and found leads: {', '.join(t for t in first_titles if t)}" |
| ) |
| else: |
| status_lines.append( |
| f"β οΈ search failed on `{q}` via {single.get('backend', 'unknown')}: " |
| f"{single.get('error', 'no results')}" |
| ) |
| tool_response = ( |
| per_query[0] |
| if len(per_query) == 1 |
| else {"ok": True, "queries": queries, "results": per_query} |
| ) |
| unique_backends = sorted(set(backend_labels)) |
| backend_str = "/".join(unique_backends) if unique_backends else "?" |
| status_lines.append( |
| f"β
turn {turn}: got {hits_total} hit(s) via {backend_str}" |
| ) |
| yield _emit() |
| elif tool_name == "visit": |
| raw_url = tool_args.get("url", "") |
| urls: List[str] |
| if isinstance(raw_url, list): |
| urls = [str(u).strip() for u in raw_url if str(u).strip()] |
| else: |
| urls = [str(raw_url).strip()] if str(raw_url).strip() else [] |
| goal = str(tool_args.get("goal", "")).strip() |
| max_chars = int(tool_args.get("max_chars", 6000)) |
| max_chars = max(500, min(max_chars, 20000)) |
|
|
| urls_preview = ", ".join(f"`{u[:60]}`" for u in urls) or "_(empty)_" |
| status_lines.append(f"π turn {turn}: visiting {urls_preview}") |
| yield _emit() |
|
|
| per_url: List[Dict[str, Any]] = [] |
| visit_ok = 0 |
| for u in urls: |
| if u in state.visited_url_set: |
| per_url.append({ |
| "ok": True, |
| "url": u, |
| "cached": True, |
| "note": "Already visited; reusing cached result.", |
| }) |
| visit_ok += 1 |
| continue |
| state.visited_urls.append(u) |
| state.visited_url_set.add(u) |
| single = _run_visit_single(u, max_chars, goal) |
| per_url.append(single) |
| if single.get("ok"): |
| visit_ok += 1 |
| snippet = str(single.get("content", ""))[:180] |
| if snippet: |
| state.trusted_notes.append( |
| f"Visited {u} and extracted key context: {snippet}" |
| ) |
| tool_response = ( |
| per_url[0] |
| if len(per_url) == 1 |
| else {"ok": True, "goal": goal, "results": per_url} |
| ) |
| status_lines.append( |
| f"β
turn {turn}: read {visit_ok}/{len(urls)} page(s)" |
| ) |
| yield _emit() |
| else: |
| tool_response = {"ok": False, "error": f"Unknown tool: {tool_name}"} |
| status_lines.append(f"β οΈ turn {turn}: unknown tool `{tool_name}`") |
| yield _emit() |
|
|
| state.trace.append({"turn": turn, "tool": tool_name, "tool_response": tool_response}) |
| messages.append( |
| { |
| "role": "user", |
| "content": TOOL_RESPONSE_TEMPLATE.format( |
| payload=json.dumps(tool_response, ensure_ascii=False) |
| ), |
| } |
| ) |
|
|
| if final_answer is None: |
| final_answer = ( |
| "I could not finish a complete research answer within the configured turns. " |
| "Try increasing max turns or switching to a stronger model." |
| ) |
| else: |
| final_answer = ensure_markdown_table_blank_lines(final_answer) |
|
|
| citations = "\n".join(f"- {url}" for url in sorted(set(state.visited_urls))) |
| final_answer = f"**Model used:** `{used_model}`\n\n{final_answer}" |
| if citations: |
| final_answer = f"{final_answer}\n\n### Visited Sources\n{citations}" |
|
|
| trace_text = _trace_to_json(state, used_model) |
| yield (final_answer, trace_text) |
|
|
|
|
| def run_ui( |
| question: str, |
| max_turns: int, |
| memory_strategy: str, |
| temperature: float, |
| ): |
| if not question.strip(): |
| yield "Please input a question.", "{}" |
| return |
| if not os.getenv("HF_TOKEN"): |
| warning = ( |
| "HF_TOKEN is not configured in Space Secrets. " |
| "Go to Settings -> Secrets -> add `HF_TOKEN`, then retry." |
| ) |
| yield warning, json.dumps({"error": warning}, ensure_ascii=False, indent=2) |
| return |
| if not QUEST_BASE_URL: |
| warning = ( |
| f"`{QUEST_MODEL_ID}` needs a private HF Inference Endpoint. " |
| "Create one at https://ui.endpoints.huggingface.co/, then set " |
| "`QUEST_BASE_URL` in Space Secrets to the endpoint's `/v1/` URL." |
| ) |
| yield warning, json.dumps({"error": warning}, ensure_ascii=False, indent=2) |
| return |
| try: |
| for partial_answer, partial_trace in build_research_agent( |
| question=question, |
| model=QUEST_MODEL_ID, |
| max_turns=max_turns, |
| temperature=temperature, |
| memory_strategy=memory_strategy, |
| ): |
| yield partial_answer, partial_trace |
| except Exception as exc: |
| yield f"Error: {exc}", json.dumps({"error": str(exc)}, ensure_ascii=False, indent=2) |
|
|
|
|
| EXAMPLES = [ |
| { |
| "category": "Multi-hop facts", |
| "icon": "π―", |
| "text": "Who was the first person to walk on the Moon, and which U.S. President set that goal in his famous 1962 βMoon speechβ?", |
| }, |
| { |
| "category": "Time-varying + multi-hop", |
| "icon": "π", |
| "text": "Who is the current CEO of the company that acquired GitHub in 2018, and what was that company's market capitalization at the close of the most recent quarter?", |
| }, |
| { |
| "category": "Multi-constraint", |
| "icon": "π§©", |
| "text": "Find a 2-day itinerary in Tokyo under $250 focused on contemporary art museums and vegetarian restaurants, including transit between sites.", |
| }, |
| { |
| "category": "Research Report", |
| "icon": "π", |
| "text": "Compare the LLM-safety research approaches of Anthropic, OpenAI, and Google DeepMind over the past 18 months, focusing on alignment techniques and red-teaming methodologies.", |
| }, |
| ] |
|
|
|
|
| def _example_label(ex: Dict[str, str]) -> str: |
| return f"{ex['icon']} {ex['category']} β {ex['text']}" |
|
|
|
|
| with gr.Blocks( |
| title="QUEST Β· Deep Research by OSU NLP", |
| theme=APP_THEME, |
| css=CUSTOM_CSS, |
| fill_width=True, |
| ) as demo: |
| |
| gr.HTML( |
| """ |
| <header class="quest-header"> |
| <div class="quest-header-text"> |
| <h1 class="quest-header-title"><span class="quest-name">QUEST</span>: A Fully Open Recipe for Training Deep Research Agents from Scratch</h1> |
| <a class="quest-header-byline" href="https://x.com/osunlp" target="_blank" rel="noopener noreferrer">Built by OSU NLP Group</a> |
| </div> |
| </header> |
| """ |
| ) |
|
|
| |
| with gr.Row(elem_classes="layout-gap"): |
| with gr.Column(scale=6, min_width=420): |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML( |
| '<div class="section-heading">Ask the agent</div>' |
| '<div class="hero-heading"><span class="quest-name">QUEST</span>: What I can research for you?</div>' |
| ) |
| question = gr.Textbox( |
| show_label=False, |
| placeholder="Ask anything you want to research in depth...", |
| lines=6, |
| ) |
| with gr.Row(elem_classes="action-row"): |
| run_btn = gr.Button("Run Research", variant="primary", size="lg") |
| stop_btn = gr.Button("Stop", variant="stop", size="lg") |
| clear_btn = gr.Button("Clear", variant="secondary", size="lg") |
|
|
| with gr.Group(elem_classes="section-card"): |
| gr.HTML( |
| '<div class="section-heading">Try examples</div>' |
| '<div class="example-note"><span class="quest-name">QUEST</span> can handle multiple types of queries as shown below.</div>' |
| ) |
| with gr.Column(elem_classes="example-buttons"): |
| example_buttons = [ |
| gr.Button(_example_label(ex), variant="secondary", elem_classes="example-btn") |
| for ex in EXAMPLES |
| ] |
|
|
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-heading">Output</div>') |
| with gr.Tabs(): |
| with gr.TabItem("Result"): |
| answer = gr.Markdown(label="Final Answer") |
| with gr.TabItem("Record"): |
| trace = gr.Code(label="Execution Trace (JSON)", language="json") |
|
|
| with gr.Column(scale=4, min_width=340, elem_classes="right-stack"): |
| with gr.Group(elem_classes="section-card"): |
| gr.HTML( |
| f""" |
| <div class="section-heading">Open release</div> |
| <div class="resource-grid"> |
| <a class="resource-card" href="{PAPER_URL}" target="_blank" rel="noopener noreferrer"> |
| <span class="resource-card-icon" aria-hidden="true"> |
| <svg viewBox="0 0 24 24" role="img" focusable="false"><path d="M6 2.5h8.2L19 7.3v14.2H6V2.5Zm8 1.9v3.2h3.2L14 4.4ZM8.1 9.8h8.8V8.4H8.1v1.4Zm0 3.3h8.8v-1.4H8.1v1.4Zm0 3.3h6.4V15H8.1v1.4Z"/></svg> |
| </span> |
| <span class="resource-card-text"><strong>Paper</strong><small>Blog</small></span> |
| </a> |
| <a class="resource-card" href="{CODE_URL}" target="_blank" rel="noopener noreferrer"> |
| <span class="resource-card-icon" aria-hidden="true"> |
| <svg viewBox="0 0 24 24" role="img" focusable="false"><path d="M12 1.8c-5.7 0-10.3 4.6-10.3 10.3 0 4.6 3 8.5 7.1 9.8.5.1.7-.2.7-.5v-1.8c-2.9.6-3.5-1.2-3.5-1.2-.5-1.2-1.1-1.5-1.1-1.5-.9-.6.1-.6.1-.6 1 .1 1.6 1.1 1.6 1.1.9 1.6 2.4 1.1 3 .8.1-.7.4-1.1.7-1.3-2.3-.3-4.7-1.2-4.7-5.1 0-1.1.4-2.1 1.1-2.8-.1-.3-.5-1.4.1-2.8 0 0 .9-.3 2.9 1.1.8-.2 1.7-.3 2.6-.3s1.8.1 2.6.3c2-1.4 2.9-1.1 2.9-1.1.6 1.4.2 2.5.1 2.8.7.8 1.1 1.7 1.1 2.8 0 4-2.4 4.8-4.7 5.1.4.3.7 1 .7 2v2.9c0 .3.2.6.7.5 4.1-1.4 7.1-5.2 7.1-9.8C22.3 6.4 17.7 1.8 12 1.8Z"/></svg> |
| </span> |
| <span class="resource-card-text"><strong>Code</strong><small>GitHub</small></span> |
| </a> |
| <a class="resource-card" href="{DATASET_URL}" target="_blank" rel="noopener noreferrer"> |
| <span class="resource-card-icon resource-card-emoji" aria-hidden="true">π€</span> |
| <span class="resource-card-text"><strong>Data</strong><small>Collection</small></span> |
| </a> |
| <a class="resource-card" href="{MODEL_URL}" target="_blank" rel="noopener noreferrer"> |
| <span class="resource-card-icon resource-card-emoji" aria-hidden="true">π€</span> |
| <span class="resource-card-text"><strong>Model</strong><small>QUEST-35B-RL</small></span> |
| </a> |
| </div> |
| """ |
| ) |
|
|
| with gr.Group(elem_classes="section-card"): |
| gr.HTML('<div class="section-heading">Settings</div>') |
| gr.Textbox( |
| label="Model", |
| value=QUEST_MODEL_ID, |
| interactive=False, |
| elem_id="quest-model", |
| ) |
| memory_strategy = gr.Radio( |
| label="Memory Strategy", |
| choices=[ |
| ("Condenser (default)", "condenser"), |
| ("Vanilla", "vanilla"), |
| ("Discard-all", "discard_all"), |
| ("Hide-tool-result", "hide_tool_result"), |
| ], |
| value="condenser", |
| elem_id="quest-memory-strategy", |
| ) |
| gr.HTML( |
| '<div class="memory-help">' |
| '<b>Condenser</b> (default) β when context grows large, a State Summarizer LLM compresses earlier turns into a structured JSON of trusted/untrusted/uncertain claims, visited sources, and prior search queries; the agent continues with that compact state.<br>' |
| '<b>Vanilla</b> β memory management disabled; the full conversation history is kept.<br>' |
| '<b>Discard-all</b> β when context grows large, the entire message history is reset, restarting the agent from the original question with no accumulated context.<br>' |
| '<b>Hide-tool-result</b> β when context grows large, older tool responses are pruned; only the most recent tool result is kept.' |
| '</div>' |
| ) |
| max_turns = gr.Slider( |
| label="Max Turns", |
| minimum=2, |
| maximum=50, |
| value=6, |
| step=1, |
| elem_id="quest-max-turns", |
| ) |
| temperature = gr.Slider( |
| label="Temperature", |
| minimum=0.0, |
| maximum=1.5, |
| value=1.0, |
| step=0.1, |
| elem_id="quest-temperature", |
| ) |
|
|
| gr.HTML( |
| """ |
| <footer class="quest-footer"> |
| <p>QUEST is a fully open recipe for training deep research agents from scratch — covering data synthesis, memory management, infrastructure, and long-horizon training.</p> |
| <div class="quest-footer-links"> |
| <a href="https://nlp.osu.edu/" target="_blank" rel="noopener noreferrer">OSU NLP</a> |
| <a href="https://huggingface.co/osunlp" target="_blank" rel="noopener noreferrer">Hugging Face</a> |
| </div> |
| </footer> |
| """ |
| ) |
|
|
| run_event = run_btn.click( |
| fn=run_ui, |
| inputs=[question, max_turns, memory_strategy, temperature], |
| outputs=[answer, trace], |
| ) |
| for btn, ex in zip(example_buttons, EXAMPLES): |
| btn.click( |
| fn=(lambda text=ex["text"]: text), |
| inputs=[], |
| outputs=[question], |
| ) |
| stop_btn.click(fn=None, cancels=[run_event]) |
| clear_btn.click( |
| fn=lambda: ("", "", "{}"), |
| inputs=[], |
| outputs=[question, answer, trace], |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch() |
|
|