Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -20,8 +20,6 @@ from langchain_core.messages import HumanMessage, SystemMessage
|
|
| 20 |
from langchain_core.tools import tool
|
| 21 |
from langchain_groq import ChatGroq
|
| 22 |
from langchain_community.tools import DuckDuckGoSearchRun
|
| 23 |
-
from langchain_community.tools.wikipedia.tool import WikipediaQueryRun
|
| 24 |
-
from langchain_community.utilities.wikipedia import WikipediaAPIWrapper
|
| 25 |
from langgraph.graph import END, StateGraph
|
| 26 |
|
| 27 |
|
|
@@ -119,7 +117,6 @@ def _is_image(ct: str, data: bytes) -> bool:
|
|
| 119 |
return True
|
| 120 |
if data.startswith(b"GIF87a") or data.startswith(b"GIF89a"):
|
| 121 |
return True
|
| 122 |
-
# RIFF can also be WAV, so verify WEBP signature.
|
| 123 |
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
|
| 124 |
return True
|
| 125 |
return False
|
|
@@ -144,6 +141,11 @@ def _is_audio_or_video(ct: str, path: str) -> bool:
|
|
| 144 |
return ct.startswith("audio/") or ct.startswith("video/") or ext in AUDIO_VIDEO_EXTS
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
def detect_local_file_kind(task_id: str) -> tuple[str, str | None]:
|
| 148 |
local_path = get_task_file(task_id)
|
| 149 |
if not local_path:
|
|
@@ -347,11 +349,6 @@ def run_python_file(path: Path, timeout_seconds: int = 8) -> str:
|
|
| 347 |
return f"ERROR: Code execution failed: {type(e).__name__}: {e}"
|
| 348 |
|
| 349 |
web_search_tool = DuckDuckGoSearchRun(name="web_search")
|
| 350 |
-
wikipedia_tool = WikipediaQueryRun(
|
| 351 |
-
name="wikipedia",
|
| 352 |
-
api_wrapper=WikipediaAPIWrapper(top_k_results=3, doc_content_chars_max=3000),
|
| 353 |
-
)
|
| 354 |
-
|
| 355 |
|
| 356 |
def safe_tool_run(tool_obj: Any, query: str, limit: int = 6000) -> str:
|
| 357 |
try:
|
|
@@ -398,6 +395,15 @@ def is_bad_answer(answer: str) -> bool:
|
|
| 398 |
"not enough information",
|
| 399 |
"unable to answer",
|
| 400 |
"no answer",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
]
|
| 402 |
return any(m in a for m in bad_markers)
|
| 403 |
|
|
@@ -408,7 +414,7 @@ def reversed_english_question(question: str) -> bool:
|
|
| 408 |
return sum(1 for m in markers if m in rev) >= 2
|
| 409 |
|
| 410 |
|
| 411 |
-
def
|
| 412 |
q = question.strip()
|
| 413 |
|
| 414 |
if reversed_english_question(q):
|
|
@@ -465,6 +471,11 @@ def last_nonempty_line(text: str) -> str:
|
|
| 465 |
return lines[-1] if lines else ""
|
| 466 |
|
| 467 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
class AgentState(TypedDict):
|
| 469 |
question: str
|
| 470 |
task_id: str
|
|
@@ -558,6 +569,8 @@ class BasicAgent:
|
|
| 558 |
route = "solve_direct"
|
| 559 |
elif direct_question(question):
|
| 560 |
route = "solve_direct"
|
|
|
|
|
|
|
| 561 |
else:
|
| 562 |
route = "solve_research"
|
| 563 |
|
|
@@ -655,7 +668,7 @@ class BasicAgent:
|
|
| 655 |
file_kind = state.get("file_kind", "none")
|
| 656 |
local_path = state.get("local_path")
|
| 657 |
|
| 658 |
-
shortcut =
|
| 659 |
if shortcut is not None:
|
| 660 |
return {"context": "Solved by deterministic Python shortcut.", "raw_answer": shortcut}
|
| 661 |
|
|
@@ -678,29 +691,60 @@ class BasicAgent:
|
|
| 678 |
print(f"[research query] {query}")
|
| 679 |
|
| 680 |
web_results = safe_tool_run(web_search_tool, query, limit=5000)
|
| 681 |
-
wiki_results = safe_tool_run(wikipedia_tool, query, limit=5000)
|
| 682 |
|
| 683 |
print(f"[web len] {len(web_results)}")
|
| 684 |
-
print(f"[wiki len] {len(wiki_results)}")
|
| 685 |
print(f"[web preview] {repr(web_results[:500])}")
|
| 686 |
-
print(f"[wiki preview] {repr(wiki_results[:500])}")
|
| 687 |
|
| 688 |
context = truncate_text(
|
| 689 |
-
f"Search query:\n{query}\n\n"
|
| 690 |
-
f"Web search results:\n{web_results}\n\n"
|
| 691 |
-
f"Wikipedia results:\n{wiki_results}",
|
| 692 |
MAX_SEARCH_CONTEXT_CHARS,
|
| 693 |
)
|
| 694 |
|
| 695 |
raw_answer = self.answer_from_context(
|
| 696 |
question=question,
|
| 697 |
context=context,
|
| 698 |
-
context_label="Web
|
| 699 |
llm=self.research_llm,
|
| 700 |
)
|
| 701 |
|
| 702 |
print(f"[research raw_answer] {repr(raw_answer[:500])}")
|
| 703 |
return {"context": context, "raw_answer": raw_answer}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
|
| 705 |
def verify_answer(self, state: AgentState) -> dict[str, Any]:
|
| 706 |
question = state.get("question", "")
|
|
@@ -909,7 +953,7 @@ with gr.Blocks() as demo:
|
|
| 909 |
"""
|
| 910 |
**Architecture:** `classify_task β route_by_type β solve_* β verify_answer β final_cleaner`.
|
| 911 |
|
| 912 |
-
Local files are routed deterministically by Python. Web
|
| 913 |
"""
|
| 914 |
)
|
| 915 |
|
|
|
|
| 20 |
from langchain_core.tools import tool
|
| 21 |
from langchain_groq import ChatGroq
|
| 22 |
from langchain_community.tools import DuckDuckGoSearchRun
|
|
|
|
|
|
|
| 23 |
from langgraph.graph import END, StateGraph
|
| 24 |
|
| 25 |
|
|
|
|
| 117 |
return True
|
| 118 |
if data.startswith(b"GIF87a") or data.startswith(b"GIF89a"):
|
| 119 |
return True
|
|
|
|
| 120 |
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
|
| 121 |
return True
|
| 122 |
return False
|
|
|
|
| 141 |
return ct.startswith("audio/") or ct.startswith("video/") or ext in AUDIO_VIDEO_EXTS
|
| 142 |
|
| 143 |
|
| 144 |
+
def is_youtube_question(question: str) -> bool:
|
| 145 |
+
q = question.lower()
|
| 146 |
+
return "youtube.com/watch" in q or "youtu.be/" in q
|
| 147 |
+
|
| 148 |
+
|
| 149 |
def detect_local_file_kind(task_id: str) -> tuple[str, str | None]:
|
| 150 |
local_path = get_task_file(task_id)
|
| 151 |
if not local_path:
|
|
|
|
| 349 |
return f"ERROR: Code execution failed: {type(e).__name__}: {e}"
|
| 350 |
|
| 351 |
web_search_tool = DuckDuckGoSearchRun(name="web_search")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
def safe_tool_run(tool_obj: Any, query: str, limit: int = 6000) -> str:
|
| 354 |
try:
|
|
|
|
| 395 |
"not enough information",
|
| 396 |
"unable to answer",
|
| 397 |
"no answer",
|
| 398 |
+
"no answer found",
|
| 399 |
+
"i could not find",
|
| 400 |
+
"could not find",
|
| 401 |
+
"not found",
|
| 402 |
+
"not in the search results",
|
| 403 |
+
"this answer is not",
|
| 404 |
+
"unknown",
|
| 405 |
+
"insufficient information",
|
| 406 |
+
"cannot determine",
|
| 407 |
]
|
| 408 |
return any(m in a for m in bad_markers)
|
| 409 |
|
|
|
|
| 414 |
return sum(1 for m in markers if m in rev) >= 2
|
| 415 |
|
| 416 |
|
| 417 |
+
def solve_directly_with_python(question: str) -> str | None:
|
| 418 |
q = question.strip()
|
| 419 |
|
| 420 |
if reversed_english_question(q):
|
|
|
|
| 471 |
return lines[-1] if lines else ""
|
| 472 |
|
| 473 |
|
| 474 |
+
def extract_youtube_id(question: str) -> str | None:
|
| 475 |
+
m = re.search(r"(?:v=|youtu\.be/)([A-Za-z0-9_-]{11})", question)
|
| 476 |
+
return m.group(1) if m else None
|
| 477 |
+
|
| 478 |
+
|
| 479 |
class AgentState(TypedDict):
|
| 480 |
question: str
|
| 481 |
task_id: str
|
|
|
|
| 569 |
route = "solve_direct"
|
| 570 |
elif direct_question(question):
|
| 571 |
route = "solve_direct"
|
| 572 |
+
elif is_youtube_question(question):
|
| 573 |
+
route = "solve_youtube"
|
| 574 |
else:
|
| 575 |
route = "solve_research"
|
| 576 |
|
|
|
|
| 668 |
file_kind = state.get("file_kind", "none")
|
| 669 |
local_path = state.get("local_path")
|
| 670 |
|
| 671 |
+
shortcut = solve_directly_with_python(question)
|
| 672 |
if shortcut is not None:
|
| 673 |
return {"context": "Solved by deterministic Python shortcut.", "raw_answer": shortcut}
|
| 674 |
|
|
|
|
| 691 |
print(f"[research query] {query}")
|
| 692 |
|
| 693 |
web_results = safe_tool_run(web_search_tool, query, limit=5000)
|
|
|
|
| 694 |
|
| 695 |
print(f"[web len] {len(web_results)}")
|
|
|
|
| 696 |
print(f"[web preview] {repr(web_results[:500])}")
|
|
|
|
| 697 |
|
| 698 |
context = truncate_text(
|
| 699 |
+
f"Search query:\n{query}\n\n",
|
| 700 |
+
f"Web search results:\n{web_results}\n\n",
|
|
|
|
| 701 |
MAX_SEARCH_CONTEXT_CHARS,
|
| 702 |
)
|
| 703 |
|
| 704 |
raw_answer = self.answer_from_context(
|
| 705 |
question=question,
|
| 706 |
context=context,
|
| 707 |
+
context_label="Web research results",
|
| 708 |
llm=self.research_llm,
|
| 709 |
)
|
| 710 |
|
| 711 |
print(f"[research raw_answer] {repr(raw_answer[:500])}")
|
| 712 |
return {"context": context, "raw_answer": raw_answer}
|
| 713 |
+
|
| 714 |
+
def solve_youtube(self, state: AgentState) -> dict:
|
| 715 |
+
question = state["question"]
|
| 716 |
+
video_id = extract_youtube_id(question)
|
| 717 |
+
|
| 718 |
+
queries = []
|
| 719 |
+
|
| 720 |
+
if video_id:
|
| 721 |
+
queries += [
|
| 722 |
+
f'"{video_id}" transcript',
|
| 723 |
+
f'"{video_id}" subtitles',
|
| 724 |
+
f'"{video_id}" "{question[:80]}"',
|
| 725 |
+
]
|
| 726 |
+
|
| 727 |
+
queries.append(question)
|
| 728 |
+
|
| 729 |
+
parts = []
|
| 730 |
+
|
| 731 |
+
for query in queries:
|
| 732 |
+
result = safe_tool_run(web_search_tool, query, limit=5000)
|
| 733 |
+
parts.append(f"Query: {query}\nResults:\n{result}")
|
| 734 |
+
|
| 735 |
+
context = "\n\n---\n\n".join(parts)[:16000]
|
| 736 |
+
|
| 737 |
+
raw_answer = self.answer_from_context(
|
| 738 |
+
question=question,
|
| 739 |
+
context=context,
|
| 740 |
+
context_label="YouTube/web transcript search results",
|
| 741 |
+
llm=self.research_llm,
|
| 742 |
+
)
|
| 743 |
+
|
| 744 |
+
return {
|
| 745 |
+
"context": context,
|
| 746 |
+
"raw_answer": raw_answer,
|
| 747 |
+
}
|
| 748 |
|
| 749 |
def verify_answer(self, state: AgentState) -> dict[str, Any]:
|
| 750 |
question = state.get("question", "")
|
|
|
|
| 953 |
"""
|
| 954 |
**Architecture:** `classify_task β route_by_type β solve_* β verify_answer β final_cleaner`.
|
| 955 |
|
| 956 |
+
Local files are routed deterministically by Python. Web are called only inside `solve_research`, without automatic LLM tool-calling.
|
| 957 |
"""
|
| 958 |
)
|
| 959 |
|