GT5557 commited on
Commit
9320c58
Β·
verified Β·
1 Parent(s): db0987b

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +103 -14
agent.py CHANGED
@@ -121,7 +121,60 @@ def reverse_text(text: str) -> str:
121
  return text[::-1]
122
 
123
 
124
- TOOLS = [wiki_search, web_search, fetch_page, run_python, reverse_text]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  # ==========================================================
127
  # MODELS β€” primary + ordered fallback chain
@@ -153,6 +206,9 @@ SYSTEM_PROMPT = """You are a precise benchmark task solver.
153
  Produce the exact correct answer β€” nothing more, nothing less.
154
 
155
  ## Tool use
 
 
 
156
  - Use wiki_search for historical facts, biographies, science, geography.
157
  - Use web_search for recent events, specific articles, prices, or anything time-sensitive.
158
  - Use fetch_page when a URL is provided or a search result points to a relevant page.
@@ -184,10 +240,10 @@ Produce the exact correct answer β€” nothing more, nothing less.
184
  - Vegetables (botanical): true vegetables are leaves (lettuce, spinach), stems (celery), roots
185
  (carrot, sweet potato), bulbs (onion), or flowers (broccoli, cauliflower).
186
  - Do NOT confuse culinary and botanical definitions. A tomato is a fruit botanically.
187
- 10. Never abbreviate. Always write full words:
188
- - City/place names in full: "Ho Chi Minh City" not "HCMC", "Saint Petersburg" not "St. Petersburg"
189
- - Country names in full: "United States" not "US" or "USA", "United Kingdom" not "UK"
190
- - Exception: only use an abbreviation if the question itself uses it or explicitly asks for it.
191
 
192
  ## Required final line
193
  Always end your response with exactly:
@@ -254,7 +310,19 @@ def _answer_looks_weak(result) -> bool:
254
  # INVOKE β€” exception fallback + content-quality fallback
255
  # ==========================================================
256
 
257
- def invoke(messages: list) -> object:
 
 
 
 
 
 
 
 
 
 
 
 
258
  global LAST_MODEL_USED, LAST_MODEL_FALLBACK, LAST_MODEL_ERROR
259
 
260
  LAST_MODEL_FALLBACK = "No"
@@ -279,26 +347,31 @@ def invoke(messages: list) -> object:
279
  result = model.bind_tools(TOOLS).invoke(messages)
280
  last_result = result
281
 
282
- # If the model wants tool calls, return immediately β€”
283
- # the graph will handle the tool execution and loop back
284
  tool_calls = getattr(result, "tool_calls", None)
285
  if tool_calls:
286
  return result
287
 
288
- # No tool calls β€” check if the answer is actually useful
 
 
 
 
 
289
  if not _answer_looks_weak(result):
290
  return result
291
 
292
- # Answer is weak (N/A or missing FINAL ANSWER) β€” try next model
293
  LAST_MODEL_FALLBACK = "Yes"
294
  LAST_MODEL_ERROR = f"weak answer from {key}"
 
295
  continue
296
 
297
  except Exception as e:
298
  LAST_MODEL_ERROR = str(e)
 
299
  continue
300
 
301
- # All models tried β€” return whatever the last one gave us
302
  if last_result is not None:
303
  return last_result
304
 
@@ -316,8 +389,16 @@ def assistant(state: MessagesState) -> dict:
316
  if direct is not None:
317
  return {"messages": [AIMessage(content=f"FINAL ANSWER: {direct}")]}
318
 
 
 
 
 
 
 
 
 
319
  messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
320
- result = invoke(messages)
321
  return {"messages": [result]}
322
 
323
 
@@ -399,13 +480,21 @@ def _expand_abbreviation(answer: str) -> str:
399
  """
400
  If the entire answer is a known abbreviation, replace it with the full form.
401
  Also expands 'St. <Name>' β†’ 'Saint <Name>' for city/place names.
402
- Does NOT modify answers that are longer than a single token/short phrase,
403
- to avoid corrupting sentences that happen to contain an abbreviation.
 
 
 
404
  """
405
  # Only act on short answers (≀ 5 words) to stay safe
406
  if len(answer.split()) > 5:
407
  return answer
408
 
 
 
 
 
 
409
  # Whole-answer lookup (case-insensitive)
410
  lookup = answer.lower().strip(".")
411
  if lookup in _ABBREV_MAP:
 
121
  return text[::-1]
122
 
123
 
124
+ @tool
125
+ def youtube_transcript(url: str) -> str:
126
+ """
127
+ Fetch the transcript of a YouTube video.
128
+ Use this for ANY question that references a YouTube URL or asks about
129
+ what was said, shown, or happened in a video.
130
+ Provide the full YouTube URL (e.g. https://www.youtube.com/watch?v=XXXX).
131
+ Returns the transcript text which you can read to answer the question.
132
+ """
133
+ try:
134
+ from youtube_transcript_api import YouTubeTranscriptApi
135
+ from youtube_transcript_api._errors import TranscriptsDisabled, NoTranscriptFound
136
+
137
+ # Extract video ID from various YouTube URL formats
138
+ match = re.search(
139
+ r'(?:v=|youtu\.be/|embed/|shorts/)([A-Za-z0-9_-]{11})',
140
+ url
141
+ )
142
+ if not match:
143
+ return "Could not extract video ID from URL. Check the URL format."
144
+
145
+ video_id = match.group(1)
146
+
147
+ # Try English first, then any available language
148
+ try:
149
+ transcript_list = YouTubeTranscriptApi.get_transcript(
150
+ video_id, languages=["en"]
151
+ )
152
+ except NoTranscriptFound:
153
+ # Fall back to auto-generated or any other available transcript
154
+ transcripts = YouTubeTranscriptApi.list_transcripts(video_id)
155
+ transcript_list = transcripts.find_generated_transcript(
156
+ ["en", "en-US", "en-GB"]
157
+ ).fetch()
158
+
159
+ if not transcript_list:
160
+ return "Transcript is empty."
161
+
162
+ # Join all text segments into a readable block
163
+ full_text = " ".join(
164
+ entry["text"].strip()
165
+ for entry in transcript_list
166
+ if entry.get("text", "").strip()
167
+ )
168
+
169
+ return full_text[:8000] if full_text else "Transcript appears empty."
170
+
171
+ except TranscriptsDisabled:
172
+ return "Transcripts are disabled for this video. Try web_search for the video title to find a summary."
173
+ except Exception as e:
174
+ return f"Transcript unavailable: {type(e).__name__}: {e}. Try web_search for the video content instead."
175
+
176
+
177
+ TOOLS = [wiki_search, web_search, fetch_page, run_python, reverse_text, youtube_transcript]
178
 
179
  # ==========================================================
180
  # MODELS β€” primary + ordered fallback chain
 
206
  Produce the exact correct answer β€” nothing more, nothing less.
207
 
208
  ## Tool use
209
+ - Use youtube_transcript for ANY question that includes a YouTube URL or asks
210
+ about video content (what was said, what happened, species shown, quotes, etc.).
211
+ Always try youtube_transcript FIRST before web_search for YouTube questions.
212
  - Use wiki_search for historical facts, biographies, science, geography.
213
  - Use web_search for recent events, specific articles, prices, or anything time-sensitive.
214
  - Use fetch_page when a URL is provided or a search result points to a relevant page.
 
240
  - Vegetables (botanical): true vegetables are leaves (lettuce, spinach), stems (celery), roots
241
  (carrot, sweet potato), bulbs (onion), or flowers (broccoli, cauliflower).
242
  - Do NOT confuse culinary and botanical definitions. A tomato is a fruit botanically.
243
+ 10. Never abbreviate UNLESS the question explicitly asks for an abbreviation or code:
244
+ - If asked for an IOC country code: return the 3-letter code (e.g. CUB, GBR, EGY).
245
+ - If asked for an ISO code, airport code, or similar: return the code as-is.
246
+ - Otherwise write full words: "United States" not "US", "Saint Petersburg" not "St. Petersburg".
247
 
248
  ## Required final line
249
  Always end your response with exactly:
 
310
  # INVOKE β€” exception fallback + content-quality fallback
311
  # ==========================================================
312
 
313
+ def invoke(messages: list, is_final: bool = False) -> object:
314
+ """
315
+ Call models in fallback order.
316
+
317
+ is_final=False β†’ first turn (no tool results yet). Use Qwen only.
318
+ If Qwen wants tools, return immediately.
319
+ If Qwen gives a weak answer on the first turn,
320
+ still return it β€” the graph hasn't searched yet,
321
+ so a weak answer just means it will call a tool next.
322
+ is_final=True β†’ the model has already used tools and is giving its
323
+ terminal answer. If that answer is weak, try the
324
+ next model in the chain.
325
+ """
326
  global LAST_MODEL_USED, LAST_MODEL_FALLBACK, LAST_MODEL_ERROR
327
 
328
  LAST_MODEL_FALLBACK = "No"
 
347
  result = model.bind_tools(TOOLS).invoke(messages)
348
  last_result = result
349
 
350
+ # Model wants to call a tool β€” return immediately regardless of turn
 
351
  tool_calls = getattr(result, "tool_calls", None)
352
  if tool_calls:
353
  return result
354
 
355
+ # Not a final turn β€” don't burn fallback models on intermediate steps.
356
+ # Return whatever Qwen gave; the graph will loop or terminate naturally.
357
+ if not is_final:
358
+ return result
359
+
360
+ # Final turn β€” check answer quality and try next model if weak
361
  if not _answer_looks_weak(result):
362
  return result
363
 
 
364
  LAST_MODEL_FALLBACK = "Yes"
365
  LAST_MODEL_ERROR = f"weak answer from {key}"
366
+ time.sleep(0.5) # brief pause to avoid 413/429 on rapid retries
367
  continue
368
 
369
  except Exception as e:
370
  LAST_MODEL_ERROR = str(e)
371
+ time.sleep(0.5)
372
  continue
373
 
374
+ # All models tried β€” return the last result rather than raising
375
  if last_result is not None:
376
  return last_result
377
 
 
389
  if direct is not None:
390
  return {"messages": [AIMessage(content=f"FINAL ANSWER: {direct}")]}
391
 
392
+ # Detect whether this is a final turn (tool results already in history).
393
+ # If so, the model should be giving its terminal answer β€” activate fallback
394
+ # chain if that answer is weak.
395
+ is_final = any(
396
+ getattr(m, "type", "") == "tool"
397
+ for m in state["messages"]
398
+ )
399
+
400
  messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
401
+ result = invoke(messages, is_final=is_final)
402
  return {"messages": [result]}
403
 
404
 
 
480
  """
481
  If the entire answer is a known abbreviation, replace it with the full form.
482
  Also expands 'St. <Name>' β†’ 'Saint <Name>' for city/place names.
483
+
484
+ Does NOT expand:
485
+ - IOC country codes (3 uppercase letters like CUB, EGY, GBR) β€” these are
486
+ intentional when the question asks for the IOC code.
487
+ - Answers longer than 5 words β€” too risky to mutate longer text.
488
  """
489
  # Only act on short answers (≀ 5 words) to stay safe
490
  if len(answer.split()) > 5:
491
  return answer
492
 
493
+ # Never expand 3-letter ALL-CAPS strings β€” these are almost certainly
494
+ # IOC codes, ISO codes, or other intentional abbreviations the question asked for
495
+ if re.fullmatch(r'[A-Z]{3}', answer.strip()):
496
+ return answer
497
+
498
  # Whole-answer lookup (case-insensitive)
499
  lookup = answer.lower().strip(".")
500
  if lookup in _ABBREV_MAP: