Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| DCI-Agent Pi Search โ Prestige Purple edition | |
| Gradio 6.x ยท dark-first ยท follows prestige-purple-DESIGN.md | |
| """ | |
| from __future__ import annotations | |
| import html as _html | |
| import json | |
| import os | |
| import re | |
| import shutil | |
| import tempfile | |
| import time | |
| import zipfile | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Any, Dict, Generator, List, Optional | |
| import gradio as gr | |
| try: | |
| from charset_normalizer import from_bytes | |
| except ImportError: | |
| from_bytes = None | |
| try: | |
| from pypdf import PdfReader | |
| except ImportError: | |
| PdfReader = None | |
| from pi_wrapper import run_pi_stream | |
| # โโ Paths โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| APP_DIR = Path(__file__).resolve().parent | |
| DEFAULT_CORPUS_DIR = APP_DIR / "default_corpus" | |
| DEFAULT_CORPUS_LABEL = "Default (bright_corpus)" | |
| UPLOAD_FILE_LABEL = "Upload your own corpus(single file)" | |
| UPLOAD_FOLDER_LABEL = "Upload your own corpus(folder)" | |
| HERO_LOGO_PATH = APP_DIR / "img" / "logo.png" | |
| TEXT_FILE_SUFFIXES = { | |
| ".txt", ".md", ".csv", ".tsv", ".json", ".jsonl", ".xml", ".html", ".htm", | |
| ".yaml", ".yml", ".log", ".srt", ".py", ".js", ".ts", ".tsx", ".jsx", | |
| ".java", ".c", ".cc", ".cpp", ".h", ".hpp", ".sql", ".rst", | |
| } | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Minimal Slate Gradio theme | |
| # Sets Gradio's own CSS variables so every component inherits dark colors. | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| PP_THEME = gr.themes.Base( | |
| primary_hue=gr.themes.colors.slate, | |
| secondary_hue=gr.themes.colors.slate, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=[gr.themes.GoogleFont("Plus Jakarta Sans"), "ui-sans-serif", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"], | |
| ).set( | |
| # โโ Light mode backgrounds โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| body_background_fill="#F5F7FA", | |
| background_fill_primary="#FFFFFF", | |
| background_fill_secondary="#F0F4F8", | |
| block_background_fill="#FFFFFF", | |
| block_border_color="#E5EBF0", | |
| block_border_width="1px", | |
| block_label_background_fill="#FFFFFF", | |
| block_label_text_color="#64748B", | |
| block_label_text_size="*text_xs", | |
| block_title_text_color="#64748B", | |
| block_title_background_fill="transparent", | |
| block_shadow="0 2px 8px rgba(15,23,42,0.06)", | |
| input_background_fill="#FFFFFF", | |
| input_border_color="#CBD5E1", | |
| input_border_color_focus="#475569", | |
| input_placeholder_color="#94A3B8", | |
| input_shadow="none", | |
| input_shadow_focus="0 0 0 3px rgba(71,84,105,0.1)", | |
| body_text_color="#0F172A", | |
| body_text_color_subdued="#64748B", | |
| button_primary_background_fill="linear-gradient(135deg,#334155 0%,#475569 100%)", | |
| button_primary_background_fill_hover="linear-gradient(135deg,#1E293B 0%,#334155 100%)", | |
| button_primary_text_color="#FFFFFF", | |
| button_primary_border_color="transparent", | |
| button_secondary_background_fill="transparent", | |
| button_secondary_background_fill_hover="rgba(71,84,105,0.06)", | |
| button_secondary_text_color="#334155", | |
| button_secondary_border_color="#475569", | |
| checkbox_background_color="#FFFFFF", | |
| checkbox_background_color_selected="#334155", | |
| checkbox_border_color="#CBD5E1", | |
| checkbox_border_color_focus="#475569", | |
| checkbox_label_background_fill="#F0F4F8", | |
| checkbox_label_background_fill_selected="#334155", | |
| checkbox_label_text_color="#1E293B", | |
| checkbox_label_text_color_selected="#FFFFFF", | |
| slider_color="#475569", | |
| border_color_primary="#E5EBF0", | |
| border_color_accent="#475569", | |
| color_accent="#334155", | |
| color_accent_soft="rgba(51,65,85,0.08)", | |
| panel_background_fill="#F5F3FF", | |
| panel_border_color="#DDD6FE", | |
| error_background_fill="#FFF1F2", | |
| error_border_color="#F87171", | |
| error_text_color="#DC2626", | |
| button_large_padding="10px 24px", | |
| button_large_radius="10px", | |
| form_gap_width="8px", | |
| layout_gap="12px", | |
| # โโ Dark mode overrides โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| body_background_fill_dark="#0F172A", | |
| background_fill_primary_dark="#1E293B", | |
| background_fill_secondary_dark="#334155", | |
| block_background_fill_dark="#1E293B", | |
| block_border_color_dark="#475569", | |
| block_label_background_fill_dark="#1E293B", | |
| block_label_text_color_dark="#CBD5E1", | |
| block_title_text_color_dark="#CBD5E1", | |
| block_title_background_fill_dark="transparent", | |
| block_shadow_dark="0 2px 12px rgba(0,0,0,0.45)", | |
| input_background_fill_dark="#1E293B", | |
| input_border_color_dark="#475569", | |
| input_border_color_focus_dark="#E2E8F0", | |
| input_placeholder_color_dark="#94A3B8", | |
| input_shadow_focus_dark="0 0 0 3px rgba(226,232,240,0.1)", | |
| body_text_color_dark="#E2E8F0", | |
| body_text_color_subdued_dark="#CBD5E1", | |
| button_primary_background_fill_dark="linear-gradient(135deg,#334155 0%,#475569 100%)", | |
| button_primary_background_fill_hover_dark="linear-gradient(135deg,#1E293B 0%,#334155 100%)", | |
| button_primary_text_color_dark="#FFFFFF", | |
| button_primary_border_color_dark="transparent", | |
| button_secondary_background_fill_dark="transparent", | |
| button_secondary_background_fill_hover_dark="rgba(226,232,240,0.08)", | |
| button_secondary_text_color_dark="#E2E8F0", | |
| button_secondary_border_color_dark="#E2E8F0", | |
| checkbox_background_color_dark="#1E293B", | |
| checkbox_background_color_selected_dark="#334155", | |
| checkbox_border_color_dark="#475569", | |
| checkbox_border_color_focus_dark="#E2E8F0", | |
| checkbox_label_background_fill_dark="#334155", | |
| checkbox_label_background_fill_selected_dark="#334155", | |
| checkbox_label_text_color_dark="#CBD5E1", | |
| checkbox_label_text_color_selected_dark="#FFFFFF", | |
| slider_color_dark="#E2E8F0", | |
| border_color_primary_dark="#475569", | |
| border_color_accent_dark="#E2E8F0", | |
| color_accent_soft_dark="rgba(226,232,240,0.12)", | |
| panel_background_fill_dark="#0F172A", | |
| panel_border_color_dark="#475569", | |
| error_background_fill_dark="#160808", | |
| error_border_color_dark="#EF4444", | |
| error_text_color_dark="#FC8181", | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Supplementary CSS (only for things the theme system can't set) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| CUSTOM_CSS = """ | |
| /* โโ Hero โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .hero-wrap { | |
| padding: 8px 0 12px; | |
| margin-bottom: 8px; | |
| } | |
| html.dark .hero-wrap { border-bottom: 1px solid #475569; } | |
| html:not(.dark) .hero-wrap { border-bottom: 1px solid #E5EBF0; } | |
| .hero-title-row { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| margin: 0 0 12px !important; | |
| } | |
| .hero-logo { | |
| width: 72.5px; | |
| height: 72.5px; | |
| object-fit: contain; | |
| flex: 0 0 72.5px; | |
| } | |
| .hero-title { | |
| font-size: 35px !important; | |
| font-weight: 800 !important; | |
| letter-spacing: -0.03em !important; | |
| line-height: 1.1 !important; | |
| background: linear-gradient(128deg, #334155 0%, #1E293B 100%) !important; | |
| -webkit-background-clip: text !important; | |
| -webkit-text-fill-color: transparent !important; | |
| background-clip: text !important; | |
| margin: 0 !important; | |
| padding-bottom: 4px !important; | |
| display: block; | |
| } | |
| .hero-subtitle { | |
| font-size: 11px !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0.1em !important; | |
| text-transform: uppercase !important; | |
| color: #64748B !important; | |
| margin: 2px 0 12px !important; | |
| display: block; | |
| } | |
| .hero-links { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| flex-wrap: wrap; | |
| } | |
| .hero-links a { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 3px 11px 4px; | |
| border-radius: 20px; | |
| font-size: 11px !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0.04em !important; | |
| text-decoration: none !important; | |
| transition: background 0.15s, border-color 0.15s, color 0.15s; | |
| } | |
| html.dark .hero-links a { | |
| background: rgba(180,175,242,0.07); | |
| border: 1px solid #2A2A3C; | |
| color: #8B8BA8 !important; | |
| } | |
| html:not(.dark) .hero-links a { | |
| background: #F0EEFF; | |
| border: 1px solid #DDD6FE; | |
| color: #7C6FA8 !important; | |
| } | |
| html.dark .hero-links a:hover { | |
| background: rgba(124,92,252,0.18); | |
| border-color: #7C5CFC; | |
| color: #C4BFFF !important; | |
| } | |
| html:not(.dark) .hero-links a:hover { | |
| background: #EDE9FE; | |
| border-color: #7C5CFC; | |
| color: #5822B4 !important; | |
| } | |
| .hero-links .sep { | |
| color: #2A2A3C; | |
| font-size: 11px; | |
| user-select: none; | |
| } | |
| /* โโ Card groups (sidebar sections) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .pp-card { | |
| border-radius: 12px !important; | |
| padding: 10px 12px !important; | |
| } | |
| html.dark .pp-card { | |
| background: #1E293B !important; | |
| border: 1px solid #475569 !important; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important; | |
| } | |
| html:not(.dark) .pp-card { | |
| background: #FFFFFF !important; | |
| border: 1px solid #E5EBF0 !important; | |
| box-shadow: 0 2px 8px rgba(51,65,85,0.07) !important; | |
| } | |
| /* Tighten Gradio's own block padding inside cards */ | |
| .pp-card .block, .pp-card .wrap { padding-top: 2px !important; padding-bottom: 2px !important; } | |
| .pp-card .gap { gap: 6px !important; } | |
| .main-panel { | |
| margin-top: 60px; | |
| } | |
| .sidebar-panel .hero-subtitle { | |
| font-size: 13px !important; | |
| } | |
| .sidebar-panel .hero-links a, | |
| .sidebar-panel .hero-links .sep { | |
| font-size: 13px !important; | |
| } | |
| .sidebar-panel label, | |
| .sidebar-panel .block > label, | |
| .sidebar-panel .wrap > label, | |
| .sidebar-panel span.svelte-1gfkn6j, | |
| .sidebar-panel .gr-label { | |
| font-size: 12.5px !important; | |
| font-weight: 800 !important; | |
| } | |
| .sidebar-panel input, | |
| .sidebar-panel textarea, | |
| .sidebar-panel select, | |
| .sidebar-panel [data-testid="radio"] span, | |
| .sidebar-panel [data-testid="checkbox-group"] span, | |
| .sidebar-panel .status-box textarea, | |
| .sidebar-panel [data-testid="file"] { | |
| font-size: 13.5px !important; | |
| } | |
| .sidebar-panel [data-testid="radio"] label, | |
| .sidebar-panel [data-testid="radio"] span { | |
| font-weight: 700 !important; | |
| } | |
| html.dark .sidebar-panel [data-testid="radio"] span { | |
| color: #111111 !important; | |
| } | |
| html.dark .sidebar-panel [data-testid="radio"] input:checked + span { | |
| color: #FFFFFF !important; | |
| } | |
| html:not(.dark) .sidebar-panel [data-testid="radio"] span { | |
| color: #111111 !important; | |
| } | |
| html:not(.dark) .sidebar-panel [data-testid="radio"] input:checked + span { | |
| color: #FFFFFF !important; | |
| } | |
| .card-hdr { | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| font-size: 10px !important; | |
| font-weight: 800 !important; | |
| letter-spacing: 0.1em !important; | |
| text-transform: uppercase !important; | |
| margin-bottom: 8px !important; | |
| padding-bottom: 7px !important; | |
| } | |
| html.dark .card-hdr { color: #94A3B8 !important; border-bottom: 1px solid #475569 !important; } | |
| html:not(.dark) .card-hdr { color: #64748B !important; border-bottom: 1px solid #E5EBF0 !important; } | |
| html.dark .card-hdr svg { width:13px; height:13px; stroke:#94A3B8; fill:none; } | |
| html:not(.dark) .card-hdr svg { width:13px; height:13px; stroke:#64748B; fill:none; } | |
| /* โโ Labels (override Gradio uppercase labels to match design) โโโโโโโโโโ */ | |
| label, | |
| .block > label, | |
| .wrap > label, | |
| span.svelte-1gfkn6j, | |
| .gr-label { | |
| font-size: 10.5px !important; | |
| font-weight: 700 !important; | |
| text-transform: uppercase !important; | |
| letter-spacing: 0.07em !important; | |
| color: #8B8BA8 !important; | |
| } | |
| /* โโ Slider accent โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| input[type=range] { accent-color: #475569 !important; } | |
| input[type=range]::-webkit-slider-thumb { background: #475569 !important; } | |
| input[type=range]::-moz-range-thumb { background: #475569 !important; } | |
| /* โโ File upload (Gradio 6 targets) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .upload-container, | |
| [data-testid="file"], | |
| [data-testid="upload"], | |
| div[class*="fileupload"], | |
| div[class*="file-upload"], | |
| .file-preview, | |
| .upload-btn-wrapper { | |
| background: #141420 !important; | |
| border: 2px dashed #2A2A3C !important; | |
| border-radius: 12px !important; | |
| color: #6B6B8A !important; | |
| transition: border-color 0.2s, background 0.2s !important; | |
| } | |
| [data-testid="file"]:hover, | |
| [data-testid="upload"]:hover, | |
| div[class*="fileupload"]:hover, | |
| div[class*="file-upload"]:hover { | |
| border-color: #7C5CFC !important; | |
| background: rgba(124,92,252,0.05) !important; | |
| } | |
| /* upload icon color */ | |
| [data-testid="file"] svg, | |
| [data-testid="upload"] svg { stroke: #6B6B8A !important; } | |
| /* โโ Terminal composer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| #terminal-shell { | |
| display: flex !important; | |
| flex-direction: column !important; | |
| gap: 8px !important; | |
| } | |
| #terminal-input-wrap { order: 1; } | |
| #terminal-actions-wrap { order: 2; } | |
| #terminal-examples-wrap { order: 3; } | |
| #terminal-log-wrap { order: 4; } | |
| #terminal-examples-wrap { | |
| width: 100% !important; | |
| min-width: 0 !important; | |
| overflow: visible !important; | |
| } | |
| #terminal-shell.has-run #terminal-log-wrap { order: 1; } | |
| #terminal-shell.has-run #terminal-input-wrap { order: 2; } | |
| #terminal-shell.has-run #terminal-actions-wrap { order: 3; } | |
| #terminal-shell.has-run #terminal-examples-wrap { order: 4; } | |
| #terminal-shell.log-hidden #terminal-log-wrap { | |
| display: none !important; | |
| } | |
| #terminal-shell.log-collapsed .terminal-wrapper { | |
| height: 74px !important; | |
| overflow: hidden !important; | |
| } | |
| #terminal-shell.log-collapsed .terminal-body { | |
| padding-top: 8px; | |
| padding-bottom: 8px; | |
| } | |
| #terminal-shell.log-floating #terminal-log-wrap { | |
| position: fixed !important; | |
| top: 18px; | |
| right: 24px; | |
| width: min(980px, calc(100vw - 48px)); | |
| height: min(560px, calc(100vh - 36px)); | |
| min-width: 520px; | |
| min-height: 280px; | |
| max-width: calc(100vw - 32px); | |
| max-height: calc(100vh - 24px); | |
| overflow: auto !important; | |
| resize: both; | |
| z-index: 9999; | |
| } | |
| #terminal-shell.log-floating .terminal-wrapper { | |
| height: 100%; | |
| box-shadow: 0 18px 60px rgba(0,0,0,0.38); | |
| } | |
| #terminal-question-input textarea { | |
| min-height: 90px !important; | |
| max-height: 180px !important; | |
| font-size: 16px !important; | |
| line-height: 1.75 !important; | |
| padding: 14px 16px !important; | |
| border-radius: 10px !important; | |
| resize: vertical !important; | |
| font-family: 'Plus Jakarta Sans', sans-serif !important; | |
| } | |
| html.dark #terminal-question-input textarea { | |
| color: #E6E6F0 !important; | |
| background: #0D1117 !important; | |
| border: 1px solid #1E2030 !important; | |
| } | |
| html:not(.dark) #terminal-question-input textarea { | |
| color: #2D1A5E !important; | |
| background: #FFFFFF !important; | |
| border: 1px solid #DDD6FE !important; | |
| } | |
| html.dark #terminal-question-input textarea:focus { | |
| border-color: #B4AFF2 !important; | |
| box-shadow: 0 0 0 3px rgba(180,175,242,0.14) !important; | |
| } | |
| html:not(.dark) #terminal-question-input textarea:focus { | |
| border-color: #7C5CFC !important; | |
| box-shadow: 0 0 0 3px rgba(124,92,252,0.15) !important; | |
| } | |
| /* โโ Run / Clear / Stop buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .btn-run { | |
| height: 46px !important; | |
| font-size: 14px !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 0.02em !important; | |
| } | |
| .btn-clear { | |
| height: 46px !important; | |
| font-weight: 600 !important; | |
| background: rgba(124,92,252,0.12) !important; | |
| color: #6C56D9 !important; | |
| border: 1px solid rgba(180,175,242,0.28) !important; | |
| border-radius: 10px !important; | |
| } | |
| .btn-clear:hover { | |
| background: rgba(124,92,252,0.22) !important; | |
| border-color: rgba(180,175,242,0.5) !important; | |
| } | |
| .btn-stop { | |
| height: 46px !important; | |
| font-weight: 700 !important; | |
| background: rgba(239,68,68,0.13) !important; | |
| color: #F87171 !important; | |
| border: 1px solid rgba(239,68,68,0.35) !important; | |
| border-radius: 10px !important; | |
| } | |
| .btn-stop:hover { | |
| background: rgba(239,68,68,0.22) !important; | |
| border-color: rgba(239,68,68,0.55) !important; | |
| } | |
| /* โโ Section labels above answer / log โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .sec-label { | |
| font-size: 14px !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 0.04em !important; | |
| text-transform: uppercase !important; | |
| margin: 18px 0 8px !important; | |
| display: block; | |
| } | |
| html.dark .sec-label { color: #B4AFF2 !important; } | |
| html:not(.dark) .sec-label { color: #5822B4 !important; } | |
| /* โโ Execution log terminal โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .terminal-wrapper { | |
| border-radius: 12px; | |
| padding: 0; | |
| height: 525px; | |
| overflow-y: auto; | |
| font-family: 'Plus Jakarta Sans', sans-serif; | |
| font-size: 13.5px; | |
| line-height: 1.75; | |
| scroll-behavior: smooth; | |
| } | |
| .terminal-wrapper { background:#050505; border:1px solid #1A1A1A; } | |
| .terminal-chrome { | |
| position: sticky; | |
| top: 0; | |
| z-index: 2; | |
| display: flex; | |
| align-items: center; | |
| gap: 7px; | |
| height: 38px; | |
| padding: 0 14px; | |
| border-bottom: 1px solid transparent; | |
| backdrop-filter: blur(10px); | |
| cursor: default; | |
| } | |
| #terminal-shell.log-floating .terminal-chrome { | |
| cursor: move; | |
| } | |
| .terminal-controls { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 7px; | |
| line-height: 0; | |
| } | |
| .terminal-chrome { | |
| background: rgba(12,12,12,0.96); | |
| border-bottom-color: #1F1F1F; | |
| } | |
| .term-dot { | |
| width: 11px; | |
| height: 11px; | |
| border-radius: 999px; | |
| display: block; | |
| flex: 0 0 11px; | |
| border: 0; | |
| padding: 0; | |
| margin: 0; | |
| cursor: pointer; | |
| appearance: none; | |
| vertical-align: middle; | |
| } | |
| .term-dot.red { background: #FF5F57; } | |
| .term-dot.yellow { background: #FEBC2E; } | |
| .term-dot.green { background: #28C840; } | |
| .term-dot:hover { | |
| transform: scale(1.08); | |
| } | |
| .terminal-body { | |
| padding: 18px 18px 22px; | |
| } | |
| .term-stack { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .term-block { | |
| padding: 12px 14px; | |
| border-radius: 12px; | |
| border: 1px solid transparent; | |
| } | |
| .term-block { background: rgba(16,16,16,0.92); border-color: #202020; } | |
| .term-block.term-question, | |
| .term-block.term-answer { | |
| background: transparent !important; | |
| border: 0 !important; | |
| border-top: 2px solid #303030 !important; | |
| border-radius: 0 !important; | |
| padding-left: 12px !important; | |
| padding-right: 0 !important; | |
| } | |
| .term-head { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 8px; | |
| } | |
| .term-icon { | |
| width: 22px; | |
| height: 22px; | |
| border-radius: 999px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 12px; | |
| font-weight: 800; | |
| flex: 0 0 22px; | |
| } | |
| .term-block.is-active .term-icon { | |
| animation: termPulse 0.9s ease-in-out infinite; | |
| } | |
| @keyframes termPulse { | |
| 0%, 100% { opacity: 0.5; transform: scale(0.96); } | |
| 50% { opacity: 1; transform: scale(1.08); box-shadow: 0 0 0 6px rgba(124,92,252,0.10); } | |
| } | |
| .term-label { | |
| font-size: 11px; | |
| font-weight: 800; | |
| letter-spacing: 0.12em; | |
| text-transform: uppercase; | |
| } | |
| .term-title { | |
| font-size: 14px; | |
| font-weight: 700; | |
| } | |
| .term-meta { | |
| margin-left: auto; | |
| font-size: 11px; | |
| } | |
| .term-body { | |
| word-break: break-word; | |
| overflow-wrap: anywhere; | |
| font-size: 14px; | |
| line-height: 1.75; | |
| } | |
| .term-footer { | |
| display: flex; | |
| justify-content: flex-end; | |
| margin-top: 8px; | |
| font-size: 10px; | |
| } | |
| .term-question .term-body, | |
| .term-answer .term-body { | |
| font-size: 15px; | |
| } | |
| .term-think .term-body, | |
| .term-result .term-body { | |
| font-size: 14px; | |
| line-height: 1.68; | |
| } | |
| .term-think .term-title, | |
| .term-tool .term-title, | |
| .term-result .term-title { | |
| font-size: 15px; | |
| } | |
| .term-tool .term-body { | |
| font-size: 15px; | |
| line-height: 1.7; | |
| } | |
| .term-body > *:first-child { margin-top: 0; } | |
| .term-body > *:last-child { margin-bottom: 0; } | |
| .term-body p, | |
| .term-body li, | |
| .term-body blockquote { | |
| margin: 0 0 10px; | |
| } | |
| .term-body ul, | |
| .term-body ol { | |
| margin: 0 0 10px 20px; | |
| padding: 0; | |
| } | |
| .term-body pre { | |
| margin: 8px 0 10px; | |
| padding: 12px 14px; | |
| border-radius: 10px; | |
| overflow-x: auto; | |
| } | |
| .term-body code { | |
| padding: 2px 6px; | |
| border-radius: 6px; | |
| font-size: 0.93em; | |
| } | |
| .term-body pre code { | |
| padding: 0; | |
| background: transparent !important; | |
| } | |
| .term-body blockquote { | |
| padding-left: 12px; | |
| border-left: 2px solid #7C5CFC; | |
| } | |
| .term-body a { | |
| color: #7C5CFC; | |
| text-decoration: none; | |
| } | |
| .term-body a:hover { text-decoration: underline; } | |
| .term-body pre { background: #0A0A0A; border: 1px solid #232323; } | |
| .term-body code { background: rgba(255,255,255,0.08); color: #F5F5F5; } | |
| .term-divider { | |
| height: 1px; | |
| background: linear-gradient(90deg, rgba(255,255,255,0.32), rgba(255,255,255,0.06)); | |
| margin: 2px 0 6px; | |
| } | |
| .term-muted { color: #9A9A9A; } | |
| .term-text { color: #F5F5F5; } | |
| .term-think .term-body, | |
| .term-think .term-body.term-text, | |
| .term-think .term-body *, | |
| .term-think .term-body code { | |
| color: #CFCFCF !important; | |
| } | |
| .term-tool .term-body, | |
| .term-tool .term-body.term-text, | |
| .term-tool .term-body *, | |
| .term-tool .term-body code { | |
| color: #8EC5FF !important; | |
| } | |
| .term-result .term-body, | |
| .term-result .term-body.term-text, | |
| .term-result .term-body *, | |
| .term-result .term-body code { | |
| color: #F5E6A8 !important; | |
| } | |
| .term-answer .term-body, | |
| .term-answer .term-body.term-text, | |
| .term-answer .term-body *, | |
| .term-answer .term-body code, | |
| .term-assistant .term-body, | |
| .term-assistant .term-body.term-text, | |
| .term-assistant .term-body *, | |
| .term-assistant .term-body code { | |
| color: #FFFFFF !important; | |
| } | |
| .term-info .term-icon, | |
| .term-status .term-icon { background: rgba(255,255,255,.08); color: #F5F5F5; } | |
| .term-think .term-icon { background: rgba(255,255,255,.08); color: #EAEAEA; } | |
| .term-tool .term-icon { background: rgba(255,255,255,.08); color: #EAEAEA; } | |
| .term-result .term-icon { background: rgba(255,255,255,.08); color: #EAEAEA; } | |
| .term-question .term-icon { background: rgba(124,92,252,.18); color: #F5F3FF; } | |
| .term-answer .term-icon { background: rgba(255,255,255,.08); color: #FFFFFF; } | |
| .term-error .term-icon { background: rgba(255,255,255,.08); color: #FFFFFF; } | |
| /* โโ Status box โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .status-box textarea { | |
| font-family: 'JetBrains Mono', monospace !important; | |
| font-size: 11.5px !important; | |
| color: #6B6B8A !important; | |
| border-radius: 8px !important; | |
| min-height: unset !important; | |
| } | |
| /* โโ Examples 2-column grid โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .examples-section { | |
| margin-top: 28px; | |
| padding-top: 20px; | |
| border-top: 1px solid #2A2A3C; | |
| } | |
| .ex-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 10px; | |
| margin-top: 10px; | |
| padding-top: 4px; | |
| width: 100%; | |
| box-sizing: border-box; | |
| overflow: visible; | |
| } | |
| @media (max-width: 680px) { .ex-grid { grid-template-columns: 1fr; } } | |
| .ex-card { | |
| border-radius: 12px; | |
| padding: 14px 16px; | |
| width: 100%; | |
| min-width: 0; | |
| box-sizing: border-box; | |
| overflow: hidden; | |
| cursor: pointer; | |
| transition: border-color .18s, background .18s, transform .12s, box-shadow .18s; | |
| } | |
| html.dark .ex-card { background:#141420; border:1px solid #2A2A3C; } | |
| html:not(.dark) .ex-card { background:#FFFFFF; border:1px solid #DDD6FE; } | |
| html.dark .ex-card:hover { | |
| border-color: #7C5CFC; | |
| background: rgba(124,92,252,.06); | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 32px rgba(0,0,0,.5); | |
| } | |
| html:not(.dark) .ex-card:hover { | |
| border-color: #7C5CFC; | |
| background: #F5F3FF; | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 24px rgba(88,34,180,.12); | |
| } | |
| .ex-card:active { transform: translateY(0); } | |
| .ex-ds { | |
| display: inline-block; | |
| font-size: 9px; | |
| font-weight: 800; | |
| letter-spacing: .08em; | |
| text-transform: uppercase; | |
| padding: 2px 9px; | |
| border-radius: 4px; | |
| margin-bottom: 10px; | |
| } | |
| html.dark .ex-ds { background:rgba(88,34,180,.28); color:#B4AFF2; } | |
| html:not(.dark) .ex-ds { background:#EDE9FE; color:#5822B4; } | |
| .ex-q { | |
| font-size: 15.5px; | |
| line-height: 1.55; | |
| margin: 0 0 10px; | |
| font-family: 'Plus Jakarta Sans', sans-serif; | |
| } | |
| html.dark .ex-q { color:#DEE1F0; } | |
| html:not(.dark) .ex-q { color:#1E1B4B; } | |
| .ex-a { | |
| font-size: 11.5px; | |
| line-height: 1.5; | |
| margin: 0; | |
| padding-top: 10px; | |
| font-family: 'Plus Jakarta Sans', sans-serif; | |
| } | |
| html.dark .ex-a { color:#6B6B8A; border-top:1px solid #2A2A3C; } | |
| html:not(.dark) .ex-a { color:#7C6FA8; border-top:1px solid #EDE9FE; } | |
| .ex-a::before { content: "A ยท "; font-weight: 700; } | |
| html.dark .ex-a::before { color:#4E4E72; } | |
| html:not(.dark) .ex-a::before { color:#A89CC8; } | |
| .ex-g { | |
| font-size: 10.5px; | |
| line-height: 1.45; | |
| margin: 8px 0 0; | |
| word-break: break-word; | |
| overflow-wrap: anywhere; | |
| white-space: normal; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| html.dark .ex-g { color:#8B8BA8; } | |
| html:not(.dark) .ex-g { color:#64748B; } | |
| /* โโ Scrollbar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| ::-webkit-scrollbar { width: 5px; height: 5px; } | |
| ::-webkit-scrollbar-thumb { border-radius: 3px; } | |
| html.dark ::-webkit-scrollbar-track { background: #0A0A12; } | |
| html.dark ::-webkit-scrollbar-thumb { background: #1E2030; } | |
| html.dark ::-webkit-scrollbar-thumb:hover { background: #2A2A3C; } | |
| html:not(.dark) ::-webkit-scrollbar-track { background: #F5F3FF; } | |
| html:not(.dark) ::-webkit-scrollbar-thumb { background: #DDD6FE; } | |
| html:not(.dark) ::-webkit-scrollbar-thumb:hover { background: #C4B5FD; } | |
| /* โโ Dropdown popup list โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| html.dark ul[role="listbox"], | |
| html.dark [role="option"] { | |
| background: #141420 !important; | |
| border-color: #2A2A3C !important; | |
| color: #E6E6F0 !important; | |
| } | |
| html.dark [role="option"]:hover { background: #1A1A28 !important; } | |
| html:not(.dark) ul[role="listbox"], | |
| html:not(.dark) [role="option"] { | |
| background: #FFFFFF !important; | |
| border-color: #DDD6FE !important; | |
| color: #1E1B4B !important; | |
| } | |
| html:not(.dark) [role="option"]:hover { background: #F5F3FF !important; } | |
| /* โโ Nuke residual backgrounds โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| .tabs, .tabitem, .tab-nav, footer { background: transparent !important; } | |
| /* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| DARK MODE | |
| Key insight: Gradio wraps every component in a .block div with | |
| background #141420 that shows as a "lighter box" against the | |
| #0A0A12 page background. Fix = make ALL .block transparent by | |
| default; only pp-card siblings keep an explicit card background. | |
| โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */ | |
| /* CSS variable layer โ components that read vars get the right colour */ | |
| html.dark { | |
| --block-background-fill: transparent; | |
| --input-background-fill: #141420; | |
| --background-fill-primary: #141420; | |
| --background-fill-secondary: #1A1A28; | |
| --body-background-fill: #0A0A12; | |
| --border-color-primary: #2A2A3C; | |
| --block-border-color: transparent; | |
| --input-border-color: #2A2A3C; | |
| --body-text-color: #E6E6F0; | |
| --body-text-color-subdued: #8B8BA8; | |
| --input-placeholder-color: #6B6B8A; | |
| --shadow-drop: none; | |
| --shadow-drop-lg: none; | |
| --block-shadow: none; | |
| } | |
| /* All generic wrappers โ transparent (no extra box) */ | |
| html.dark .block, | |
| html.dark .wrap, | |
| html.dark .form, | |
| html.dark .gap, | |
| html.dark .contain, | |
| html.dark .padded, | |
| html.dark .compact, | |
| html.dark fieldset, | |
| html.dark .panel, | |
| html.dark .gr-box, | |
| html.dark .gr-form, | |
| html.dark .gr-group, | |
| html.dark .gradio-group, | |
| html.dark [data-testid="html"], | |
| html.dark [data-testid="html"] > *, | |
| html.dark [data-testid="markdown"], | |
| html.dark [data-testid="markdown"] > * { | |
| background: transparent !important; | |
| border-color: transparent !important; | |
| box-shadow: none !important; | |
| } | |
| /* pp-card gets its own visible card surface */ | |
| html.dark .pp-card { | |
| background: #141420 !important; | |
| border: 1px solid #2A2A3C !important; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important; | |
| } | |
| /* inner wrappers inside a pp-card inherit the card bg */ | |
| html.dark .pp-card .block, | |
| html.dark .pp-card .wrap, | |
| html.dark .pp-card fieldset, | |
| html.dark .pp-card .form { | |
| background: #141420 !important; | |
| border-color: #2A2A3C !important; | |
| box-shadow: none !important; | |
| } | |
| /* Actual input / textarea elements */ | |
| html.dark input, | |
| html.dark textarea, | |
| html.dark select { | |
| background: #141420 !important; | |
| color: #E6E6F0 !important; | |
| border-color: #2A2A3C !important; | |
| } | |
| html.dark input:focus, | |
| html.dark textarea:focus { | |
| border-color: #B4AFF2 !important; | |
| box-shadow: 0 0 0 3px rgba(180,175,242,0.14) !important; | |
| outline: none !important; | |
| } | |
| /* Radio / checkbox spans */ | |
| html.dark .wrap span, | |
| html.dark [data-testid="radio"] span, | |
| html.dark [data-testid="checkbox-group"] span { color: #E6E6F0 !important; } | |
| html.dark [data-testid="radio"] input + span, | |
| html.dark [data-testid="checkbox-group"] input + span { | |
| background: #141420 !important; | |
| border-color: #2A2A3C !important; | |
| } | |
| /* Labels */ | |
| html.dark .block > label > span, | |
| html.dark .wrap > label > span, | |
| html.dark label > span.text-sm { color: #8B8BA8 !important; } | |
| """ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Global JavaScript | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| GLOBAL_JS = """ | |
| /* Gradio 6: js param is executed as a plain code string on page load. | |
| Do NOT wrap in an arrow function โ it won't be called. */ | |
| /* fillQuestion: populate the terminal composer from an example card click. | |
| Tries multiple selectors for robustness across Gradio 6 DOM layouts. */ | |
| window.fillQuestion = function(text) { | |
| var el = ( | |
| document.querySelector('#terminal-question-input textarea') || | |
| document.querySelector('textarea[placeholder*="search question"]') || | |
| document.querySelector('textarea[placeholder*="Enter your"]') || | |
| document.querySelector('textarea[placeholder*="question here"]') | |
| ); | |
| if (!el) { console.warn('fillQuestion: textarea not found'); return; } | |
| var setter = Object.getOwnPropertyDescriptor( | |
| window.HTMLTextAreaElement.prototype, 'value' | |
| ).set; | |
| setter.call(el, text); | |
| el.dispatchEvent(new Event('input', { bubbles: true })); | |
| el.focus(); | |
| el.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| }; | |
| window.syncTerminalShellState = function() { | |
| var shell = document.querySelector('#terminal-shell'); | |
| var terminal = document.querySelector('.terminal-wrapper'); | |
| var wrap = document.querySelector('#terminal-log-wrap'); | |
| if (!shell || !terminal) return; | |
| var text = (terminal.textContent || '').trim(); | |
| var isIdle = !text || text.indexOf('Waiting for run') !== -1; | |
| shell.classList.toggle('has-run', !isIdle); | |
| var lastState = shell.dataset.runState || 'idle'; | |
| if (!isIdle && lastState !== 'active') { | |
| shell.classList.remove('log-hidden', 'log-collapsed', 'log-floating'); | |
| if (wrap) { | |
| wrap.style.left = ''; | |
| wrap.style.top = ''; | |
| wrap.style.right = ''; | |
| wrap.style.width = ''; | |
| wrap.style.height = ''; | |
| delete wrap.dataset.floatingInit; | |
| } | |
| } | |
| shell.dataset.runState = isIdle ? 'idle' : 'active'; | |
| }; | |
| window.resetTerminalLogState = function() { | |
| var shell = document.querySelector('#terminal-shell'); | |
| var wrap = document.querySelector('#terminal-log-wrap'); | |
| if (!shell) return; | |
| shell.classList.remove('log-hidden', 'log-collapsed', 'log-floating'); | |
| if (wrap) { | |
| wrap.style.left = ''; | |
| wrap.style.top = ''; | |
| wrap.style.right = ''; | |
| wrap.style.width = ''; | |
| wrap.style.height = ''; | |
| delete wrap.dataset.floatingInit; | |
| } | |
| }; | |
| window.terminalLogControl = function(action) { | |
| var shell = document.querySelector('#terminal-shell'); | |
| var wrap = document.querySelector('#terminal-log-wrap'); | |
| if (!shell) return; | |
| if (action === 'hide') { | |
| var stopBtn = document.querySelector('.btn-stop button') || document.querySelector('.btn-stop'); | |
| if (stopBtn && typeof stopBtn.click === 'function') { | |
| stopBtn.click(); | |
| } | |
| shell.classList.remove('log-collapsed', 'log-floating'); | |
| shell.classList.add('log-hidden'); | |
| if (wrap) { | |
| wrap.style.left = ''; | |
| wrap.style.top = ''; | |
| wrap.style.right = ''; | |
| wrap.style.width = ''; | |
| wrap.style.height = ''; | |
| } | |
| return; | |
| } | |
| shell.classList.remove('log-hidden'); | |
| if (action === 'collapse') { | |
| if (shell.classList.contains('log-collapsed')) { | |
| shell.classList.remove('log-collapsed'); | |
| return; | |
| } | |
| shell.classList.remove('log-floating'); | |
| shell.classList.add('log-collapsed'); | |
| if (wrap) { | |
| wrap.style.left = ''; | |
| wrap.style.top = ''; | |
| wrap.style.right = ''; | |
| wrap.style.width = ''; | |
| wrap.style.height = ''; | |
| delete wrap.dataset.floatingInit; | |
| } | |
| return; | |
| } | |
| if (action === 'float') { | |
| if (shell.classList.contains('log-floating')) { | |
| shell.classList.remove('log-floating'); | |
| if (wrap) { | |
| wrap.style.left = ''; | |
| wrap.style.top = ''; | |
| wrap.style.right = ''; | |
| wrap.style.width = ''; | |
| wrap.style.height = ''; | |
| delete wrap.dataset.floatingInit; | |
| } | |
| return; | |
| } | |
| shell.classList.remove('log-collapsed'); | |
| shell.classList.add('log-floating'); | |
| if (wrap && !wrap.dataset.floatingInit) { | |
| wrap.style.right = '24px'; | |
| wrap.style.top = '18px'; | |
| wrap.dataset.floatingInit = '1'; | |
| } | |
| } | |
| }; | |
| window.__terminalDrag = { active: false, offsetX: 0, offsetY: 0, target: null }; | |
| document.addEventListener('mousedown', function(e) { | |
| var shell = document.querySelector('#terminal-shell'); | |
| if (!shell || !shell.classList.contains('log-floating')) return; | |
| var chrome = e.target && e.target.closest ? e.target.closest('.terminal-chrome') : null; | |
| var wrap = document.querySelector('#terminal-log-wrap'); | |
| if (!chrome || !wrap) return; | |
| if (e.target && e.target.closest && e.target.closest('.term-dot')) return; | |
| var rect = wrap.getBoundingClientRect(); | |
| window.__terminalDrag = { | |
| active: true, | |
| offsetX: e.clientX - rect.left, | |
| offsetY: e.clientY - rect.top, | |
| target: wrap | |
| }; | |
| wrap.style.right = 'auto'; | |
| wrap.style.left = rect.left + 'px'; | |
| wrap.style.top = rect.top + 'px'; | |
| wrap.style.width = rect.width + 'px'; | |
| wrap.style.height = rect.height + 'px'; | |
| document.body.style.userSelect = 'none'; | |
| }); | |
| document.addEventListener('mousemove', function(e) { | |
| var drag = window.__terminalDrag; | |
| if (!drag || !drag.active || !drag.target) return; | |
| var maxLeft = Math.max(0, window.innerWidth - drag.target.offsetWidth); | |
| var maxTop = Math.max(0, window.innerHeight - drag.target.offsetHeight); | |
| var left = Math.min(Math.max(0, e.clientX - drag.offsetX), maxLeft); | |
| var top = Math.min(Math.max(0, e.clientY - drag.offsetY), maxTop); | |
| drag.target.style.left = left + 'px'; | |
| drag.target.style.top = top + 'px'; | |
| }); | |
| document.addEventListener('mouseup', function() { | |
| if (window.__terminalDrag) { | |
| window.__terminalDrag.active = false; | |
| window.__terminalDrag.target = null; | |
| } | |
| document.body.style.userSelect = ''; | |
| }); | |
| document.addEventListener('click', function(e) { | |
| var runBtn = e.target && e.target.closest ? e.target.closest('.btn-run') : null; | |
| if (runBtn) { | |
| window.resetTerminalLogState(); | |
| } | |
| }); | |
| document.addEventListener('keydown', function(e) { | |
| var target = e.target; | |
| if (!target || !target.matches || !target.matches('#terminal-question-input textarea')) return; | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| window.resetTerminalLogState(); | |
| } | |
| }); | |
| /* Auto-scroll terminal whenever new log lines arrive */ | |
| new MutationObserver(function() { | |
| var t = document.querySelector('.terminal-wrapper'); | |
| if (t) t.scrollTop = t.scrollHeight; | |
| window.syncTerminalShellState(); | |
| }).observe(document.body, { childList: true, subtree: true }); | |
| window.syncTerminalShellState(); | |
| """ | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Example data | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| EXAMPLES_DATA: List[Dict[str, str]] = [ | |
| { | |
| "dataset": "Animal Science", | |
| "corpus": "biology", | |
| "question": "According to the documents about animal handedness, what types of animals are mentioned as being kept as pets? Cite the file path where you found this.", | |
| "answer": "Parrots, dogs, cats, and rabbits are mentioned as commonly kept pets.", | |
| "gold_doc": "biology/animals_handedness_Animal_8_3.txt.txt", | |
| }, | |
| { | |
| "dataset": "Biology", | |
| "corpus": "biology", | |
| "question": "What bacterium is mentioned as being used as a pesticide in the biology documents? Provide the specific name.", | |
| "answer": "Bacillus thuringiensis (Bt) โ a Gram-positive, soil-dwelling bacterium.", | |
| "gold_doc": "biology/bacterium_infect_another_Bacteria_13_2.txt.txt", | |
| }, | |
| { | |
| "dataset": "Earth Science", | |
| "corpus": "earth_science", | |
| "question": "According to the earth science documents, what annual rainfall range is given for a tropical savanna?", | |
| "answer": "Between 750 millimetres and 1,270 millimetres per year.", | |
| "gold_doc": "earth_science/arid_area_Earth_rainfall_climatology4_10.txt.txt", | |
| }, | |
| { | |
| "dataset": "Earth Science", | |
| "corpus": "earth_science", | |
| "question": "According to the drifting guide in the earth science corpus, which drivetrain is particularly good for drifting?", | |
| "answer": "Rear-wheel drive.", | |
| "gold_doc": "earth_science/continental_drift_what_is_drifting_guide1_7.txt.txt", | |
| }, | |
| { | |
| "dataset": "Economics", | |
| "corpus": "economics", | |
| "question": "According to the economics documents, what major event is described as a turning point after which fertility mostly continued to fall?", | |
| "answer": "The Great Recession.", | |
| "gold_doc": "economics/uspopulationgrowth_thelongtermdeclineinfertilityandwhatitmeansforstatebudgets_76.txt.txt", | |
| }, | |
| { | |
| "dataset": "Robotics", | |
| "corpus": "robotics", | |
| "question": "In the robotics documents, which message type is converted to Ackermann inputs?", | |
| "answer": "geometry_msgs/Twist.", | |
| "gold_doc": "robotics/ackermann_interfacecontrolchec_53.txt.txt", | |
| }, | |
| { | |
| "dataset": "Robotics", | |
| "corpus": "robotics", | |
| "question": "According to the robotics documents, which launch file combines tf-broadcaster and pcl2-spammer data into an octomap?", | |
| "answer": "octomap_mapping.launch.", | |
| "gold_doc": "robotics/octomap_publish_4NI0GL435o_171.txt.txt", | |
| }, | |
| ] | |
| def _build_examples_html() -> str: | |
| cards: List[str] = [] | |
| for ex in EXAMPLES_DATA: | |
| q_attr = _html.escape(ex["question"], quote=True) | |
| q_disp = _html.escape(ex["question"]) | |
| a_disp = _html.escape(ex.get("answer", "")) | |
| d_disp = _html.escape(ex["dataset"]) | |
| gold_doc = ex.get("gold_doc", "").replace(".txt.txt", ".txt") | |
| g_disp = _html.escape(gold_doc) | |
| cards.append( | |
| f'<div class="ex-card" data-q="{q_attr}" onclick="fillQuestion(this.dataset.q)">' | |
| f'<span class="ex-ds">{d_disp}</span>' | |
| f'<p class="ex-q">{q_disp}</p>' | |
| f'<p class="ex-a">{a_disp}</p>' | |
| f'<p class="ex-g">Gold doc: {g_disp}</p>' | |
| f"</div>" | |
| ) | |
| return f'<div class="ex-grid">{"".join(cards)}</div>' | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Corpus helpers | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _ts() -> str: | |
| return datetime.now().strftime("%H:%M:%S") | |
| def _example_corpus_map() -> Dict[str, str]: | |
| return { | |
| str(item["question"]): str(item["corpus"]) | |
| for item in EXAMPLES_DATA | |
| if item.get("corpus") | |
| } | |
| def _selected_example_corpus(question: str) -> Optional[str]: | |
| return _example_corpus_map().get(question.strip()) | |
| def _has_text_default_corpus() -> bool: | |
| return DEFAULT_CORPUS_DIR.exists() and any(DEFAULT_CORPUS_DIR.glob("*/*.txt")) | |
| def _decode_text_bytes(data: bytes) -> str: | |
| if not data: | |
| return "" | |
| if from_bytes is not None: | |
| best = from_bytes(data).best() | |
| if best is not None: | |
| return str(best) | |
| for encoding in ("utf-8", "utf-8-sig", "utf-16", "utf-16-le", "utf-16-be", "gb18030", "big5", "shift_jis"): | |
| try: | |
| return data.decode(encoding) | |
| except UnicodeDecodeError: | |
| continue | |
| return data.decode("utf-8", errors="replace") | |
| def _extract_pdf_text(pdf_path: Path) -> str: | |
| if PdfReader is None: | |
| raise RuntimeError("PDF parsing support is unavailable because pypdf is not installed.") | |
| reader = PdfReader(str(pdf_path)) | |
| pages: List[str] = [] | |
| for idx, page in enumerate(reader.pages, start=1): | |
| text = page.extract_text() or "" | |
| cleaned = text.strip() | |
| if cleaned: | |
| pages.append(f"[Page {idx}]\n{cleaned}") | |
| return "\n\n".join(pages).strip() | |
| def _normalize_corpus_files(corpus_root: Path) -> None: | |
| for file_path in corpus_root.rglob("*"): | |
| if not file_path.is_file(): | |
| continue | |
| suffix = file_path.suffix.lower() | |
| if suffix == ".pdf": | |
| try: | |
| extracted = _extract_pdf_text(file_path) | |
| except Exception: | |
| continue | |
| if extracted: | |
| output_path = file_path.with_name(file_path.name + ".txt") | |
| output_path.write_text(extracted, encoding="utf-8") | |
| continue | |
| if suffix not in TEXT_FILE_SUFFIXES: | |
| continue | |
| try: | |
| raw = file_path.read_bytes() | |
| decoded = _decode_text_bytes(raw) | |
| file_path.write_text(decoded, encoding="utf-8") | |
| except Exception: | |
| continue | |
| def _resolve_example_corpus(corpus_dir: Path, question: str) -> Path: | |
| domain = _selected_example_corpus(question) | |
| if not domain: | |
| return corpus_dir | |
| candidate = corpus_dir / domain | |
| return candidate if candidate.exists() else corpus_dir | |
| def _cleanup_runtime_state(runtime_state: Optional[Dict[str, Any]]) -> Dict[str, Any]: | |
| state = dict(runtime_state or {}) | |
| corpus_root = state.get("corpus_root") | |
| session_dir = state.get("session_dir") | |
| for path_value in (corpus_root, session_dir): | |
| if not path_value: | |
| continue | |
| try: | |
| path = Path(path_value) | |
| if path.exists(): | |
| shutil.rmtree(path, ignore_errors=True) | |
| except Exception: | |
| pass | |
| return {} | |
| def _uploaded_corpus_signature(uploaded_value: Any) -> str: | |
| if uploaded_value is None: | |
| return "" | |
| if isinstance(uploaded_value, (list, tuple)): | |
| items = [str(item) for item in uploaded_value if item] | |
| return json.dumps(sorted(items), ensure_ascii=False) | |
| return str(uploaded_value) | |
| def _select_uploaded_corpus(corpus_source: str, single_file: Any, folder_files: Any) -> Any: | |
| if corpus_source == UPLOAD_FOLDER_LABEL and folder_files not in (None, "", []): | |
| return folder_files | |
| if corpus_source == UPLOAD_FILE_LABEL and single_file not in (None, "", []): | |
| return single_file | |
| return None | |
| def _copy_uploaded_corpus_files(work_dir: Path, uploaded_files: List[str]) -> None: | |
| files = [Path(path) for path in uploaded_files if path and os.path.exists(path)] | |
| if not files: | |
| raise ValueError("No corpus files were uploaded.") | |
| total = sum(path.stat().st_size for path in files) | |
| if total > 25 * 1024 * 1024: | |
| raise ValueError(f"Uploaded files total {total/1024/1024:.1f} MB โ max 25 MB.") | |
| common_root: Optional[Path] = None | |
| try: | |
| common_root = Path(os.path.commonpath([str(path.parent) for path in files])) | |
| except Exception: | |
| common_root = None | |
| target_root = work_dir / "corpus" | |
| target_root.mkdir(parents=True, exist_ok=True) | |
| for file_path in files: | |
| size = file_path.stat().st_size | |
| if size > 5 * 1024 * 1024: | |
| raise ValueError(f"'{file_path.name}' {size/1024/1024:.1f} MB โ max 5 MB/file.") | |
| rel_path: Path | |
| if common_root is not None: | |
| try: | |
| rel_path = file_path.relative_to(common_root) | |
| except ValueError: | |
| rel_path = Path(file_path.name) | |
| else: | |
| rel_path = Path(file_path.name) | |
| destination = target_root / rel_path | |
| destination.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(file_path, destination) | |
| def _ensure_corpus_ready( | |
| use_default: bool, | |
| uploaded_corpus: Any, | |
| selected_domain: Optional[str] = None, | |
| ) -> Path: | |
| work_dir = Path(tempfile.mkdtemp(prefix="pi_corpus_")) | |
| if use_default: | |
| if _has_text_default_corpus(): | |
| text_root = DEFAULT_CORPUS_DIR | |
| source_root = text_root / selected_domain if selected_domain and (text_root / selected_domain).exists() else text_root | |
| shutil.copytree(source_root, work_dir / "corpus", dirs_exist_ok=True) | |
| return work_dir / "corpus" | |
| raise ValueError("Default bright corpus is unavailable.") | |
| if isinstance(uploaded_corpus, (list, tuple)): | |
| _copy_uploaded_corpus_files(work_dir, [str(item) for item in uploaded_corpus if item]) | |
| elif uploaded_corpus and os.path.exists(str(uploaded_corpus)): | |
| uploaded_path = str(uploaded_corpus) | |
| if uploaded_path.lower().endswith(".zip"): | |
| size = os.path.getsize(uploaded_path) | |
| if size > 25 * 1024 * 1024: | |
| raise ValueError(f"ZIP is {size/1024/1024:.1f} MB โ max 25 MB.") | |
| with zipfile.ZipFile(uploaded_path) as zf: | |
| total = sum(i.file_size for i in zf.infolist()) | |
| if total > 25 * 1024 * 1024: | |
| raise ValueError(f"Uncompressed {total/1024/1024:.1f} MB โ max 25 MB.") | |
| for info in zf.infolist(): | |
| if info.file_size > 5 * 1024 * 1024 and not info.is_dir(): | |
| raise ValueError(f"'{info.filename}' {info.file_size/1024/1024:.1f} MB โ max 5 MB/file.") | |
| zf.extractall(work_dir / "corpus") | |
| else: | |
| _copy_uploaded_corpus_files(work_dir, [uploaded_path]) | |
| else: | |
| raise ValueError("No corpus available. Upload a folder or enable 'Use Default Corpus'.") | |
| _normalize_corpus_files(work_dir / "corpus") | |
| return work_dir / "corpus" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Terminal HTML rendering | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _esc(v: Any) -> str: | |
| return _html.escape(str(v)) | |
| def _markdown_to_html(text: str) -> str: | |
| safe_text = str(text or "") | |
| try: | |
| import markdown # type: ignore | |
| return markdown.markdown( | |
| safe_text, | |
| extensions=["fenced_code", "tables", "nl2br", "sane_lists"], | |
| ) | |
| except Exception: | |
| try: | |
| import markdown2 # type: ignore | |
| return markdown2.markdown( | |
| safe_text, | |
| extras=["fenced-code-blocks", "tables", "break-on-newline"], | |
| ) | |
| except Exception: | |
| return _simple_markdown_to_html(safe_text) | |
| def _inline_markdown(text: str) -> str: | |
| escaped = _esc(text) | |
| escaped = re.sub(r"`([^`]+)`", r"<code>\1</code>", escaped) | |
| escaped = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", escaped) | |
| escaped = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", escaped) | |
| return escaped | |
| def _simple_markdown_to_html(text: str) -> str: | |
| lines = text.splitlines() | |
| html_parts: List[str] = [] | |
| in_list = False | |
| in_code = False | |
| code_lines: List[str] = [] | |
| paragraph: List[str] = [] | |
| def flush_paragraph() -> None: | |
| nonlocal paragraph | |
| if paragraph: | |
| html_parts.append(f"<p>{'<br>'.join(_inline_markdown(line) for line in paragraph)}</p>") | |
| paragraph = [] | |
| def flush_list() -> None: | |
| nonlocal in_list | |
| if in_list: | |
| html_parts.append("</ul>") | |
| in_list = False | |
| def flush_code() -> None: | |
| nonlocal in_code, code_lines | |
| if in_code: | |
| html_parts.append(f"<pre><code>{_esc(chr(10).join(code_lines))}</code></pre>") | |
| code_lines = [] | |
| in_code = False | |
| for line in lines: | |
| stripped = line.rstrip() | |
| if stripped.startswith("```"): | |
| flush_paragraph() | |
| flush_list() | |
| if in_code: | |
| flush_code() | |
| else: | |
| in_code = True | |
| continue | |
| if in_code: | |
| code_lines.append(line) | |
| continue | |
| if not stripped: | |
| flush_paragraph() | |
| flush_list() | |
| continue | |
| heading_match = re.match(r"^(#{1,6})\s+(.*)$", stripped) | |
| if heading_match: | |
| flush_paragraph() | |
| flush_list() | |
| level = len(heading_match.group(1)) | |
| html_parts.append(f"<h{level}>{_inline_markdown(heading_match.group(2))}</h{level}>") | |
| continue | |
| list_match = re.match(r"^[-*]\s+(.*)$", stripped) | |
| if list_match: | |
| flush_paragraph() | |
| if not in_list: | |
| html_parts.append("<ul>") | |
| in_list = True | |
| html_parts.append(f"<li>{_inline_markdown(list_match.group(1))}</li>") | |
| continue | |
| flush_list() | |
| paragraph.append(stripped) | |
| flush_paragraph() | |
| flush_list() | |
| flush_code() | |
| return "".join(html_parts) | |
| def _make_block( | |
| kind: str, | |
| *, | |
| label: str, | |
| title: str = "", | |
| body: str = "", | |
| meta: str = "", | |
| footer: str = "", | |
| icon: str = "", | |
| active: bool = False, | |
| ) -> Dict[str, Any]: | |
| return { | |
| "kind": kind, | |
| "label": label, | |
| "title": title, | |
| "body": body, | |
| "meta": meta, | |
| "footer": footer, | |
| "icon": icon, | |
| "active": active, | |
| } | |
| def _append_block(state: Dict[str, Any], block: Dict[str, Any]) -> int: | |
| state["blocks"].append(block) | |
| return len(state["blocks"]) - 1 | |
| def _insert_block(state: Dict[str, Any], idx: int, block: Dict[str, Any]) -> int: | |
| state["blocks"].insert(idx, block) | |
| for key, value in list(state.items()): | |
| if key in {"blocks"}: | |
| continue | |
| if isinstance(value, int) and value >= idx: | |
| state[key] = value + 1 | |
| return idx | |
| def _close_current(state: Dict[str, Any], key: str) -> None: | |
| idx = state.get(key) | |
| if idx is None: | |
| return | |
| if 0 <= idx < len(state["blocks"]): | |
| state["blocks"][idx]["active"] = False | |
| state[key] = None | |
| def _ensure_block( | |
| state: Dict[str, Any], | |
| key: str, | |
| *, | |
| kind: str, | |
| label: str, | |
| title: str, | |
| icon: str, | |
| ) -> Dict[str, Any]: | |
| idx = state.get(key) | |
| if idx is None: | |
| idx = _append_block(state, _make_block(kind, label=label, title=title, icon=icon, active=True)) | |
| state[key] = idx | |
| block = state["blocks"][idx] | |
| block["kind"] = kind | |
| block["label"] = label | |
| block["title"] = title | |
| block["icon"] = icon | |
| block["active"] = True | |
| return block | |
| def _tool_message(tool_name: str, args: Any = None, raw_arguments: str = "") -> str: | |
| parsed = args | |
| if not parsed and raw_arguments: | |
| try: | |
| parsed = json.loads(raw_arguments) | |
| except Exception: | |
| parsed = None | |
| if tool_name == "bash": | |
| if isinstance(parsed, dict) and parsed.get("command"): | |
| return str(parsed["command"]) | |
| return raw_arguments.strip() or "bash" | |
| if tool_name == "read": | |
| if isinstance(parsed, dict): | |
| file_path = parsed.get("file_path") or parsed.get("path") | |
| offset = parsed.get("offset") | |
| limit = parsed.get("limit") | |
| parts: List[str] = ["read"] | |
| if file_path: | |
| parts.append(str(file_path)) | |
| if offset not in (None, ""): | |
| parts.append(f"--offset {offset}") | |
| if limit not in (None, ""): | |
| parts.append(f"--limit {limit}") | |
| if len(parts) > 1: | |
| return " ".join(parts) | |
| return raw_arguments.strip() or "read" | |
| if isinstance(parsed, dict) and parsed: | |
| return "\n".join(f"{k}: {v}" for k, v in parsed.items()) | |
| return raw_arguments.strip() or tool_name | |
| def _tool_result_text(value: Any) -> str: | |
| if value is None: | |
| return "" | |
| if isinstance(value, str): | |
| stripped = value.strip() | |
| if stripped.startswith("{") or stripped.startswith("["): | |
| try: | |
| parsed = json.loads(stripped) | |
| except Exception: | |
| return value | |
| extracted = _tool_result_text(parsed) | |
| return extracted if extracted else value | |
| return value | |
| if isinstance(value, list): | |
| parts = [_tool_result_text(item) for item in value] | |
| return "\n".join(part for part in parts if part).strip() | |
| if isinstance(value, dict): | |
| content = value.get("content") | |
| if isinstance(content, list): | |
| parts: List[str] = [] | |
| for item in content: | |
| if isinstance(item, dict): | |
| if isinstance(item.get("text"), str): | |
| parts.append(item["text"]) | |
| elif item.get("type") == "text" and isinstance(item.get("content"), str): | |
| parts.append(item["content"]) | |
| else: | |
| nested = _tool_result_text(item) | |
| if nested: | |
| parts.append(nested) | |
| else: | |
| nested = _tool_result_text(item) | |
| if nested: | |
| parts.append(nested) | |
| joined = "\n".join(part for part in parts if part).strip() | |
| if joined: | |
| return joined | |
| for key in ("text", "stdout", "stderr", "output", "result", "message", "content"): | |
| extracted = _tool_result_text(value.get(key)) | |
| if extracted: | |
| return extracted | |
| fragments: List[str] = [] | |
| for nested in value.values(): | |
| extracted = _tool_result_text(nested) | |
| if extracted: | |
| fragments.append(extracted) | |
| return "\n".join(fragments).strip() | |
| return str(value) | |
| def _extract_thinking_text(event: Dict[str, Any]) -> str: | |
| direct = str(event.get("thinking") or "").strip() | |
| if direct: | |
| return direct | |
| message = event.get("message") or {} | |
| content = message.get("content") if isinstance(message, dict) else None | |
| if isinstance(content, list): | |
| parts: List[str] = [] | |
| for item in content: | |
| if not isinstance(item, dict): | |
| continue | |
| if item.get("type") == "thinking" and item.get("thinking"): | |
| parts.append(str(item.get("thinking"))) | |
| return "\n\n".join(part for part in parts if part).strip() | |
| return "" | |
| def _render_block(block: Dict[str, Any]) -> str: | |
| classes = f'term-block term-{block["kind"]}' | |
| if block.get("active"): | |
| classes += " is-active" | |
| head = ( | |
| f'<div class="term-head">' | |
| f'<span class="term-icon">{_esc(block.get("icon") or block["label"][:1])}</span>' | |
| f'<span class="term-label term-muted">{_esc(block["label"])}</span>' | |
| f'<span class="term-title term-text">{_esc(block.get("title", ""))}</span>' | |
| f'<span class="term-meta term-muted">{_esc(block.get("meta", ""))}</span>' | |
| f'</div>' | |
| ) | |
| body_value = block.get("body", "") or "" | |
| if not body_value: | |
| body = "" | |
| else: | |
| body_html = _markdown_to_html(body_value) if block["kind"] in {"think", "result", "answer", "assistant"} else _esc(body_value).replace("\n", "<br>\n") | |
| body = f'<div class="term-body term-text">{body_html}</div>' | |
| footer_value = block.get("footer", "") or "" | |
| footer = f'<div class="term-footer term-muted">{_esc(footer_value)}</div>' if footer_value else "" | |
| return f'<div class="{classes}">{head}{body}{footer}</div>' | |
| def _render_terminal(blocks: List[Dict[str, Any]]) -> str: | |
| rendered: List[str] = [] | |
| for block in blocks: | |
| if block["kind"] == "divider": | |
| rendered.append('<div class="term-divider"></div>') | |
| else: | |
| rendered.append(_render_block(block)) | |
| chrome = ( | |
| '<div class="terminal-chrome">' | |
| '<div class="terminal-controls">' | |
| '<button type="button" class="term-dot red" onclick="window.terminalLogControl(\'hide\')" aria-label="Hide log"></button>' | |
| '<button type="button" class="term-dot yellow" onclick="window.terminalLogControl(\'collapse\')" aria-label="Collapse log"></button>' | |
| '<button type="button" class="term-dot green" onclick="window.terminalLogControl(\'float\')" aria-label="Float log"></button>' | |
| '</div>' | |
| '</div>' | |
| ) | |
| return f'<div class="terminal-wrapper">{chrome}<div class="terminal-body"><div class="term-stack">{"".join(rendered)}</div></div></div>' | |
| def _initial_terminal_state(question: str, cwd: Path, mode: str, model: str, max_turns: int, selected_domain: Optional[str]) -> Dict[str, Any]: | |
| state: Dict[str, Any] = { | |
| "blocks": [], | |
| "current_think": None, | |
| "current_tool": None, | |
| "current_result": None, | |
| "current_assistant": None, | |
| "working_dir_block": None, | |
| "config_block": None, | |
| "pending_tool_name": "", | |
| "pending_tool_args": "", | |
| "thinking_started_at": None, | |
| "thinking_streamed_this_turn": False, | |
| "tool_started_at": {}, | |
| "run_started_at": time.perf_counter(), | |
| "finalized": False, | |
| } | |
| state["working_dir_block"] = _append_block(state, _make_block("info", label="Info", title="Working Dir", body=str(cwd), meta=_ts(), icon="i")) | |
| state["config_block"] = _append_block( | |
| state, | |
| _make_block( | |
| "info", | |
| label="Info", | |
| title="Run config", | |
| body=( | |
| "Runner: Native PI\n" | |
| "Thinking level: unknown\n" | |
| "Provider: openai\n" | |
| f"Model: {model}\n" | |
| f"Max turns: {max_turns}\n" | |
| f"Domain: {selected_domain or 'all'}" | |
| ), | |
| meta=_ts(), | |
| icon="i", | |
| ), | |
| ) | |
| _append_block(state, _make_block("question", label="Question", title="User question", body=question.strip(), meta=_ts(), icon="?")) | |
| _append_block(state, _make_block("divider", label="", icon="")) | |
| return state | |
| def _render_terminal_state(state: Dict[str, Any]) -> str: | |
| return _render_terminal(state["blocks"]) | |
| def _update_run_config(state: Dict[str, Any], *, runner: str, thinking_level: str, provider: str, model: str, max_turns: int, domain: Optional[str]) -> None: | |
| idx = state.get("config_block") | |
| if idx is None or idx >= len(state["blocks"]): | |
| return | |
| state["blocks"][idx]["body"] = ( | |
| f"Runner: {runner}\n" | |
| f"Thinking level: {thinking_level}\n" | |
| f"Provider: {provider}\n" | |
| f"Model: {model}\n" | |
| f"Max turns: {max_turns}\n" | |
| f"Domain: {domain or 'all'}" | |
| ) | |
| def _prepare_terminal_state( | |
| terminal_state: Optional[Dict[str, Any]], | |
| *, | |
| question: str, | |
| cwd: Path, | |
| mode: str, | |
| model: str, | |
| max_turns: int, | |
| selected_domain: Optional[str], | |
| ) -> Dict[str, Any]: | |
| if not terminal_state: | |
| return _initial_terminal_state(question, cwd, mode, model, max_turns, selected_domain) | |
| state = dict(terminal_state) | |
| state["blocks"] = [dict(block) for block in terminal_state.get("blocks", [])] | |
| for key in ("current_think", "current_tool", "current_result", "current_assistant"): | |
| _close_current(state, key) | |
| state["pending_tool_name"] = "" | |
| state["pending_tool_args"] = "" | |
| state["thinking_started_at"] = None | |
| state["thinking_streamed_this_turn"] = False | |
| state["tool_started_at"] = {} | |
| state["run_started_at"] = time.perf_counter() | |
| state["finalized"] = False | |
| working_idx = state.get("working_dir_block") | |
| if isinstance(working_idx, int) and 0 <= working_idx < len(state["blocks"]): | |
| state["blocks"][working_idx]["body"] = str(cwd) | |
| state["blocks"][working_idx]["meta"] = _ts() | |
| _update_run_config( | |
| state, | |
| runner="Native PI", | |
| thinking_level="unknown", | |
| provider="openai", | |
| model=model, | |
| max_turns=max_turns, | |
| domain=selected_domain, | |
| ) | |
| config_idx = state.get("config_block") | |
| if isinstance(config_idx, int) and 0 <= config_idx < len(state["blocks"]): | |
| state["blocks"][config_idx]["meta"] = _ts() | |
| _append_block(state, _make_block("divider", label="", icon="")) | |
| _append_block(state, _make_block("question", label="Question", title="User question", body=question.strip(), meta=_ts(), icon="?")) | |
| _append_block(state, _make_block("divider", label="", icon="")) | |
| return state | |
| def _run_button_label(runtime_state: Optional[Dict[str, Any]]) -> str: | |
| runtime = dict(runtime_state or {}) | |
| if runtime.get("session_file") or runtime.get("session_id"): | |
| return "โถ Continue" | |
| return "โถ Start deep research now" | |
| def _finalize_answer_block(state: Dict[str, Any], final_text: str) -> None: | |
| final_text = final_text.strip() or "(empty answer)" | |
| idx = state.get("current_assistant") | |
| if idx is None: | |
| for pos in range(len(state["blocks"]) - 1, -1, -1): | |
| if state["blocks"][pos]["kind"] in {"assistant", "answer"}: | |
| idx = pos | |
| break | |
| if idx is None: | |
| idx = _append_block( | |
| state, | |
| _make_block("answer", label="Answer", title="Final answer", body=final_text, meta=_ts(), icon="A"), | |
| ) | |
| block = state["blocks"][idx] | |
| block["kind"] = "answer" | |
| block["label"] = "Answer" | |
| block["title"] = "Final answer" | |
| block["icon"] = "A" | |
| if final_text: | |
| block["body"] = final_text | |
| block["meta"] = _ts() | |
| block["active"] = False | |
| state["current_assistant"] = None | |
| def _apply_event(state: Dict[str, Any], event: Dict[str, Any]) -> bool: | |
| et = event.get("type", "") | |
| if et == "turn_start": | |
| state["thinking_streamed_this_turn"] = False | |
| return False | |
| if et in {"message_start", "message_end", "agent_start", "provider_request_context", "response"}: | |
| return False | |
| if et == "turn_end": | |
| thinking = _extract_thinking_text(event) | |
| if not thinking: | |
| return False | |
| if state.get("thinking_streamed_this_turn"): | |
| return False | |
| response_idx = None | |
| for pos in range(len(state["blocks"]) - 1, -1, -1): | |
| if state["blocks"][pos]["kind"] in {"assistant", "answer"}: | |
| response_idx = pos | |
| break | |
| if response_idx is not None and state.get("current_think") is None: | |
| idx = _insert_block( | |
| state, | |
| response_idx, | |
| _make_block("think", label="Thinking", title="Reasoning", icon="T", active=True), | |
| ) | |
| state["current_think"] = idx | |
| block = _ensure_block(state, "current_think", kind="think", label="Thinking", title="Reasoning", icon="T") | |
| block["body"] += thinking | |
| if state.get("thinking_started_at") is not None: | |
| block["meta"] = f"{time.perf_counter() - state['thinking_started_at']:.1f}s" | |
| state["thinking_started_at"] = None | |
| _close_current(state, "current_think") | |
| return True | |
| if et == "message_update": | |
| ame = event.get("assistantMessageEvent", {}) or {} | |
| mt = ame.get("type") | |
| if mt == "thinking_start": | |
| _ensure_block(state, "current_think", kind="think", label="Thinking", title="Reasoning", icon="T") | |
| state["thinking_started_at"] = time.perf_counter() | |
| state["thinking_streamed_this_turn"] = True | |
| return True | |
| if mt == "thinking_delta": | |
| block = _ensure_block(state, "current_think", kind="think", label="Thinking", title="Reasoning", icon="T") | |
| block["body"] += ame.get("delta", "") | |
| state["thinking_streamed_this_turn"] = True | |
| return True | |
| if mt == "thinking_end": | |
| block = _ensure_block(state, "current_think", kind="think", label="Thinking", title="Reasoning", icon="T") | |
| if not block["body"] and ame.get("thinking"): | |
| block["body"] = ame.get("thinking", "") | |
| state["thinking_streamed_this_turn"] = True | |
| if state.get("thinking_started_at") is not None: | |
| block["meta"] = f"{time.perf_counter() - state['thinking_started_at']:.1f}s" | |
| state["thinking_started_at"] = None | |
| _close_current(state, "current_think") | |
| return True | |
| if mt == "toolcall_start": | |
| state["pending_tool_name"] = "" | |
| state["pending_tool_args"] = "" | |
| return False | |
| if mt == "toolcall_delta": | |
| state["pending_tool_args"] += ame.get("delta", "") | |
| return False | |
| if mt == "toolcall_end": | |
| tool_call = ame.get("toolCall", {}) or {} | |
| state["pending_tool_name"] = str(tool_call.get("name") or "tool") | |
| state["pending_tool_args"] = str(tool_call.get("arguments") or state.get("pending_tool_args", "")) | |
| return False | |
| if mt == "text_start": | |
| _ensure_block(state, "current_assistant", kind="assistant", label="Answer", title="Drafting response", icon="A") | |
| return True | |
| if mt == "text_delta": | |
| block = _ensure_block(state, "current_assistant", kind="assistant", label="Answer", title="Drafting response", icon="A") | |
| block["body"] += ame.get("delta", "") | |
| return True | |
| if mt == "text_end": | |
| block = _ensure_block(state, "current_assistant", kind="assistant", label="Answer", title="Drafting response", icon="A") | |
| if ame.get("content") and len(str(ame["content"])) >= len(block["body"]): | |
| block["body"] = str(ame["content"]) | |
| _close_current(state, "current_assistant") | |
| return True | |
| if mt in {"done", "error"}: | |
| _close_current(state, "current_assistant") | |
| _close_current(state, "current_think") | |
| _close_current(state, "current_tool") | |
| state["pending_tool_name"] = "" | |
| state["pending_tool_args"] = "" | |
| state["thinking_started_at"] = None | |
| return True | |
| return False | |
| if et == "tool_execution_start": | |
| tool_name = str(event.get("toolName") or "tool") | |
| args = event.get("args") or {} | |
| tool_call_id = str(event.get("toolCallId") or "") | |
| pending_args = str(state.get("pending_tool_args") or "") | |
| _append_block( | |
| state, | |
| _make_block( | |
| "tool", | |
| label="Tool", | |
| title=tool_name, | |
| body=_tool_message(tool_name, args=args, raw_arguments=pending_args), | |
| meta=_ts(), | |
| icon=">", | |
| ), | |
| ) | |
| if tool_call_id: | |
| state["tool_started_at"][tool_call_id] = time.perf_counter() | |
| state["pending_tool_name"] = "" | |
| state["pending_tool_args"] = "" | |
| _ensure_block(state, "current_result", kind="result", label="Result", title=tool_name, icon="#") | |
| return True | |
| if et == "tool_execution_update": | |
| output = event.get("output") or event.get("partialOutput") or event.get("delta") or "" | |
| if not output: | |
| return False | |
| tool_name = str(event.get("toolName") or "tool") | |
| block = _ensure_block(state, "current_result", kind="result", label="Result", title=tool_name, icon="#") | |
| block["body"] += _tool_result_text(output) | |
| return True | |
| if et == "tool_execution_end": | |
| tool_name = str(event.get("toolName") or "tool") | |
| result = _tool_result_text(event.get("result")) | |
| tool_call_id = str(event.get("toolCallId") or "") | |
| block = _ensure_block(state, "current_result", kind="result", label="Result", title=tool_name, icon="#") | |
| if result and len(result) >= len(block["body"]): | |
| block["body"] = result | |
| if tool_call_id: | |
| started = state["tool_started_at"].pop(tool_call_id, None) | |
| if started is not None: | |
| block["meta"] = _ts() | |
| block["footer"] = f"{time.perf_counter() - started:.1f}s" | |
| elif not block.get("meta"): | |
| block["meta"] = _ts() | |
| _close_current(state, "current_result") | |
| if event.get("isError"): | |
| block["kind"] = "error" | |
| block["label"] = "Error" | |
| return True | |
| if et == "agent_end": | |
| _close_current(state, "current_think") | |
| _close_current(state, "current_tool") | |
| _close_current(state, "current_result") | |
| state["thinking_started_at"] = None | |
| state["pending_tool_name"] = "" | |
| state["pending_tool_args"] = "" | |
| state["tool_started_at"].clear() | |
| note = str(event.get("note") or "").strip() | |
| if note: | |
| _append_block(state, _make_block("status", label="Status", title="Agent finished", body=note, meta=_ts(), icon="=")) | |
| return True | |
| return False | |
| if et == "error": | |
| _close_current(state, "current_think") | |
| _close_current(state, "current_tool") | |
| _close_current(state, "current_result") | |
| _close_current(state, "current_assistant") | |
| state["thinking_started_at"] = None | |
| state["pending_tool_name"] = "" | |
| state["pending_tool_args"] = "" | |
| state["tool_started_at"].clear() | |
| _append_block( | |
| state, | |
| _make_block("error", label="Error", title="Run error", body=str(event.get("error", "?")), meta=_ts(), icon="!"), | |
| ) | |
| return True | |
| if et == "__final__": | |
| if state.get("finalized"): | |
| return False | |
| _finalize_answer_block(state, str(event.get("text", ""))) | |
| elapsed = time.perf_counter() - state["run_started_at"] | |
| _append_block( | |
| state, | |
| _make_block( | |
| "status", | |
| label="Status", | |
| title="Run time", | |
| body=f"{elapsed:.1f} second", | |
| meta=_ts(), | |
| icon="=", | |
| ), | |
| ) | |
| state["finalized"] = True | |
| return True | |
| return False | |
| _TERM_IDLE = _render_terminal( | |
| [ | |
| _make_block("info", label="Info", title="Execution log", body="Waiting for run...", meta=_ts(), icon="i"), | |
| ] | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Prompt builders | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def build_benchmark_prompt(query: str, corpus_dir: Path) -> str: | |
| return ( | |
| "Answer the following question. " | |
| f"The answer is contained in the corpus directory at @{corpus_dir}. " | |
| "**Do Not use web search!** Use ripgrep (rg) instead of grep.\n\n" | |
| f"QUESTION:\n{query}\n" | |
| ) | |
| def build_ir_prompt(query: str, corpus_dir: Path) -> str: | |
| return ( | |
| f"You are a careful research assistant. Answer using ONLY documents in @{corpus_dir}.\n" | |
| "Do not use online search or any external tools beyond Grep and Bash.\n\n" | |
| f"Question:\n{query}\n\n" | |
| "SEARCH STRATEGY:\n" | |
| "1. Use Grep/Bash ONLY โ no Agent tool, no subagents, no web.\n" | |
| "2. Run multiple searches IN PARALLEL per response.\n" | |
| "3. Diverse, targeted keywords; exhaust all angles.\n\n" | |
| "RETRIEVAL:\n" | |
| "- Maximize both recall and precision (NDCG scoring).\n" | |
| "- Include every relevant document; exclude every irrelevant one.\n\n" | |
| "OUTPUT FORMAT:\n" | |
| "Relevant Documents (ranked; max 20):\n" | |
| "1. {corpus}/path/doc1.txt\n\n" | |
| "Explanation: {reasoning}\n" | |
| "Exact Answer: {answer}\n" | |
| "Confidence: {0โ100%}\n" | |
| ) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Main search handler | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def run_search( | |
| api_key: str, | |
| question: str, | |
| model: str, | |
| max_turns: int, | |
| mode: str, | |
| corpus_source: str, | |
| uploaded_corpus: Any, | |
| runtime_state: Optional[Dict[str, Any]], | |
| ) -> Generator[tuple[str, str, Dict[str, Any]], None, None]: | |
| """Yields (terminal_html, status_str, runtime_state, run_btn_update).""" | |
| if not api_key or not api_key.strip(): | |
| runtime = dict(runtime_state or {}) | |
| yield _TERM_IDLE, "โ OpenAI API Key is required.", runtime, gr.update(value=_run_button_label(runtime)) | |
| return | |
| if not question or not question.strip(): | |
| runtime = dict(runtime_state or {}) | |
| yield _TERM_IDLE, "โ Please enter a question.", runtime, gr.update(value=_run_button_label(runtime)) | |
| return | |
| runtime = dict(runtime_state or {}) | |
| use_default_corpus = corpus_source == DEFAULT_CORPUS_LABEL | |
| selected_domain = _selected_example_corpus(question) if use_default_corpus else None | |
| stored_domain = str(runtime.get("selected_domain") or "").strip() or None | |
| if use_default_corpus and selected_domain and selected_domain != stored_domain: | |
| runtime = _cleanup_runtime_state(runtime) | |
| corpus_key = json.dumps( | |
| { | |
| "source": corpus_source, | |
| "uploaded_corpus": _uploaded_corpus_signature(uploaded_corpus), | |
| }, | |
| ensure_ascii=False, | |
| sort_keys=True, | |
| ) | |
| corpus_root_value = runtime.get("corpus_root") | |
| corpus_root = Path(corpus_root_value) if corpus_root_value else None | |
| if runtime.get("corpus_key") != corpus_key or corpus_root is None or not corpus_root.exists(): | |
| runtime = _cleanup_runtime_state(runtime) | |
| try: | |
| corpus_root = _ensure_corpus_ready(use_default_corpus, uploaded_corpus, selected_domain) | |
| except ValueError as exc: | |
| yield _TERM_IDLE, f"โ {exc}", runtime, gr.update(value=_run_button_label(runtime)) | |
| return | |
| except Exception as exc: | |
| yield _TERM_IDLE, f"โ Corpus error: {exc}", runtime, gr.update(value=_run_button_label(runtime)) | |
| return | |
| session_dir = Path(tempfile.mkdtemp(prefix="pi_session_")) | |
| runtime.update( | |
| { | |
| "corpus_key": corpus_key, | |
| "corpus_root": str(corpus_root), | |
| "session_dir": str(session_dir), | |
| "session_file": "", | |
| "session_id": "", | |
| "selected_domain": selected_domain or "", | |
| "session_cwd": str(_resolve_example_corpus(corpus_root, question) if use_default_corpus else corpus_root), | |
| } | |
| ) | |
| assert corpus_root is not None | |
| active_domain = str(runtime.get("selected_domain") or "").strip() or None | |
| session_cwd_value = runtime.get("session_cwd") | |
| cwd = Path(session_cwd_value) if session_cwd_value else (_resolve_example_corpus(corpus_root, question) if use_default_corpus else corpus_root) | |
| prompt = build_ir_prompt(question, cwd) if mode == "IR" else build_benchmark_prompt(question, cwd) | |
| state = _prepare_terminal_state( | |
| runtime.get("terminal_state"), | |
| question=question, | |
| cwd=cwd, | |
| mode=mode, | |
| model=model, | |
| max_turns=max_turns, | |
| selected_domain=active_domain, | |
| ) | |
| runtime["terminal_state"] = state | |
| yield _render_terminal_state(state), "โ Runningโฆ", runtime, gr.update(value=_run_button_label(runtime)) | |
| try: | |
| for event in run_pi_stream( | |
| prompt, | |
| cwd=cwd, | |
| api_key=api_key.strip(), | |
| provider="openai", | |
| model=model, | |
| max_turns=max_turns, | |
| session_dir=Path(runtime["session_dir"]) if runtime.get("session_dir") else None, | |
| session_path=Path(runtime["session_file"]) if runtime.get("session_file") else None, | |
| continue_session=bool(runtime.get("session_file") or runtime.get("session_id")), | |
| ): | |
| if event.get("type") == "__session__": | |
| if event.get("sessionFile"): | |
| runtime["session_file"] = str(event["sessionFile"]) | |
| if event.get("sessionId"): | |
| runtime["session_id"] = str(event["sessionId"]) | |
| _update_run_config( | |
| state, | |
| runner=str(event.get("runner") or "Native PI"), | |
| thinking_level=str(event.get("thinkingLevel") or "unknown"), | |
| provider=str(event.get("provider") or "openai"), | |
| model=str(event.get("model") or model), | |
| max_turns=max_turns, | |
| domain=active_domain, | |
| ) | |
| runtime["terminal_state"] = state | |
| yield _render_terminal_state(state), "โ Runningโฆ", runtime, gr.update(value=_run_button_label(runtime)) | |
| continue | |
| if _apply_event(state, event): | |
| status = "โ Completed" if event.get("type") == "__final__" else "โ Runningโฆ" | |
| runtime["terminal_state"] = state | |
| yield _render_terminal_state(state), status, runtime, gr.update(value=_run_button_label(runtime)) | |
| except Exception as exc: | |
| _apply_event(state, {"type": "error", "error": str(exc)}) | |
| runtime["terminal_state"] = state | |
| yield _render_terminal_state(state), f"โ {exc}", runtime, gr.update(value=_run_button_label(runtime)) | |
| return | |
| if not state.get("finalized"): | |
| _apply_event(state, {"type": "__final__", "text": ""}) | |
| runtime["terminal_state"] = state | |
| yield _render_terminal_state(state), "โ Completed", runtime, gr.update(value=_run_button_label(runtime)) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Gradio UI | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def build_ui() -> gr.Blocks: | |
| hero_logo_src = f"/gradio_api/file={HERO_LOGO_PATH}" | |
| with gr.Blocks(title="DCI-Agent Search") as demo: | |
| runtime_state = gr.State({}) | |
| with gr.Row(equal_height=False): | |
| # โโ Left sidebar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(scale=1, min_width=270, elem_classes=["sidebar-panel"]): | |
| gr.HTML(f""" | |
| <div class="hero-wrap"> | |
| <div class="hero-title-row"> | |
| <img class="hero-logo" src="{hero_logo_src}" alt="DCI-Agent logo" /> | |
| <span class="hero-title">DCI-Agent Search</span> | |
| </div> | |
| <span class="hero-subtitle"> | |
| Deep Research on your own knowledge base | |
| </span> | |
| <div class="hero-links"> | |
| <a href="https://huggingface.co/papers/2605.05242" title="Hugging Face Paper" target="_blank" rel="noopener noreferrer"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg> | |
| HF Paper | |
| </a> | |
| <a href="https://github.com/DCI-Agent/DCI-Agent-Lite" title="GitHub" target="_blank" rel="noopener noreferrer"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/></svg> | |
| GitHub | |
| </a> | |
| <a href="https://x.com/zhuofengli96475/status/2052784645398303198" title="X (Twitter)" target="_blank" rel="noopener noreferrer"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.747l7.73-8.835L1.254 2.25H8.08l4.253 5.622 5.911-5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> | |
| X (Twitter) | |
| </a> | |
| <a href="#" title="Slack" target="_blank" rel="noopener noreferrer"> | |
| <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 10c-.83 0-1.5-.67-1.5-1.5v-5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5z"/><path d="M20.5 10H19V8.5c0-.83.67-1.5 1.5-1.5s1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/><path d="M9.5 14c.83 0 1.5.67 1.5 1.5v5c0 .83-.67 1.5-1.5 1.5S8 21.33 8 20.5v-5c0-.83.67-1.5 1.5-1.5z"/><path d="M3.5 14H5v1.5c0 .83-.67 1.5-1.5 1.5S2 16.33 2 15.5 2.67 14 3.5 14z"/><path d="M14 14.5c0-.83.67-1.5 1.5-1.5h5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5h-5c-.83 0-1.5-.67-1.5-1.5z"/><path d="M15.5 19H14v1.5c0 .83.67 1.5 1.5 1.5s1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z"/><path d="M10 9.5C10 8.67 9.33 8 8.5 8h-5C2.67 8 2 8.67 2 9.5S2.67 11 3.5 11h5c.83 0 1.5-.67 1.5-1.5z"/><path d="M8.5 5H10V3.5C10 2.67 9.33 2 8.5 2S7 2.67 7 3.5 7.67 5 8.5 5z"/></svg> | |
| Slack | |
| </a> | |
| </div> | |
| </div> | |
| """) | |
| # Card 1: Configuration | |
| with gr.Group(elem_classes=["pp-card"]): | |
| _env_key = os.environ.get("OPENAI_API_KEY", "") | |
| if not _env_key: | |
| gr.HTML('<div style="color:#FC8181;font-size:11px">โ OPENAI_API_KEY not configured</div>') | |
| api_key = gr.Textbox( | |
| label="OpenAI API Key", | |
| value=_env_key, | |
| visible=False, | |
| ) | |
| model = gr.Dropdown( | |
| choices=[ | |
| ("GPT-5.4 nano", "gpt-5.4-nano"), | |
| ], | |
| value="gpt-5.4-nano", | |
| label="Model", | |
| allow_custom_value=False, | |
| filterable=False, | |
| ) | |
| max_turns = gr.Slider( | |
| minimum=1, maximum=50, step=1, value=20, | |
| label="Max Turns", | |
| ) | |
| # Mode (QA by default, hidden) | |
| mode = gr.Dropdown( | |
| choices=["QA"], | |
| value="QA", | |
| visible=False, | |
| ) | |
| # Card 3: Corpus | |
| with gr.Group(elem_classes=["pp-card"]): | |
| corpus_source = gr.Radio( | |
| choices=[DEFAULT_CORPUS_LABEL, UPLOAD_FILE_LABEL, UPLOAD_FOLDER_LABEL], | |
| value=DEFAULT_CORPUS_LABEL, | |
| label="Corpus source", | |
| ) | |
| with gr.Column(visible=False) as upload_file_wrap: | |
| uploaded_single_file = gr.File( | |
| label="file", | |
| file_count="single", | |
| ) | |
| with gr.Column(visible=False) as upload_folder_wrap: | |
| uploaded_folder = gr.File( | |
| label="folder", | |
| file_count="directory", | |
| ) | |
| corpus_hint = gr.HTML( | |
| value='<div style="font-size:10.5px;color:#4E4E72;line-height:1.9;margin-top:8px">' | |
| 'Use single file for one document ยท Max 5 MB per file' | |
| '</div>', | |
| visible=False, | |
| ) | |
| # Status | |
| status_text = gr.Textbox( | |
| label="Status", | |
| value="Ready โ", | |
| interactive=False, | |
| elem_classes=["status-box"], | |
| ) | |
| # โโ Right main panel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(scale=3, elem_classes=["main-panel"]): | |
| with gr.Column(elem_id="terminal-shell"): | |
| with gr.Column(elem_id="terminal-input-wrap"): | |
| terminal_question = gr.Textbox( | |
| label="", | |
| placeholder="In our demo, we use the BRIGHT corpus, where all questions are tied to the provided documents", | |
| lines=3, | |
| elem_id="terminal-question-input", | |
| ) | |
| with gr.Row(elem_id="terminal-actions-wrap"): | |
| run_btn = gr.Button( | |
| "โถ Start deep research now", | |
| variant="primary", | |
| elem_classes=["btn-run"], | |
| scale=3, | |
| ) | |
| stop_btn = gr.Button( | |
| "โ Stop", | |
| variant="secondary", | |
| elem_classes=["btn-stop"], | |
| scale=1, | |
| ) | |
| clear_btn = gr.Button( | |
| "New", | |
| variant="secondary", | |
| elem_classes=["btn-clear"], | |
| scale=1, | |
| ) | |
| with gr.Column(elem_id="terminal-examples-wrap"): | |
| gr.HTML(f""" | |
| <div style="margin-top:22px"> | |
| <span class="sec-label"> | |
| Example Questions | |
| <span style="font-weight:400;text-transform:none;letter-spacing:0;color:#4E4E72;font-size:10.5px"> | |
| โ click to fill | |
| </span> | |
| </span> | |
| {_build_examples_html()} | |
| </div> | |
| """) | |
| with gr.Column(elem_id="terminal-log-wrap"): | |
| terminal = gr.HTML(value=_TERM_IDLE) | |
| # โโ Wiring โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def _toggle_upload(source: str): | |
| show_file = source == UPLOAD_FILE_LABEL | |
| show_folder = source == UPLOAD_FOLDER_LABEL | |
| show_hint = show_file or show_folder | |
| if show_file: | |
| hint_html = ( | |
| '<div style="font-size:10.5px;color:#4E4E72;line-height:1.9;margin-top:8px">' | |
| 'Use single file for one document ยท Max 5 MB per file' | |
| '</div>' | |
| ) | |
| elif show_folder: | |
| hint_html = ( | |
| '<div style="font-size:10.5px;color:#4E4E72;line-height:1.9;margin-top:8px">' | |
| 'Use folder for a corpus ยท Max 25 MB total ยท 5 MB per file' | |
| '</div>' | |
| ) | |
| else: | |
| hint_html = "" | |
| return ( | |
| gr.update(visible=show_file), | |
| gr.update(visible=show_folder), | |
| gr.update(visible=show_hint, value=hint_html), | |
| ) | |
| corpus_source.change( | |
| fn=_toggle_upload, | |
| inputs=[corpus_source], | |
| outputs=[upload_file_wrap, upload_folder_wrap, corpus_hint], | |
| ) | |
| corpus_source.select( | |
| fn=_toggle_upload, | |
| inputs=[corpus_source], | |
| outputs=[upload_file_wrap, upload_folder_wrap, corpus_hint], | |
| ) | |
| def clear_all(runtime_state): | |
| cleared_runtime = _cleanup_runtime_state(runtime_state) | |
| return ( | |
| _TERM_IDLE, "", "Ready โ", cleared_runtime, gr.update(value=_run_button_label(cleared_runtime)), | |
| ) | |
| clear_btn.click( | |
| fn=clear_all, | |
| inputs=[runtime_state], | |
| outputs=[terminal, terminal_question, status_text, runtime_state, run_btn], | |
| ) | |
| def _run_with_qa_mode(api_key, question, model, max_turns, corpus_source, uploaded_single_file, uploaded_folder, runtime_state): | |
| uploaded_corpus = _select_uploaded_corpus(corpus_source, uploaded_single_file, uploaded_folder) | |
| yield from run_search(api_key, question, model, max_turns, "QA", corpus_source, uploaded_corpus, runtime_state) | |
| run_event = run_btn.click( | |
| fn=_run_with_qa_mode, | |
| inputs=[api_key, terminal_question, model, max_turns, corpus_source, uploaded_single_file, uploaded_folder, runtime_state], | |
| outputs=[terminal, status_text, runtime_state, run_btn], | |
| ) | |
| terminal_question.submit( | |
| fn=_run_with_qa_mode, | |
| inputs=[api_key, terminal_question, model, max_turns, corpus_source, uploaded_single_file, uploaded_folder, runtime_state], | |
| outputs=[terminal, status_text, runtime_state, run_btn], | |
| ) | |
| stop_btn.click(fn=None, cancels=[run_event]) | |
| return demo | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| # Entry point | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if __name__ == "__main__": | |
| demo = build_ui() | |
| demo.queue(default_concurrency_limit=3) | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=int(os.environ.get("PORT", 7860)), | |
| share=False, | |
| theme=PP_THEME, | |
| css=CUSTOM_CSS, | |
| js=GLOBAL_JS, | |
| allowed_paths=[str(APP_DIR / "img")], | |
| ) | |