airayven7 commited on
Commit
a6a1bb9
·
verified ·
1 Parent(s): 9c0bd3b

Sync from GitHub 86a5d9c

Browse files
Files changed (3) hide show
  1. app.py +34 -10
  2. frontend/index.html +5 -2
  3. pipelines/agent_ask.py +29 -2
app.py CHANGED
@@ -42,9 +42,11 @@ Module layout:
42
  import base64
43
  import io
44
  import json
 
45
  import os
46
  import shutil
47
  import time
 
48
 
49
  import gradio as gr
50
  from fastapi.responses import FileResponse, HTMLResponse, Response
@@ -61,6 +63,19 @@ from core.constants import (
61
  VISUAL_SUBDIR,
62
  )
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  def _build_libraries() -> dict:
66
  """The {approach: (store, pipeline)} map, constructed once at startup.
@@ -105,8 +120,9 @@ def sync_library() -> None:
105
  snapshot_download(
106
  LIBRARY_DATASET_ID, repo_type="dataset", local_dir=PREINDEXED_DIR
107
  )
 
108
  except Exception as e:
109
- print(f"Library dataset not synced ({LIBRARY_DATASET_ID}): {e}")
110
  return
111
  # PREINDEXED_DIR mirrors the dataset (one dir per method); prune top-level
112
  # leftovers from the pre-method-prefix layout, which snapshot_download
@@ -243,10 +259,13 @@ def api_ask(
243
  }
244
  return
245
  start = time.monotonic()
 
 
 
 
 
246
  try:
247
- events = pipeline.run_agent(
248
- store, question, _clean_history(history), [manual], int(k)
249
- )
250
  for ev in events:
251
  if ev.get("type") == "tool_result" and "gallery" in ev:
252
  pages = [p for _, p in ev["page_refs"]]
@@ -260,16 +279,21 @@ def api_ask(
260
  ],
261
  }
262
  elif ev.get("type") == "answer":
263
- yield {
264
- **ev,
265
- "elapsed": round(time.monotonic() - start, 1),
266
- "approach": approach,
267
- "k": int(k),
268
- }
 
269
  else:
270
  yield ev
271
  except ValueError as e:
 
272
  yield {"type": "error", "error": f"⚠️ {e}"}
 
 
 
273
 
274
 
275
  # --- custom FastAPI routes: serve the SPA and the source PDFs ---------------
 
42
  import base64
43
  import io
44
  import json
45
+ import logging
46
  import os
47
  import shutil
48
  import time
49
+ import warnings
50
 
51
  import gradio as gr
52
  from fastapi.responses import FileResponse, HTMLResponse, Response
 
63
  VISUAL_SUBDIR,
64
  )
65
 
66
+ # gradio 6.17.3 (pinned — see README frontmatter) still uses starlette's old
67
+ # 422 constant, so every queue join emits a StarletteDeprecationWarning. Not
68
+ # ours to fix; silence it so the Space logs stay readable.
69
+ warnings.filterwarnings(
70
+ "ignore", message=r"'HTTP_422_UNPROCESSABLE_ENTITY' is deprecated"
71
+ )
72
+
73
+ logging.basicConfig(
74
+ level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s | %(message)s"
75
+ )
76
+ logging.getLogger("httpx").setLevel(logging.WARNING) # logs every hub request
77
+ log = logging.getLogger("repairguy")
78
+
79
 
80
  def _build_libraries() -> dict:
81
  """The {approach: (store, pipeline)} map, constructed once at startup.
 
120
  snapshot_download(
121
  LIBRARY_DATASET_ID, repo_type="dataset", local_dir=PREINDEXED_DIR
122
  )
123
+ log.info("library synced from %s", LIBRARY_DATASET_ID)
124
  except Exception as e:
125
+ log.warning("library dataset not synced (%s): %s", LIBRARY_DATASET_ID, e)
126
  return
127
  # PREINDEXED_DIR mirrors the dataset (one dir per method); prune top-level
128
  # leftovers from the pre-method-prefix layout, which snapshot_download
 
259
  }
260
  return
261
  start = time.monotonic()
262
+ hist = _clean_history(history)
263
+ log.info(
264
+ "ask: manual=%s approach=%s k=%s history=%d q=%r",
265
+ manual, approach, k, len(hist), question[:200],
266
+ )
267
  try:
268
+ events = pipeline.run_agent(store, question, hist, [manual], int(k))
 
 
269
  for ev in events:
270
  if ev.get("type") == "tool_result" and "gallery" in ev:
271
  pages = [p for _, p in ev["page_refs"]]
 
279
  ],
280
  }
281
  elif ev.get("type") == "answer":
282
+ elapsed = round(time.monotonic() - start, 1)
283
+ log.info(
284
+ "ask: answered in %.1fs (%d chars, history=%d, summarized=%s)",
285
+ elapsed, len(ev.get("answer") or ""),
286
+ len(ev.get("history") or []), ev.get("summarized"),
287
+ )
288
+ yield {**ev, "elapsed": elapsed, "approach": approach, "k": int(k)}
289
  else:
290
  yield ev
291
  except ValueError as e:
292
+ log.warning("ask: rejected — %s", e)
293
  yield {"type": "error", "error": f"⚠️ {e}"}
294
+ except Exception as e:
295
+ log.exception("ask: failed after %.1fs", time.monotonic() - start)
296
+ yield {"type": "error", "error": f"⚠️ Something went wrong: {e}"}
297
 
298
 
299
  # --- custom FastAPI routes: serve the SPA and the source PDFs ---------------
frontend/index.html CHANGED
@@ -172,8 +172,11 @@
172
  <!-- RIGHT: PDF + cited pages -->
173
  <section class="min-h-0 flex flex-col bg-white rounded-2xl shadow-panel ring-1 ring-navy/10 overflow-hidden">
174
  <div class="flex-1 min-h-0 relative bg-brand-50/40">
175
- <template x-if="pdfDocId">
176
- <iframe :src="pdfUrl" class="absolute inset-0 w-full h-full"></iframe>
 
 
 
177
  </template>
178
  <div x-show="!pdfDocId" class="chat-grid absolute inset-0 grid place-items-center text-center px-8">
179
  <div>
 
172
  <!-- RIGHT: PDF + cited pages -->
173
  <section class="min-h-0 flex flex-col bg-white rounded-2xl shadow-panel ring-1 ring-navy/10 overflow-hidden">
174
  <div class="flex-1 min-h-0 relative bg-brand-50/40">
175
+ <!-- keyed on the full URL: PDF viewers only honor the #page fragment at
176
+ load time and ignore hash-only src changes, so the iframe must be
177
+ recreated to jump pages (same-origin reload, served from cache) -->
178
+ <template x-for="u in (pdfDocId ? [pdfUrl] : [])" :key="u">
179
+ <iframe :src="u" class="absolute inset-0 w-full h-full"></iframe>
180
  </template>
181
  <div x-show="!pdfDocId" class="chat-grid absolute inset-0 grid place-items-center text-center px-8">
182
  <div>
pipelines/agent_ask.py CHANGED
@@ -31,6 +31,8 @@ cannot live in history under chat()'s 16384-token input cap).
31
 
32
  from __future__ import annotations
33
 
 
 
34
  import spaces
35
 
36
  from core.constants import (
@@ -42,6 +44,8 @@ from core.constants import (
42
  from core.pdf import render_page
43
  from models import minicpm
44
 
 
 
45
  GIVE_UP_ANSWER = (
46
  "I couldn't put together a grounded answer within my tool budget — try "
47
  "rephrasing the question or pointing me at a section of the manual."
@@ -77,8 +81,12 @@ def agent_events(
77
  summarized = False
78
  if (
79
  len(history) > HISTORY_KEEP_MESSAGES
80
- and minicpm.history_tokens(history) > HISTORY_TOKEN_BUDGET
81
  ):
 
 
 
 
82
  yield {
83
  "type": "status",
84
  "kind": "summarizing",
@@ -86,16 +94,26 @@ def agent_events(
86
  }
87
  history = minicpm.summarize_history(history)
88
  summarized = True
 
 
 
 
89
 
90
  msgs = [{"role": m["role"], "content": [m["content"]]} for m in history]
91
  msgs.append({"role": "user", "content": [question]})
92
 
93
  trace: list[str] = [] # text record of tool calls for the durable history
94
  answer = None
95
- for _ in range(AGENT_MAX_STEPS):
96
  yield {"type": "status", "text": "Thinking…"}
97
  reply = minicpm.generate_step(msgs, manual)
98
  call = minicpm.parse_tool_call(reply)
 
 
 
 
 
 
99
  if call is None:
100
  answer = reply
101
  break
@@ -105,6 +123,10 @@ def agent_events(
105
  query = str(call["args"].get("query") or "").strip() or question
106
  yield {"type": "tool_call", "tool": "search_docs", "args": {"query": query}}
107
  refs = search(query, store, doc_ids, top_k)
 
 
 
 
108
  pages = [
109
  (f"{names[doc_id]} — p.{page}", render_page(store.pdf_path(doc_id), page))
110
  for doc_id, page, _ in refs
@@ -157,6 +179,10 @@ def agent_events(
157
  trace.append(f"[displayed page {page} in the viewer]")
158
 
159
  if answer is None: # tool budget exhausted: force a plain answer
 
 
 
 
160
  yield {"type": "status", "text": "Wrapping up…"}
161
  msgs.append(
162
  {
@@ -169,6 +195,7 @@ def agent_events(
169
  )
170
  answer = minicpm.generate_step(msgs, manual)
171
  if minicpm.parse_tool_call(answer) is not None:
 
172
  answer = GIVE_UP_ANSWER
173
 
174
  durable = ("\n".join(trace) + "\n\n" if trace else "") + answer
 
31
 
32
  from __future__ import annotations
33
 
34
+ import logging
35
+
36
  import spaces
37
 
38
  from core.constants import (
 
44
  from core.pdf import render_page
45
  from models import minicpm
46
 
47
+ log = logging.getLogger("repairguy.agent")
48
+
49
  GIVE_UP_ANSWER = (
50
  "I couldn't put together a grounded answer within my tool budget — try "
51
  "rephrasing the question or pointing me at a section of the manual."
 
81
  summarized = False
82
  if (
83
  len(history) > HISTORY_KEEP_MESSAGES
84
+ and (tokens := minicpm.history_tokens(history)) > HISTORY_TOKEN_BUDGET
85
  ):
86
+ log.info(
87
+ "history %d msgs / %d tokens over budget %d — summarizing",
88
+ len(history), tokens, HISTORY_TOKEN_BUDGET,
89
+ )
90
  yield {
91
  "type": "status",
92
  "kind": "summarizing",
 
94
  }
95
  history = minicpm.summarize_history(history)
96
  summarized = True
97
+ log.info(
98
+ "summarized to %d msgs / %d tokens",
99
+ len(history), minicpm.history_tokens(history),
100
+ )
101
 
102
  msgs = [{"role": m["role"], "content": [m["content"]]} for m in history]
103
  msgs.append({"role": "user", "content": [question]})
104
 
105
  trace: list[str] = [] # text record of tool calls for the durable history
106
  answer = None
107
+ for step in range(1, AGENT_MAX_STEPS + 1):
108
  yield {"type": "status", "text": "Thinking…"}
109
  reply = minicpm.generate_step(msgs, manual)
110
  call = minicpm.parse_tool_call(reply)
111
+ log.info(
112
+ "step %d/%d: %s | reply=%r",
113
+ step, AGENT_MAX_STEPS,
114
+ f"tool_call {call['tool']} {call['args']}" if call else "final answer",
115
+ reply[:200],
116
+ )
117
  if call is None:
118
  answer = reply
119
  break
 
123
  query = str(call["args"].get("query") or "").strip() or question
124
  yield {"type": "tool_call", "tool": "search_docs", "args": {"query": query}}
125
  refs = search(query, store, doc_ids, top_k)
126
+ log.info(
127
+ "search_docs(%r) → %s",
128
+ query, [(d, p, round(s, 3)) for d, p, s in refs],
129
+ )
130
  pages = [
131
  (f"{names[doc_id]} — p.{page}", render_page(store.pdf_path(doc_id), page))
132
  for doc_id, page, _ in refs
 
179
  trace.append(f"[displayed page {page} in the viewer]")
180
 
181
  if answer is None: # tool budget exhausted: force a plain answer
182
+ log.warning(
183
+ "tool budget exhausted after %d steps — forcing a plain answer",
184
+ AGENT_MAX_STEPS,
185
+ )
186
  yield {"type": "status", "text": "Wrapping up…"}
187
  msgs.append(
188
  {
 
195
  )
196
  answer = minicpm.generate_step(msgs, manual)
197
  if minicpm.parse_tool_call(answer) is not None:
198
+ log.warning("forced answer was still a tool call — using give-up text")
199
  answer = GIVE_UP_ANSWER
200
 
201
  durable = ("\n".join(trace) + "\n\n" if trace else "") + answer