Spaces:
Running
Running
Commit ·
d52caba
1
Parent(s): 1ed0433
Rewrite explorer with Docker SDK, streaming, citations, custom UI
Browse filesSwitch from Gradio SDK to Docker SDK with custom HTML frontend.
Add streaming answers, citation processing (from harness), source
list, trace toggle, file upload with drag-and-drop, session cost
tracking. Shared chat-ui.js/css fetched at build time from
harness-apps-shared repo. Pin harness to acfd4f2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Dockerfile +20 -0
- README.md +1 -4
- app.py +413 -225
- requirements.txt +1 -1
- static/.gitkeep +1 -0
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
RUN apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*
|
| 4 |
+
|
| 5 |
+
RUN useradd -m -u 1000 user
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt "gradio>=5.12,<6"
|
| 10 |
+
|
| 11 |
+
RUN mkdir -p /app/static && \
|
| 12 |
+
curl -sL https://raw.githubusercontent.com/chuckfinca/harness-apps-shared/main/chat-ui.js -o /app/static/chat-ui.js && \
|
| 13 |
+
curl -sL https://raw.githubusercontent.com/chuckfinca/harness-apps-shared/main/chat-ui.css -o /app/static/chat-ui.css
|
| 14 |
+
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
USER user
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -3,11 +3,8 @@ title: Document Explorer
|
|
| 3 |
emoji: 📄
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: green
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version: "6.9.0"
|
| 8 |
-
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
-
python_version: "3.12"
|
| 11 |
license: mit
|
| 12 |
---
|
| 13 |
|
|
|
|
| 3 |
emoji: 📄
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: green
|
| 6 |
+
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
|
|
|
| 8 |
license: mit
|
| 9 |
---
|
| 10 |
|
app.py
CHANGED
|
@@ -1,10 +1,7 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
Requires LH_MODEL and LH_ACCESS_TOKEN in .env or environment.
|
| 7 |
-
Uses E2B sandboxes for code execution (no Docker required).
|
| 8 |
"""
|
| 9 |
|
| 10 |
from __future__ import annotations
|
|
@@ -13,6 +10,7 @@ import json
|
|
| 13 |
import os
|
| 14 |
import tempfile
|
| 15 |
import time
|
|
|
|
| 16 |
from dataclasses import asdict
|
| 17 |
from datetime import datetime, timezone
|
| 18 |
from pathlib import Path
|
|
@@ -23,10 +21,11 @@ from dotenv import load_dotenv
|
|
| 23 |
from huggingface_hub import HfApi
|
| 24 |
|
| 25 |
from llm_harness.agent import run_agent_loop
|
|
|
|
| 26 |
from llm_harness.prompt import build_system_prompt
|
| 27 |
from llm_harness.tools import TOOL_DEFINITIONS
|
| 28 |
from llm_harness.trace_viewer import render_trace
|
| 29 |
-
from llm_harness.types import Message, ToolCallEvent, ToolResultEvent
|
| 30 |
|
| 31 |
from sandbox_e2b import run_python as e2b_run_python
|
| 32 |
|
|
@@ -40,18 +39,25 @@ MAX_SESSION_COST = float(os.environ.get("LH_MAX_SESSION_COST", "0.50"))
|
|
| 40 |
HF_TRACES_REPO = os.environ.get("HF_TRACES_REPO", "")
|
| 41 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 42 |
|
|
|
|
|
|
|
| 43 |
hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN else None
|
| 44 |
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
def _slugify(text: str, max_len: int = 50) -> str:
|
| 47 |
slug = text.lower().replace(" ", "-")
|
| 48 |
slug = "".join(c for c in slug if c.isalnum() or c == "-")
|
| 49 |
return slug[:max_len].rstrip("-")
|
| 50 |
|
| 51 |
|
| 52 |
-
def upload_trace(result: dict) ->
|
| 53 |
if not hf_api or not HF_TRACES_REPO:
|
| 54 |
-
return
|
| 55 |
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
|
| 56 |
question_slug = _slugify(result.get("question", ""))
|
| 57 |
filename = f"{timestamp}_{question_slug}.json" if question_slug else f"{timestamp}.json"
|
|
@@ -63,36 +69,8 @@ def upload_trace(result: dict) -> str | None:
|
|
| 63 |
repo_id=HF_TRACES_REPO,
|
| 64 |
repo_type="dataset",
|
| 65 |
)
|
| 66 |
-
return filename
|
| 67 |
except Exception as exc:
|
| 68 |
print(f"WARNING: trace upload failed: {exc}")
|
| 69 |
-
return None
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def list_traces() -> list[str]:
|
| 73 |
-
if not hf_api or not HF_TRACES_REPO:
|
| 74 |
-
return []
|
| 75 |
-
try:
|
| 76 |
-
files = hf_api.list_repo_files(HF_TRACES_REPO, repo_type="dataset")
|
| 77 |
-
traces = sorted(
|
| 78 |
-
[f for f in files if f.endswith(".json")],
|
| 79 |
-
reverse=True,
|
| 80 |
-
)
|
| 81 |
-
return traces
|
| 82 |
-
except Exception:
|
| 83 |
-
return []
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def fetch_trace(filename: str) -> dict | None:
|
| 87 |
-
if not hf_api or not HF_TRACES_REPO or not filename:
|
| 88 |
-
return None
|
| 89 |
-
try:
|
| 90 |
-
path = hf_api.hf_hub_download(
|
| 91 |
-
HF_TRACES_REPO, filename, repo_type="dataset"
|
| 92 |
-
)
|
| 93 |
-
return json.loads(Path(path).read_text())
|
| 94 |
-
except Exception:
|
| 95 |
-
return None
|
| 96 |
|
| 97 |
|
| 98 |
def save_uploaded_files(files: list[str]) -> Path:
|
|
@@ -104,56 +82,78 @@ def save_uploaded_files(files: list[str]) -> Path:
|
|
| 104 |
|
| 105 |
|
| 106 |
def format_stats(trace: object) -> str:
|
| 107 |
-
cost_str = f"${trace.cost:.4f}" if trace.cost else "n/a"
|
| 108 |
cached = trace.cached_tokens
|
| 109 |
cache_str = f" ({cached} cached)" if cached else ""
|
| 110 |
-
scratchpad = len(trace.scratch_files)
|
| 111 |
model_name = trace.model.split("/")[-1] if trace.model else ""
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
f"
|
| 115 |
-
f"
|
| 116 |
-
f"
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
workspace_path: str,
|
| 128 |
scratch_path: str,
|
| 129 |
session_cost: float,
|
| 130 |
-
token: str
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
if ACCESS_TOKEN and token != ACCESS_TOKEN:
|
| 135 |
-
yield ("Invalid access token."
|
| 136 |
return
|
| 137 |
|
| 138 |
if not MODEL:
|
| 139 |
-
yield ("
|
| 140 |
return
|
| 141 |
|
| 142 |
if session_cost >= MAX_SESSION_COST:
|
| 143 |
-
yield (
|
| 144 |
-
|
| 145 |
-
f"${
|
| 146 |
-
|
| 147 |
-
)
|
| 148 |
return
|
| 149 |
|
| 150 |
-
# Set up workspace from uploaded files (first message only)
|
| 151 |
workspace = Path(workspace_path) if workspace_path else None
|
| 152 |
if files and not workspace:
|
| 153 |
workspace = save_uploaded_files(files)
|
| 154 |
-
workspace_path = str(workspace)
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
| 157 |
if not scratch_path:
|
| 158 |
scratch_path = tempfile.mkdtemp(prefix="lh-scratch-")
|
| 159 |
scratch_dir = Path(scratch_path)
|
|
@@ -161,10 +161,9 @@ def chat(
|
|
| 161 |
system_prompt = build_system_prompt(base_prompt="", workspace=workspace)
|
| 162 |
messages: list[Message] = [
|
| 163 |
{"role": "system", "content": system_prompt},
|
| 164 |
-
{"role": "user", "content":
|
| 165 |
]
|
| 166 |
|
| 167 |
-
# Run agent loop with E2B sandbox
|
| 168 |
start = time.monotonic()
|
| 169 |
agent_run = run_agent_loop(
|
| 170 |
model=MODEL,
|
|
@@ -174,185 +173,374 @@ def chat(
|
|
| 174 |
workspace=workspace,
|
| 175 |
scratch_dir=scratch_dir,
|
| 176 |
sandbox_fn=e2b_run_python,
|
|
|
|
| 177 |
)
|
| 178 |
|
| 179 |
tool_call_count = 0
|
| 180 |
try:
|
| 181 |
for event in agent_run:
|
| 182 |
-
if isinstance(event,
|
|
|
|
|
|
|
| 183 |
tool_call_count += 1
|
| 184 |
-
|
| 185 |
-
yield status, "", workspace_path, scratch_path, session_cost, None
|
| 186 |
-
elif isinstance(event, ToolResultEvent):
|
| 187 |
-
continue
|
| 188 |
-
else:
|
| 189 |
-
cost = agent_run.trace.cost or 0
|
| 190 |
-
session_cost += cost
|
| 191 |
except Exception as exc:
|
| 192 |
-
yield (
|
| 193 |
-
f"Error: {exc}",
|
| 194 |
-
"",
|
| 195 |
-
workspace_path,
|
| 196 |
-
scratch_path,
|
| 197 |
-
session_cost,
|
| 198 |
-
None,
|
| 199 |
-
)
|
| 200 |
return
|
| 201 |
|
| 202 |
trace = agent_run.trace
|
| 203 |
trace.wall_time_s = round(time.monotonic() - start, 2)
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
trace_html = render_trace(result, max_chars=2000)
|
| 209 |
|
| 210 |
-
yield (
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
|
| 220 |
def build_app() -> gr.Blocks:
|
| 221 |
-
with gr.Blocks(title="Document Explorer"
|
| 222 |
-
gr.
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
session_traces_state = gr.State([]) # filenames uploaded this session
|
| 235 |
-
|
| 236 |
-
with gr.Row():
|
| 237 |
-
token_input = gr.Textbox(
|
| 238 |
-
placeholder="Access token",
|
| 239 |
-
label="Access Token",
|
| 240 |
-
type="password",
|
| 241 |
-
scale=1,
|
| 242 |
-
)
|
| 243 |
-
|
| 244 |
-
with gr.Accordion("Upload documents", open=True):
|
| 245 |
-
file_upload = gr.File(
|
| 246 |
-
label="Text, CSV, Markdown, or JSON files",
|
| 247 |
-
file_count="multiple",
|
| 248 |
-
file_types=[".txt", ".csv", ".md", ".json"],
|
| 249 |
-
)
|
| 250 |
-
|
| 251 |
-
chatbot = gr.Chatbot(height=500)
|
| 252 |
-
msg = gr.Textbox(
|
| 253 |
-
placeholder="Ask a question about your documents...",
|
| 254 |
-
label="",
|
| 255 |
-
show_label=False,
|
| 256 |
-
)
|
| 257 |
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
message, history, files, workspace_path, scratch_path,
|
| 263 |
-
session_cost, session_traces, token,
|
| 264 |
-
):
|
| 265 |
-
history = history or []
|
| 266 |
-
history.append({"role": "user", "content": message})
|
| 267 |
-
|
| 268 |
-
for response, trace_html, wp, sp, sc, trace_file in chat(
|
| 269 |
-
message, files, workspace_path, scratch_path, session_cost, token
|
| 270 |
-
):
|
| 271 |
-
if trace_file:
|
| 272 |
-
session_traces = [*session_traces, trace_file]
|
| 273 |
-
history_with_response = [
|
| 274 |
-
*history,
|
| 275 |
-
{"role": "assistant", "content": response},
|
| 276 |
-
]
|
| 277 |
-
accordion = gr.Accordion(visible=bool(trace_html))
|
| 278 |
-
yield (
|
| 279 |
-
history_with_response, "", trace_html, accordion,
|
| 280 |
-
wp, sp, sc, session_traces,
|
| 281 |
-
)
|
| 282 |
-
|
| 283 |
-
msg.submit(
|
| 284 |
-
respond,
|
| 285 |
-
inputs=[
|
| 286 |
-
msg,
|
| 287 |
-
chatbot,
|
| 288 |
-
file_upload,
|
| 289 |
-
workspace_state,
|
| 290 |
-
scratch_state,
|
| 291 |
-
cost_state,
|
| 292 |
-
session_traces_state,
|
| 293 |
-
token_input,
|
| 294 |
-
],
|
| 295 |
-
outputs=[
|
| 296 |
-
chatbot,
|
| 297 |
-
msg,
|
| 298 |
-
trace_display,
|
| 299 |
-
trace_accordion,
|
| 300 |
-
workspace_state,
|
| 301 |
-
scratch_state,
|
| 302 |
-
cost_state,
|
| 303 |
-
session_traces_state,
|
| 304 |
-
],
|
| 305 |
-
)
|
| 306 |
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
return ""
|
| 324 |
-
return render_trace(result, max_chars=5000)
|
| 325 |
-
|
| 326 |
-
def refresh_traces(session_traces, is_admin):
|
| 327 |
-
if is_admin:
|
| 328 |
-
filenames = list_traces()
|
| 329 |
-
else:
|
| 330 |
-
filenames = sorted(session_traces, reverse=True)
|
| 331 |
-
return gr.Dropdown(
|
| 332 |
-
choices=filenames,
|
| 333 |
-
value=filenames[0] if filenames else None,
|
| 334 |
-
)
|
| 335 |
-
|
| 336 |
-
demo.load(
|
| 337 |
-
check_admin,
|
| 338 |
-
outputs=[admin_state],
|
| 339 |
-
)
|
| 340 |
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
| 345 |
)
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
inputs=[session_traces_state, admin_state],
|
| 349 |
-
outputs=[trace_dropdown],
|
| 350 |
)
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 356 |
|
| 357 |
return demo
|
| 358 |
|
|
@@ -362,4 +550,4 @@ if __name__ == "__main__":
|
|
| 362 |
print("WARNING: LH_ACCESS_TOKEN not set — app is unprotected")
|
| 363 |
|
| 364 |
app = build_app()
|
| 365 |
-
app.launch()
|
|
|
|
| 1 |
+
"""Document Explorer — upload documents, ask questions with cited answers.
|
| 2 |
|
| 3 |
+
The LLM explores documents by writing Python code. No training, no vector DB.
|
| 4 |
+
Built on a-simple-llm-harness.
|
|
|
|
|
|
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
|
|
|
| 10 |
import os
|
| 11 |
import tempfile
|
| 12 |
import time
|
| 13 |
+
from collections.abc import Generator
|
| 14 |
from dataclasses import asdict
|
| 15 |
from datetime import datetime, timezone
|
| 16 |
from pathlib import Path
|
|
|
|
| 21 |
from huggingface_hub import HfApi
|
| 22 |
|
| 23 |
from llm_harness.agent import run_agent_loop
|
| 24 |
+
from llm_harness.citations import process_citations, superscript
|
| 25 |
from llm_harness.prompt import build_system_prompt
|
| 26 |
from llm_harness.tools import TOOL_DEFINITIONS
|
| 27 |
from llm_harness.trace_viewer import render_trace
|
| 28 |
+
from llm_harness.types import Message, TextDeltaEvent, ToolCallEvent, ToolResultEvent
|
| 29 |
|
| 30 |
from sandbox_e2b import run_python as e2b_run_python
|
| 31 |
|
|
|
|
| 39 |
HF_TRACES_REPO = os.environ.get("HF_TRACES_REPO", "")
|
| 40 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 41 |
|
| 42 |
+
SOURCE = "prod" if os.environ.get("SPACE_ID") else "dev"
|
| 43 |
+
|
| 44 |
hf_api = HfApi(token=HF_TOKEN) if HF_TOKEN else None
|
| 45 |
|
| 46 |
|
| 47 |
+
# ---------------------------------------------------------------------------
|
| 48 |
+
# Helpers
|
| 49 |
+
# ---------------------------------------------------------------------------
|
| 50 |
+
|
| 51 |
+
|
| 52 |
def _slugify(text: str, max_len: int = 50) -> str:
|
| 53 |
slug = text.lower().replace(" ", "-")
|
| 54 |
slug = "".join(c for c in slug if c.isalnum() or c == "-")
|
| 55 |
return slug[:max_len].rstrip("-")
|
| 56 |
|
| 57 |
|
| 58 |
+
def upload_trace(result: dict) -> None:
|
| 59 |
if not hf_api or not HF_TRACES_REPO:
|
| 60 |
+
return
|
| 61 |
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
|
| 62 |
question_slug = _slugify(result.get("question", ""))
|
| 63 |
filename = f"{timestamp}_{question_slug}.json" if question_slug else f"{timestamp}.json"
|
|
|
|
| 69 |
repo_id=HF_TRACES_REPO,
|
| 70 |
repo_type="dataset",
|
| 71 |
)
|
|
|
|
| 72 |
except Exception as exc:
|
| 73 |
print(f"WARNING: trace upload failed: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
|
| 76 |
def save_uploaded_files(files: list[str]) -> Path:
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def format_stats(trace: object) -> str:
|
|
|
|
| 85 |
cached = trace.cached_tokens
|
| 86 |
cache_str = f" ({cached} cached)" if cached else ""
|
|
|
|
| 87 |
model_name = trace.model.split("/")[-1] if trace.model else ""
|
| 88 |
+
parts = [
|
| 89 |
+
model_name,
|
| 90 |
+
f"{trace.prompt_tokens + trace.completion_tokens:,} tokens{cache_str}",
|
| 91 |
+
f"{len(trace.tool_calls)} tool calls",
|
| 92 |
+
f"{trace.wall_time_s:.1f}s",
|
| 93 |
+
]
|
| 94 |
+
if trace.cost:
|
| 95 |
+
parts.append(f"${trace.cost:.4f}")
|
| 96 |
+
return " · ".join(parts)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def format_stats_from_trace(trace: dict) -> str:
|
| 100 |
+
cached = trace.get("cached_tokens", 0)
|
| 101 |
+
cache_str = f" ({cached} cached)" if cached else ""
|
| 102 |
+
model = trace.get("model", "")
|
| 103 |
+
model_name = model.split("/")[-1] if model else ""
|
| 104 |
+
prompt = trace.get("prompt_tokens", 0)
|
| 105 |
+
completion = trace.get("completion_tokens", 0)
|
| 106 |
+
tool_calls = trace.get("tool_calls", [])
|
| 107 |
+
wall = trace.get("wall_time_s", 0)
|
| 108 |
+
cost = trace.get("cost")
|
| 109 |
+
parts = [
|
| 110 |
+
model_name,
|
| 111 |
+
f"{prompt + completion:,} tokens{cache_str}",
|
| 112 |
+
f"{len(tool_calls)} tool calls",
|
| 113 |
+
f"{wall:.1f}s",
|
| 114 |
+
]
|
| 115 |
+
if cost:
|
| 116 |
+
parts.append(f"${cost:.4f}")
|
| 117 |
+
return " · ".join(parts)
|
| 118 |
|
| 119 |
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
# Streaming question handler
|
| 122 |
+
# ---------------------------------------------------------------------------
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def stream_question(
|
| 126 |
+
question: str,
|
| 127 |
workspace_path: str,
|
| 128 |
scratch_path: str,
|
| 129 |
session_cost: float,
|
| 130 |
+
token: str,
|
| 131 |
+
files: list[str] | None = None,
|
| 132 |
+
) -> Generator[str, None, None]:
|
| 133 |
+
"""Streaming API — yields JSON event strings."""
|
| 134 |
if ACCESS_TOKEN and token != ACCESS_TOKEN:
|
| 135 |
+
yield json.dumps({"type": "error", "error": "Invalid access token."})
|
| 136 |
return
|
| 137 |
|
| 138 |
if not MODEL:
|
| 139 |
+
yield json.dumps({"type": "error", "error": "LH_MODEL not set."})
|
| 140 |
return
|
| 141 |
|
| 142 |
if session_cost >= MAX_SESSION_COST:
|
| 143 |
+
yield json.dumps({
|
| 144 |
+
"type": "error",
|
| 145 |
+
"error": f"Session cost limit reached (${session_cost:.2f} / ${MAX_SESSION_COST:.2f}).",
|
| 146 |
+
})
|
|
|
|
| 147 |
return
|
| 148 |
|
|
|
|
| 149 |
workspace = Path(workspace_path) if workspace_path else None
|
| 150 |
if files and not workspace:
|
| 151 |
workspace = save_uploaded_files(files)
|
|
|
|
| 152 |
|
| 153 |
+
if not workspace:
|
| 154 |
+
yield json.dumps({"type": "error", "error": "No documents uploaded."})
|
| 155 |
+
return
|
| 156 |
+
|
| 157 |
if not scratch_path:
|
| 158 |
scratch_path = tempfile.mkdtemp(prefix="lh-scratch-")
|
| 159 |
scratch_dir = Path(scratch_path)
|
|
|
|
| 161 |
system_prompt = build_system_prompt(base_prompt="", workspace=workspace)
|
| 162 |
messages: list[Message] = [
|
| 163 |
{"role": "system", "content": system_prompt},
|
| 164 |
+
{"role": "user", "content": question},
|
| 165 |
]
|
| 166 |
|
|
|
|
| 167 |
start = time.monotonic()
|
| 168 |
agent_run = run_agent_loop(
|
| 169 |
model=MODEL,
|
|
|
|
| 173 |
workspace=workspace,
|
| 174 |
scratch_dir=scratch_dir,
|
| 175 |
sandbox_fn=e2b_run_python,
|
| 176 |
+
stream=True,
|
| 177 |
)
|
| 178 |
|
| 179 |
tool_call_count = 0
|
| 180 |
try:
|
| 181 |
for event in agent_run:
|
| 182 |
+
if isinstance(event, TextDeltaEvent):
|
| 183 |
+
yield json.dumps({"type": "delta", "content": event.content})
|
| 184 |
+
elif isinstance(event, ToolCallEvent):
|
| 185 |
tool_call_count += 1
|
| 186 |
+
yield json.dumps({"type": "tool_call", "count": tool_call_count, "name": event.name})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
except Exception as exc:
|
| 188 |
+
yield json.dumps({"type": "error", "error": str(exc)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
return
|
| 190 |
|
| 191 |
trace = agent_run.trace
|
| 192 |
trace.wall_time_s = round(time.monotonic() - start, 2)
|
| 193 |
+
|
| 194 |
+
clean_answer, sources = process_citations(trace.answer or "", workspace)
|
| 195 |
+
|
| 196 |
+
result = {
|
| 197 |
+
"question": question,
|
| 198 |
+
"source": SOURCE,
|
| 199 |
+
"passed": True,
|
| 200 |
+
"assertions": {},
|
| 201 |
+
"trace": asdict(trace),
|
| 202 |
+
"citations": sources,
|
| 203 |
+
}
|
| 204 |
+
upload_trace(result)
|
| 205 |
trace_html = render_trace(result, max_chars=2000)
|
| 206 |
|
| 207 |
+
yield json.dumps({
|
| 208 |
+
"type": "done",
|
| 209 |
+
"answer": clean_answer,
|
| 210 |
+
"sources": sources,
|
| 211 |
+
"stats": format_stats(trace),
|
| 212 |
+
"trace_html": trace_html,
|
| 213 |
+
"workspace_path": str(workspace),
|
| 214 |
+
"scratch_path": str(scratch_dir),
|
| 215 |
+
"session_cost": session_cost + (trace.cost or 0),
|
| 216 |
+
})
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# ---------------------------------------------------------------------------
|
| 220 |
+
# Gradio app
|
| 221 |
+
# ---------------------------------------------------------------------------
|
| 222 |
+
|
| 223 |
+
CUSTOM_HTML = """
|
| 224 |
+
<div id="explorer-app">
|
| 225 |
+
<div id="setup-panel">
|
| 226 |
+
<div class="setup-field">
|
| 227 |
+
<label for="token-input">Access Token</label>
|
| 228 |
+
<input type="password" id="token-input" placeholder="Enter your access token" />
|
| 229 |
+
</div>
|
| 230 |
+
<div class="setup-field">
|
| 231 |
+
<label>Upload Documents</label>
|
| 232 |
+
<div id="drop-zone" class="drop-zone">
|
| 233 |
+
<p>Drag and drop files here, or <label for="file-input" class="file-label">browse</label></p>
|
| 234 |
+
<input type="file" id="file-input" multiple accept=".txt,.csv,.md,.json,.pdf" style="display:none" />
|
| 235 |
+
<div id="file-list"></div>
|
| 236 |
+
</div>
|
| 237 |
+
</div>
|
| 238 |
+
<button id="start-btn" disabled>Start exploring</button>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
<div id="chat-panel" style="display:none">
|
| 242 |
+
<div class="chat-container">
|
| 243 |
+
<div class="chat-history" id="chat-history"></div>
|
| 244 |
+
<div class="chat-input-wrapper">
|
| 245 |
+
<input type="text" id="chat-input" class="chat-input" placeholder="Ask a question about your documents..." autocomplete="off" />
|
| 246 |
+
</div>
|
| 247 |
+
<div class="chat-stats" id="session-cost"></div>
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
| 251 |
+
|
| 252 |
+
<link rel="stylesheet" href="/file=static/chat-ui.css" />
|
| 253 |
+
<script src="/file=static/chat-ui.js"></script>
|
| 254 |
+
<style>
|
| 255 |
+
#explorer-app { font-family: var(--font-family, 'Inter', system-ui, sans-serif); max-width: 800px; margin: 0 auto; }
|
| 256 |
+
#setup-panel { padding: 24px 0; }
|
| 257 |
+
.setup-field { margin-bottom: 16px; }
|
| 258 |
+
.setup-field label { display: block; font-size: 13px; color: #6B7280; margin-bottom: 4px; }
|
| 259 |
+
#token-input { width: 100%; padding: 8px 12px; border: 1px solid #E5E7EB; border-radius: 4px; font-family: inherit; font-size: 14px; }
|
| 260 |
+
#token-input:focus { outline: none; border-color: #4682B4; }
|
| 261 |
+
.drop-zone { border: 2px dashed #E5E7EB; border-radius: 8px; padding: 32px; text-align: center; color: #9CA3AF; cursor: pointer; transition: border-color 0.2s; }
|
| 262 |
+
.drop-zone.drag-over { border-color: #4682B4; background: #f0f7ff; }
|
| 263 |
+
.drop-zone p { margin: 0; }
|
| 264 |
+
.file-label { color: #4682B4; cursor: pointer; text-decoration: underline; }
|
| 265 |
+
#file-list { margin-top: 8px; font-size: 13px; color: #374151; text-align: left; }
|
| 266 |
+
#file-list div { padding: 2px 0; }
|
| 267 |
+
#start-btn { width: 100%; padding: 10px; background: #4682B4; color: white; border: none; border-radius: 4px; font-family: inherit; font-size: 14px; cursor: pointer; margin-top: 8px; }
|
| 268 |
+
#start-btn:disabled { background: #D1D5DB; cursor: default; }
|
| 269 |
+
#start-btn:not(:disabled):hover { background: #3a6f9a; }
|
| 270 |
+
#chat-panel { padding-top: 8px; }
|
| 271 |
+
</style>
|
| 272 |
+
<script>
|
| 273 |
+
(function() {
|
| 274 |
+
var API_BASE = window.location.origin;
|
| 275 |
+
var tokenInput = document.getElementById('token-input');
|
| 276 |
+
var fileInput = document.getElementById('file-input');
|
| 277 |
+
var dropZone = document.getElementById('drop-zone');
|
| 278 |
+
var fileList = document.getElementById('file-list');
|
| 279 |
+
var startBtn = document.getElementById('start-btn');
|
| 280 |
+
var setupPanel = document.getElementById('setup-panel');
|
| 281 |
+
var chatPanel = document.getElementById('chat-panel');
|
| 282 |
+
var chatInput = document.getElementById('chat-input');
|
| 283 |
+
var chatHistory = document.getElementById('chat-history');
|
| 284 |
+
var sessionCostEl = document.getElementById('session-cost');
|
| 285 |
+
|
| 286 |
+
var selectedFiles = [];
|
| 287 |
+
var workspacePath = '';
|
| 288 |
+
var scratchPath = '';
|
| 289 |
+
var sessionCost = 0;
|
| 290 |
+
|
| 291 |
+
function updateStartBtn() {
|
| 292 |
+
startBtn.disabled = !(selectedFiles.length > 0);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
function showFiles() {
|
| 296 |
+
fileList.innerHTML = selectedFiles.map(function(f) { return '<div>' + escapeHtml(f.name) + '</div>'; }).join('');
|
| 297 |
+
updateStartBtn();
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
fileInput.addEventListener('change', function() {
|
| 301 |
+
selectedFiles = Array.from(fileInput.files);
|
| 302 |
+
showFiles();
|
| 303 |
+
});
|
| 304 |
+
|
| 305 |
+
dropZone.addEventListener('click', function() { fileInput.click(); });
|
| 306 |
+
dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
| 307 |
+
dropZone.addEventListener('dragleave', function() { dropZone.classList.remove('drag-over'); });
|
| 308 |
+
dropZone.addEventListener('drop', function(e) {
|
| 309 |
+
e.preventDefault();
|
| 310 |
+
dropZone.classList.remove('drag-over');
|
| 311 |
+
selectedFiles = Array.from(e.dataTransfer.files);
|
| 312 |
+
showFiles();
|
| 313 |
+
});
|
| 314 |
+
|
| 315 |
+
startBtn.addEventListener('click', function() {
|
| 316 |
+
startBtn.disabled = true;
|
| 317 |
+
startBtn.textContent = 'Uploading...';
|
| 318 |
+
|
| 319 |
+
var formData = new FormData();
|
| 320 |
+
selectedFiles.forEach(function(f) { formData.append('files', f); });
|
| 321 |
+
|
| 322 |
+
fetch(API_BASE + '/gradio_api/call/upload', {
|
| 323 |
+
method: 'POST',
|
| 324 |
+
headers: { 'Content-Type': 'application/json' },
|
| 325 |
+
body: JSON.stringify({ data: [tokenInput.value] })
|
| 326 |
+
})
|
| 327 |
+
.then(function(r) { return r.json(); })
|
| 328 |
+
.then(function(result) {
|
| 329 |
+
return fetch(API_BASE + '/gradio_api/call/upload/' + result.event_id);
|
| 330 |
+
})
|
| 331 |
+
.then(function(r) { return r.text(); })
|
| 332 |
+
.then(function() {
|
| 333 |
+
setupPanel.style.display = 'none';
|
| 334 |
+
chatPanel.style.display = 'block';
|
| 335 |
+
chatInput.focus();
|
| 336 |
+
})
|
| 337 |
+
.catch(function() {
|
| 338 |
+
startBtn.disabled = false;
|
| 339 |
+
startBtn.textContent = 'Start exploring';
|
| 340 |
+
});
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
chatInput.addEventListener('keydown', function(e) {
|
| 344 |
+
if (e.key !== 'Enter' || !chatInput.value.trim() || chatInput.disabled) return;
|
| 345 |
+
e.preventDefault();
|
| 346 |
+
var question = chatInput.value.trim();
|
| 347 |
+
chatInput.value = '';
|
| 348 |
+
chatInput.disabled = true;
|
| 349 |
+
chatInput.placeholder = '';
|
| 350 |
+
|
| 351 |
+
var turn = document.createElement('div');
|
| 352 |
+
turn.className = 'chat-turn';
|
| 353 |
+
turn.innerHTML = '<div class="chat-question">' + escapeHtml(question) + '</div>';
|
| 354 |
+
chatHistory.appendChild(turn);
|
| 355 |
+
|
| 356 |
+
var answerEl = document.createElement('div');
|
| 357 |
+
answerEl.className = 'chat-answer';
|
| 358 |
+
turn.appendChild(answerEl);
|
| 359 |
+
var accumulated = '';
|
| 360 |
+
var toolCount = 0;
|
| 361 |
+
|
| 362 |
+
fetch(API_BASE + '/gradio_api/call/ask', {
|
| 363 |
+
method: 'POST',
|
| 364 |
+
headers: { 'Content-Type': 'application/json' },
|
| 365 |
+
body: JSON.stringify({ data: [question, workspacePath, scratchPath, sessionCost, tokenInput.value] })
|
| 366 |
+
})
|
| 367 |
+
.then(function(r) { return r.json(); })
|
| 368 |
+
.then(function(result) {
|
| 369 |
+
var eventSource = new EventSource(API_BASE + '/gradio_api/call/ask/' + result.event_id);
|
| 370 |
+
|
| 371 |
+
function handleEvent(e) {
|
| 372 |
+
var raw = JSON.parse(e.data);
|
| 373 |
+
var eventData = JSON.parse(Array.isArray(raw) ? raw[0] : raw);
|
| 374 |
+
|
| 375 |
+
if (eventData.type === 'delta') {
|
| 376 |
+
accumulated += eventData.content;
|
| 377 |
+
answerEl.innerHTML = markdownToHtml(accumulated);
|
| 378 |
+
answerEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
| 379 |
+
} else if (eventData.type === 'tool_call') {
|
| 380 |
+
toolCount = eventData.count;
|
| 381 |
+
answerEl.innerHTML = '<em class="chat-tool-status">Exploring documents (' + toolCount + ' tool calls)...</em>';
|
| 382 |
+
accumulated = '';
|
| 383 |
+
} else if (eventData.type === 'error') {
|
| 384 |
+
eventSource.close();
|
| 385 |
+
answerEl.innerHTML = '<span class="chat-error">' + escapeHtml(eventData.error) + '</span>';
|
| 386 |
+
chatInput.disabled = false;
|
| 387 |
+
chatInput.placeholder = 'Ask a question about your documents...';
|
| 388 |
+
} else if (eventData.type === 'done') {
|
| 389 |
+
eventSource.close();
|
| 390 |
+
var finalHtml = '<div class="chat-answer">' + markdownToHtml(eventData.answer || accumulated) + '</div>';
|
| 391 |
+
finalHtml += renderSources(eventData.sources);
|
| 392 |
+
if (eventData.stats) {
|
| 393 |
+
finalHtml += '<div class="chat-stats">' + eventData.stats + '</div>';
|
| 394 |
+
}
|
| 395 |
+
if (eventData.trace_html) {
|
| 396 |
+
finalHtml += '<button class="chat-trace-toggle" onclick="this.nextElementSibling.classList.toggle(\'open\')">trace</button>';
|
| 397 |
+
finalHtml += '<div class="chat-trace">' + eventData.trace_html + '</div>';
|
| 398 |
+
}
|
| 399 |
+
turn.innerHTML = '<div class="chat-question">' + escapeHtml(question) + '</div>' + finalHtml;
|
| 400 |
+
|
| 401 |
+
if (eventData.workspace_path) workspacePath = eventData.workspace_path;
|
| 402 |
+
if (eventData.scratch_path) scratchPath = eventData.scratch_path;
|
| 403 |
+
if (eventData.session_cost != null) {
|
| 404 |
+
sessionCost = eventData.session_cost;
|
| 405 |
+
sessionCostEl.textContent = 'Session cost: $' + sessionCost.toFixed(4);
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
chatInput.disabled = false;
|
| 409 |
+
chatInput.placeholder = 'Ask a question about your documents...';
|
| 410 |
+
chatInput.focus({ preventScroll: true });
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
eventSource.addEventListener('generating', handleEvent);
|
| 415 |
+
eventSource.addEventListener('complete', handleEvent);
|
| 416 |
+
|
| 417 |
+
eventSource.onerror = function() {
|
| 418 |
+
eventSource.close();
|
| 419 |
+
if (!accumulated) {
|
| 420 |
+
answerEl.innerHTML = '<span class="chat-error">Connection error.</span>';
|
| 421 |
+
}
|
| 422 |
+
chatInput.disabled = false;
|
| 423 |
+
chatInput.placeholder = 'Ask a question about your documents...';
|
| 424 |
+
};
|
| 425 |
+
})
|
| 426 |
+
.catch(function() {
|
| 427 |
+
turn.innerHTML += '<div class="chat-error">Connection error.</div>';
|
| 428 |
+
chatInput.disabled = false;
|
| 429 |
+
chatInput.placeholder = 'Ask a question about your documents...';
|
| 430 |
+
});
|
| 431 |
+
});
|
| 432 |
+
})();
|
| 433 |
+
</script>
|
| 434 |
+
"""
|
| 435 |
|
| 436 |
|
| 437 |
def build_app() -> gr.Blocks:
|
| 438 |
+
with gr.Blocks(title="Document Explorer") as demo:
|
| 439 |
+
gr.HTML(CUSTOM_HTML)
|
| 440 |
+
|
| 441 |
+
# Hidden state for file upload workspace
|
| 442 |
+
upload_workspace = gr.State("")
|
| 443 |
+
|
| 444 |
+
# Streaming ask endpoint
|
| 445 |
+
api_ask_input = [
|
| 446 |
+
gr.Textbox(visible=False), # question
|
| 447 |
+
gr.Textbox(visible=False), # workspace_path
|
| 448 |
+
gr.Textbox(visible=False), # scratch_path
|
| 449 |
+
gr.Number(visible=False), # session_cost
|
| 450 |
+
gr.Textbox(visible=False), # token
|
| 451 |
+
]
|
| 452 |
+
api_ask_output = gr.Textbox(visible=False)
|
| 453 |
+
|
| 454 |
+
def api_ask_stream(question, workspace_path, scratch_path, session_cost, token):
|
| 455 |
+
for event_json in stream_question(
|
| 456 |
+
question, workspace_path, scratch_path, session_cost, token
|
| 457 |
+
):
|
| 458 |
+
yield event_json
|
| 459 |
+
|
| 460 |
+
api_ask_btn = gr.Button(visible=False)
|
| 461 |
+
api_ask_btn.click(
|
| 462 |
+
api_ask_stream,
|
| 463 |
+
inputs=api_ask_input,
|
| 464 |
+
outputs=api_ask_output,
|
| 465 |
+
api_name="ask",
|
| 466 |
+
)
|
| 467 |
|
| 468 |
+
# Upload endpoint (validates token, returns confirmation)
|
| 469 |
+
upload_input = gr.Textbox(visible=False)
|
| 470 |
+
upload_output = gr.Textbox(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
+
def api_upload(token):
|
| 473 |
+
if ACCESS_TOKEN and token != ACCESS_TOKEN:
|
| 474 |
+
return json.dumps({"error": "Invalid access token."})
|
| 475 |
+
return json.dumps({"ok": True})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
|
| 477 |
+
upload_btn = gr.Button(visible=False)
|
| 478 |
+
upload_btn.click(api_upload, inputs=upload_input, outputs=upload_output, api_name="upload")
|
| 479 |
+
|
| 480 |
+
# Document viewer endpoint
|
| 481 |
+
doc_input = gr.Textbox(visible=False)
|
| 482 |
+
doc_output = gr.Textbox(visible=False)
|
| 483 |
+
|
| 484 |
+
def api_get_doc(filename):
|
| 485 |
+
return json.dumps({"error": "not available"})
|
| 486 |
+
|
| 487 |
+
doc_btn = gr.Button(visible=False)
|
| 488 |
+
doc_btn.click(api_get_doc, inputs=doc_input, outputs=doc_output, api_name="doc")
|
| 489 |
+
|
| 490 |
+
# Trace list endpoint
|
| 491 |
+
traces_input = gr.Textbox(visible=False)
|
| 492 |
+
traces_output = gr.Textbox(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
|
| 494 |
+
def api_list_traces(query):
|
| 495 |
+
if not hf_api or not HF_TRACES_REPO:
|
| 496 |
+
return json.dumps({"error": "traces not configured"})
|
| 497 |
+
try:
|
| 498 |
+
files = hf_api.list_repo_files(
|
| 499 |
+
repo_id=HF_TRACES_REPO, repo_type="dataset"
|
| 500 |
)
|
| 501 |
+
traces = sorted(
|
| 502 |
+
[f for f in files if f.endswith(".json")], reverse=True
|
|
|
|
|
|
|
| 503 |
)
|
| 504 |
+
if query:
|
| 505 |
+
traces = [f for f in traces if query.lower() in f.lower()]
|
| 506 |
+
return json.dumps({"traces": traces[:100]})
|
| 507 |
+
except Exception as exc:
|
| 508 |
+
return json.dumps({"error": str(exc)})
|
| 509 |
+
|
| 510 |
+
traces_btn = gr.Button(visible=False)
|
| 511 |
+
traces_btn.click(api_list_traces, inputs=traces_input, outputs=traces_output, api_name="traces")
|
| 512 |
+
|
| 513 |
+
# Trace replay endpoint
|
| 514 |
+
replay_input = gr.Textbox(visible=False)
|
| 515 |
+
replay_output = gr.Textbox(visible=False)
|
| 516 |
+
|
| 517 |
+
def api_get_trace(filename):
|
| 518 |
+
if not hf_api or not HF_TRACES_REPO or not filename:
|
| 519 |
+
return json.dumps({"error": "not found"})
|
| 520 |
+
safe_name = Path(filename).name
|
| 521 |
+
try:
|
| 522 |
+
path = hf_api.hf_hub_download(
|
| 523 |
+
HF_TRACES_REPO, safe_name, repo_type="dataset"
|
| 524 |
)
|
| 525 |
+
data = json.loads(Path(path).read_text())
|
| 526 |
+
trace = data.get("trace", {})
|
| 527 |
+
raw_answer = trace.get("answer", "")
|
| 528 |
+
clean_answer, sources = process_citations(raw_answer, None)
|
| 529 |
+
trace_html = render_trace(data, max_chars=2000)
|
| 530 |
+
return json.dumps({
|
| 531 |
+
"question": data.get("question", ""),
|
| 532 |
+
"answer": clean_answer,
|
| 533 |
+
"sources": sources,
|
| 534 |
+
"stats": format_stats_from_trace(trace),
|
| 535 |
+
"source_tag": data.get("source", ""),
|
| 536 |
+
"trace_html": trace_html,
|
| 537 |
+
"filename": safe_name,
|
| 538 |
+
})
|
| 539 |
+
except Exception as exc:
|
| 540 |
+
return json.dumps({"error": str(exc)})
|
| 541 |
+
|
| 542 |
+
replay_btn = gr.Button(visible=False)
|
| 543 |
+
replay_btn.click(api_get_trace, inputs=replay_input, outputs=replay_output, api_name="replay")
|
| 544 |
|
| 545 |
return demo
|
| 546 |
|
|
|
|
| 550 |
print("WARNING: LH_ACCESS_TOKEN not set — app is unprotected")
|
| 551 |
|
| 552 |
app = build_app()
|
| 553 |
+
app.launch(server_name="0.0.0.0", server_port=7860)
|
requirements.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
click>=8.1
|
| 2 |
litellm @ git+https://github.com/BerriAI/litellm.git
|
| 3 |
-
a-simple-llm-harness @ git+https://github.com/chuckfinca/a-simple-llm-harness.git
|
| 4 |
e2b-code-interpreter>=2.5
|
| 5 |
huggingface-hub
|
| 6 |
python-dotenv
|
|
|
|
| 1 |
click>=8.1
|
| 2 |
litellm @ git+https://github.com/BerriAI/litellm.git
|
| 3 |
+
a-simple-llm-harness @ git+https://github.com/chuckfinca/a-simple-llm-harness.git@acfd4f2
|
| 4 |
e2b-code-interpreter>=2.5
|
| 5 |
huggingface-hub
|
| 6 |
python-dotenv
|
static/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
.gitkeep
|