Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,6 +7,7 @@ import os
|
|
| 7 |
import re
|
| 8 |
import subprocess
|
| 9 |
import sys
|
|
|
|
| 10 |
import time
|
| 11 |
from functools import lru_cache
|
| 12 |
from pathlib import Path
|
|
@@ -29,32 +30,67 @@ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
|
|
| 29 |
ERROR_PREFIX = "ОШИБКА:"
|
| 30 |
|
| 31 |
GROQ_TEXT_MODEL = os.getenv("GROQ_TEXT_MODEL", "llama-3.1-8b-instant")
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
GROQ_VISION_MODEL = os.getenv("GROQ_VISION_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct")
|
| 36 |
GROQ_AUDIO_MODEL = os.getenv("GROQ_AUDIO_MODEL", "whisper-large-v3-turbo")
|
| 37 |
|
| 38 |
GAIA_DIR = os.getenv("GAIA_DIR", "./data/gaia")
|
| 39 |
-
ALLOW_CODE_EXECUTION = os.getenv("ALLOW_CODE_EXECUTION", "1").lower() not in {
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
MAX_CONTEXT_CHARS = int(os.getenv("MAX_CONTEXT_CHARS", "9000"))
|
| 43 |
MAX_SEARCH_CONTEXT_CHARS = int(os.getenv("MAX_SEARCH_CONTEXT_CHARS", "7000"))
|
| 44 |
SEARCH_RESULTS_PER_QUERY = int(os.getenv("SEARCH_RESULTS_PER_QUERY", "4"))
|
| 45 |
SEARCH_FETCH_LIMIT = int(os.getenv("SEARCH_FETCH_LIMIT", "2200"))
|
|
|
|
|
|
|
| 46 |
|
| 47 |
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
|
| 48 |
-
AUDIO_VIDEO_EXTS = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
SPREADSHEET_EXTS = {".xlsx", ".xls"}
|
| 50 |
PDF_EXTS = {".pdf"}
|
| 51 |
CODE_EXTS = {".py", ".js", ".ts", ".java", ".cpp", ".c", ".rb", ".go", ".rs"}
|
| 52 |
-
TEXT_EXTS = {
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
|
| 60 |
def error_text(message: str, exc: Exception | None = None) -> str:
|
|
@@ -83,14 +119,15 @@ def get_task_file_map() -> dict[str, str]:
|
|
| 83 |
validation_dir = Path(GAIA_DIR) / "2023" / "validation"
|
| 84 |
|
| 85 |
if not validation_dir.exists():
|
| 86 |
-
|
|
|
|
|
|
|
| 87 |
return result
|
| 88 |
|
| 89 |
for path in validation_dir.rglob("*"):
|
| 90 |
if path.is_file() and path.suffix.lower() != ".parquet":
|
| 91 |
result[path.stem] = str(path)
|
| 92 |
|
| 93 |
-
logger.info("Найдено локальных файлов GAIA: %s (%s)", len(result), validation_dir)
|
| 94 |
return result
|
| 95 |
|
| 96 |
|
|
@@ -183,9 +220,14 @@ def analyze_image(task_id: str, question: str = "") -> str:
|
|
| 183 |
return error_text(f"не удалось открыть изображение для task_id={task_id}", exc)
|
| 184 |
|
| 185 |
if not is_image(content_type, data):
|
| 186 |
-
return error_text(
|
|
|
|
|
|
|
| 187 |
|
| 188 |
-
prompt =
|
|
|
|
|
|
|
|
|
|
| 189 |
if "chess" in prompt.lower():
|
| 190 |
prompt += (
|
| 191 |
"\n\nThis is a chess task. Identify board coordinates, side to move, relevant pieces, "
|
|
@@ -219,6 +261,30 @@ def analyze_image(task_id: str, question: str = "") -> str:
|
|
| 219 |
return error_text("ошибка vision-модели", exc)
|
| 220 |
|
| 221 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
@tool
|
| 223 |
def transcribe_audio(task_id: str) -> str:
|
| 224 |
"""Transcribe the GAIA audio or video file attached to a task and return the transcript."""
|
|
@@ -226,24 +292,18 @@ def transcribe_audio(task_id: str) -> str:
|
|
| 226 |
data, content_type = fetch_task_bytes(task_id)
|
| 227 |
local_path = get_task_file(task_id) or ""
|
| 228 |
except Exception as exc:
|
| 229 |
-
return error_text(
|
|
|
|
|
|
|
| 230 |
|
| 231 |
if not is_audio_or_video(content_type, local_path):
|
| 232 |
-
return error_text(
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
if suffix == "mpeg":
|
| 236 |
-
suffix = "mp3"
|
| 237 |
|
| 238 |
try:
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
transcription = client.audio.transcriptions.create(
|
| 242 |
-
file=audio_file,
|
| 243 |
-
model=GROQ_AUDIO_MODEL,
|
| 244 |
-
response_format="text",
|
| 245 |
-
)
|
| 246 |
-
return str(transcription).strip()
|
| 247 |
except Exception as exc:
|
| 248 |
return error_text("ошибка транскрибации аудио", exc)
|
| 249 |
|
|
@@ -289,7 +349,9 @@ def read_pdf_context(path: Path) -> str:
|
|
| 289 |
try:
|
| 290 |
text = page.extract_text() or ""
|
| 291 |
except Exception as exc:
|
| 292 |
-
text =
|
|
|
|
|
|
|
| 293 |
parts.append(f"\n--- Page {index} ---\n{text}")
|
| 294 |
if len("\n".join(parts)) > MAX_CONTEXT_CHARS:
|
| 295 |
break
|
|
@@ -347,14 +409,20 @@ def build_spreadsheet_summary(path: Path) -> str:
|
|
| 347 |
try:
|
| 348 |
df = pd.read_excel(path, sheet_name=sheet_name)
|
| 349 |
except Exception as exc:
|
| 350 |
-
parts.append(
|
|
|
|
|
|
|
| 351 |
continue
|
| 352 |
if df.empty:
|
| 353 |
continue
|
| 354 |
|
| 355 |
work = df.copy()
|
| 356 |
work.columns = [str(column).strip() for column in work.columns]
|
| 357 |
-
numeric_cols = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
categorical_cols = [
|
| 359 |
column
|
| 360 |
for column in work.columns
|
|
@@ -363,14 +431,18 @@ def build_spreadsheet_summary(path: Path) -> str:
|
|
| 363 |
|
| 364 |
parts.append(f"Sheet: {sheet_name}")
|
| 365 |
if numeric_cols:
|
| 366 |
-
totals =
|
|
|
|
|
|
|
| 367 |
parts.append("Numeric column totals:")
|
| 368 |
parts.append(totals.to_string())
|
| 369 |
|
| 370 |
for category_col in categorical_cols[:6]:
|
| 371 |
if not numeric_cols:
|
| 372 |
break
|
| 373 |
-
grouped = work.groupby(category_col, dropna=False)[numeric_cols].sum(
|
|
|
|
|
|
|
| 374 |
if not grouped.empty:
|
| 375 |
parts.append(f"Totals grouped by {category_col}:")
|
| 376 |
parts.append(grouped.head(40).to_csv())
|
|
@@ -456,8 +528,7 @@ def likely_relevant_url(url: str) -> bool:
|
|
| 456 |
def ddg_search(query: str, max_results: int = 5) -> list[dict[str, str]]:
|
| 457 |
try:
|
| 458 |
results = DDGS().text(query, max_results=max_results)
|
| 459 |
-
except Exception
|
| 460 |
-
logger.warning("DuckDuckGo-поиск не сработал: %s: %s", type(exc).__name__, exc)
|
| 461 |
return []
|
| 462 |
|
| 463 |
normalized: list[dict[str, str]] = []
|
|
@@ -481,7 +552,9 @@ def solve_reversed_english_task(question: str) -> str | None:
|
|
| 481 |
return None
|
| 482 |
|
| 483 |
reversed_text = question[::-1]
|
| 484 |
-
match = re.search(
|
|
|
|
|
|
|
| 485 |
if not match:
|
| 486 |
return None
|
| 487 |
|
|
@@ -502,10 +575,14 @@ def solve_reversed_english_task(question: str) -> str | None:
|
|
| 502 |
|
| 503 |
def solve_commutativity_table(question: str) -> str | None:
|
| 504 |
lower = question.lower()
|
| 505 |
-
if "|---" not in question or (
|
|
|
|
|
|
|
| 506 |
return None
|
| 507 |
|
| 508 |
-
lines = [
|
|
|
|
|
|
|
| 509 |
if len(lines) < 3:
|
| 510 |
return None
|
| 511 |
|
|
@@ -525,7 +602,11 @@ def solve_commutativity_table(question: str) -> str | None:
|
|
| 525 |
continue
|
| 526 |
left_right = table.get(left, {}).get(right)
|
| 527 |
right_left = table.get(right, {}).get(left)
|
| 528 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
counterexample_elements.update({left, right})
|
| 530 |
|
| 531 |
if counterexample_elements:
|
|
@@ -535,7 +616,11 @@ def solve_commutativity_table(question: str) -> str | None:
|
|
| 535 |
|
| 536 |
def solve_botany_grocery_list(question: str) -> str | None:
|
| 537 |
lower = question.lower()
|
| 538 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
return None
|
| 540 |
|
| 541 |
match = re.search(
|
|
@@ -545,7 +630,9 @@ def solve_botany_grocery_list(question: str) -> str | None:
|
|
| 545 |
if not match:
|
| 546 |
return None
|
| 547 |
|
| 548 |
-
items = [
|
|
|
|
|
|
|
| 549 |
vegetable_names = {
|
| 550 |
"broccoli",
|
| 551 |
"cabbage",
|
|
@@ -579,7 +666,11 @@ def is_direct_question(question: str) -> bool:
|
|
| 579 |
return (
|
| 580 |
is_reversed_english_task(question)
|
| 581 |
or ("|---" in question and ("commutative" in lower or "commutativity" in lower))
|
| 582 |
-
or (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
)
|
| 584 |
|
| 585 |
|
|
@@ -595,7 +686,9 @@ def build_research_queries(question: str, base_query: str) -> list[str]:
|
|
| 595 |
if parsed.netloc:
|
| 596 |
queries.append(f"site:{parsed.netloc} {base_query}")
|
| 597 |
|
| 598 |
-
capitalized_terms = re.findall(
|
|
|
|
|
|
|
| 599 |
if capitalized_terms:
|
| 600 |
queries.append(" ".join(f'"{term}"' for term in capitalized_terms[:4]))
|
| 601 |
|
|
@@ -619,7 +712,9 @@ def build_research_queries(question: str, base_query: str) -> list[str]:
|
|
| 619 |
return deduped[:5]
|
| 620 |
|
| 621 |
|
| 622 |
-
def build_additional_research_queries(
|
|
|
|
|
|
|
| 623 |
messages = [
|
| 624 |
SystemMessage(
|
| 625 |
content=(
|
|
@@ -633,8 +728,7 @@ def build_additional_research_queries(question: str, previous_queries: list[str]
|
|
| 633 |
|
| 634 |
try:
|
| 635 |
raw = llm.invoke(messages).content
|
| 636 |
-
except Exception
|
| 637 |
-
logger.warning("Не удалось расширить поисковые запросы: %s: %s", type(exc).__name__, exc)
|
| 638 |
return []
|
| 639 |
|
| 640 |
previous = {query.lower() for query in previous_queries}
|
|
@@ -659,9 +753,17 @@ def build_research_context(question: str, base_query: str) -> str:
|
|
| 659 |
|
| 660 |
for index, result in enumerate(results, 1):
|
| 661 |
url = result["url"]
|
| 662 |
-
parts.append(
|
|
|
|
|
|
|
| 663 |
|
| 664 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
continue
|
| 666 |
|
| 667 |
seen_urls.add(url)
|
|
@@ -677,7 +779,9 @@ def build_research_context(question: str, base_query: str) -> str:
|
|
| 677 |
return truncate_text("\n".join(parts), MAX_SEARCH_CONTEXT_CHARS)
|
| 678 |
|
| 679 |
|
| 680 |
-
def extend_research_context(
|
|
|
|
|
|
|
| 681 |
extra_queries = build_additional_research_queries(question, [used_query], llm)
|
| 682 |
if not extra_queries:
|
| 683 |
return context
|
|
@@ -692,8 +796,15 @@ def extend_research_context(question: str, context: str, used_query: str, llm: C
|
|
| 692 |
|
| 693 |
for index, result in enumerate(results, 1):
|
| 694 |
url = result["url"]
|
| 695 |
-
parts.append(
|
| 696 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
continue
|
| 698 |
|
| 699 |
seen_urls.add(url)
|
|
@@ -726,36 +837,196 @@ def fetch_youtube_timedtext(video_id: str) -> str:
|
|
| 726 |
|
| 727 |
for url in urls:
|
| 728 |
try:
|
| 729 |
-
response = requests.get(
|
|
|
|
|
|
|
| 730 |
response.raise_for_status()
|
| 731 |
chunks = re.findall(r"<text[^>]*>(.*?)</text>", response.text, flags=re.S)
|
| 732 |
if not chunks:
|
| 733 |
continue
|
| 734 |
-
text = " ".join(
|
|
|
|
|
|
|
| 735 |
text = re.sub(r"\s+", " ", text).strip()
|
| 736 |
if text:
|
| 737 |
return text
|
| 738 |
-
except Exception
|
| 739 |
-
|
| 740 |
|
| 741 |
return ""
|
| 742 |
|
| 743 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
def build_youtube_context(question: str, video_id: str | None) -> str:
|
| 745 |
queries = [question]
|
| 746 |
quoted_phrases = re.findall(r'["“]([^"”]{3,120})["”]', question)
|
| 747 |
|
| 748 |
if video_id:
|
| 749 |
-
queries = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
queries.extend(f'"{video_id}" "{phrase}"' for phrase in quoted_phrases[:3])
|
| 751 |
if quoted_phrases:
|
| 752 |
queries.append(" ".join(f'"{phrase}"' for phrase in quoted_phrases[:3]))
|
| 753 |
|
| 754 |
parts = [f"Question: {question}", f"YouTube video id: {video_id or 'unknown'}"]
|
| 755 |
-
if video_id:
|
| 756 |
-
transcript = fetch_youtube_timedtext(video_id)
|
| 757 |
-
if transcript:
|
| 758 |
-
parts.append(f"\n=== YouTube timedtext transcript ===\n{truncate_text(transcript, 8000)}")
|
| 759 |
|
| 760 |
seen_urls: set[str] = set()
|
| 761 |
total_fetched = 0
|
|
@@ -765,9 +1036,16 @@ def build_youtube_context(question: str, video_id: str | None) -> str:
|
|
| 765 |
|
| 766 |
for index, result in enumerate(results, 1):
|
| 767 |
url = result["url"]
|
| 768 |
-
parts.append(
|
|
|
|
|
|
|
| 769 |
parsed = urlparse(url)
|
| 770 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
continue
|
| 772 |
if "youtube.com" in parsed.netloc or "youtu.be" in parsed.netloc:
|
| 773 |
continue
|
|
@@ -858,20 +1136,20 @@ class AgentState(TypedDict, total=False):
|
|
| 858 |
|
| 859 |
class BasicAgent:
|
| 860 |
def __init__(self) -> None:
|
| 861 |
-
self.
|
| 862 |
-
self.final_llm = make_chat_model(GROQ_FINAL_MODEL, max_tokens=48)
|
| 863 |
-
self.strong_llm = make_chat_model(GROQ_STRONG_MODEL, max_tokens=384)
|
| 864 |
-
self.research_llm = make_chat_model(GROQ_RESEARCH_MODEL, max_tokens=384)
|
| 865 |
self.graph = self.build_graph()
|
| 866 |
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
|
|
|
|
|
|
|
|
|
| 875 |
|
| 876 |
def build_graph(self):
|
| 877 |
graph = StateGraph(AgentState)
|
|
@@ -939,7 +1217,10 @@ class BasicAgent:
|
|
| 939 |
else:
|
| 940 |
route = "solve_research"
|
| 941 |
|
| 942 |
-
|
|
|
|
|
|
|
|
|
|
| 943 |
return {"file_kind": file_kind, "local_path": local_path, "route": route}
|
| 944 |
|
| 945 |
def select_route(self, state: AgentState) -> str:
|
|
@@ -959,7 +1240,7 @@ class BasicAgent:
|
|
| 959 |
question = state.get("question", "")
|
| 960 |
task_id = state.get("task_id", "")
|
| 961 |
context = analyze_image.invoke({"task_id": task_id, "question": question})
|
| 962 |
-
raw_answer = self.answer_from_context(question, context, "Image analysis"
|
| 963 |
return {"context": context, "raw_answer": raw_answer}
|
| 964 |
|
| 965 |
def solve_audio(self, state: AgentState) -> dict[str, Any]:
|
|
@@ -967,14 +1248,18 @@ class BasicAgent:
|
|
| 967 |
task_id = state.get("task_id", "")
|
| 968 |
transcript = transcribe_audio.invoke({"task_id": task_id})
|
| 969 |
context = f"Audio/video transcript:\n{transcript}"
|
| 970 |
-
raw_answer = self.answer_from_context(question, context, "Audio transcript"
|
| 971 |
return {"context": context, "raw_answer": raw_answer}
|
| 972 |
|
| 973 |
def solve_spreadsheet(self, state: AgentState) -> dict[str, Any]:
|
| 974 |
question = state.get("question", "")
|
| 975 |
local_path = state.get("local_path")
|
| 976 |
if not local_path:
|
| 977 |
-
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
|
| 979 |
path = Path(local_path)
|
| 980 |
context = read_spreadsheet_context(path)
|
|
@@ -986,7 +1271,6 @@ class BasicAgent:
|
|
| 986 |
question,
|
| 987 |
context,
|
| 988 |
"Spreadsheet data and computed summary",
|
| 989 |
-
self.strong_llm,
|
| 990 |
)
|
| 991 |
return {"context": context, "raw_answer": raw_answer}
|
| 992 |
|
|
@@ -994,20 +1278,33 @@ class BasicAgent:
|
|
| 994 |
question = state.get("question", "")
|
| 995 |
local_path = state.get("local_path")
|
| 996 |
if not local_path:
|
| 997 |
-
return {
|
|
|
|
|
|
|
| 998 |
|
| 999 |
path = Path(local_path)
|
| 1000 |
code_context = read_code_context(path)
|
| 1001 |
-
execution_context =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1002 |
context = f"{code_context}\n\n--- Execution result ---\n{execution_context}"
|
| 1003 |
|
| 1004 |
-
if
|
| 1005 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
candidate = last_nonempty_line(stdout_block)
|
| 1007 |
if candidate and re.search(r"[-+]?\d", candidate):
|
| 1008 |
return {"context": context, "raw_answer": candidate}
|
| 1009 |
|
| 1010 |
-
raw_answer = self.answer_from_context(
|
|
|
|
|
|
|
| 1011 |
return {"context": context, "raw_answer": raw_answer}
|
| 1012 |
|
| 1013 |
def solve_direct(self, state: AgentState) -> dict[str, Any]:
|
|
@@ -1018,44 +1315,50 @@ class BasicAgent:
|
|
| 1018 |
|
| 1019 |
direct_answer = solve_directly(question)
|
| 1020 |
if direct_answer is not None:
|
| 1021 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 1022 |
|
| 1023 |
context = ""
|
| 1024 |
if local_path and file_kind in {"pdf", "text", "binary"}:
|
| 1025 |
context = read_text_file.invoke({"task_id": task_id})
|
| 1026 |
|
| 1027 |
-
raw_answer = self.answer_from_context(
|
|
|
|
|
|
|
| 1028 |
return {"context": context, "raw_answer": raw_answer}
|
| 1029 |
|
| 1030 |
def solve_research(self, state: AgentState) -> dict[str, Any]:
|
| 1031 |
question = state.get("question", "")
|
| 1032 |
query = self.make_search_query(question)
|
| 1033 |
-
logger.info("Поисковый запрос: %s", query)
|
| 1034 |
-
|
| 1035 |
context = build_research_context(question, query)
|
| 1036 |
-
logger.info("Размер поискового контекста: %s", len(context))
|
| 1037 |
|
| 1038 |
-
raw_answer = self.answer_from_context(question, context, "Web research results"
|
| 1039 |
if is_bad_answer(raw_answer) and ENABLE_RESEARCH_RETRY:
|
| 1040 |
-
context = extend_research_context(question, context, query, self.
|
| 1041 |
-
|
| 1042 |
-
|
|
|
|
| 1043 |
|
| 1044 |
return {"context": context, "raw_answer": raw_answer}
|
| 1045 |
|
| 1046 |
def solve_youtube(self, state: AgentState) -> dict[str, Any]:
|
| 1047 |
question = state.get("question", "")
|
| 1048 |
video_id = extract_youtube_id(question)
|
| 1049 |
-
|
| 1050 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1051 |
|
| 1052 |
if is_bad_answer(raw_answer) and ENABLE_RESEARCH_RETRY:
|
| 1053 |
-
context = extend_research_context(
|
|
|
|
|
|
|
| 1054 |
raw_answer = self.answer_from_context(
|
| 1055 |
-
question,
|
| 1056 |
-
context,
|
| 1057 |
-
"Extended YouTube/web transcript search results",
|
| 1058 |
-
self.strong_llm,
|
| 1059 |
)
|
| 1060 |
|
| 1061 |
return {"context": context, "raw_answer": raw_answer}
|
|
@@ -1070,10 +1373,18 @@ class BasicAgent:
|
|
| 1070 |
if is_bad_answer(raw_answer):
|
| 1071 |
return {"verified_answer": "", "error": raw_answer or "пустой ответ"}
|
| 1072 |
|
| 1073 |
-
if route not in {"solve_research", "solve_youtube"} or file_kind in {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1074 |
return {"verified_answer": raw_answer}
|
| 1075 |
|
| 1076 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1077 |
return {"verified_answer": raw_answer}
|
| 1078 |
|
| 1079 |
messages = [
|
|
@@ -1095,9 +1406,8 @@ class BasicAgent:
|
|
| 1095 |
]
|
| 1096 |
|
| 1097 |
try:
|
| 1098 |
-
verified = self.
|
| 1099 |
-
except Exception
|
| 1100 |
-
logger.warning("Проверка ответа не сработала: %s: %s", type(exc).__name__, exc)
|
| 1101 |
verified = raw_answer
|
| 1102 |
|
| 1103 |
if is_bad_answer(verified):
|
|
@@ -1106,20 +1416,34 @@ class BasicAgent:
|
|
| 1106 |
|
| 1107 |
def final_cleaner(self, state: AgentState) -> dict[str, Any]:
|
| 1108 |
question = state.get("question", "")
|
| 1109 |
-
answer = clean_answer(
|
|
|
|
|
|
|
| 1110 |
|
| 1111 |
if is_bad_answer(answer):
|
| 1112 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 1113 |
|
| 1114 |
if "\n" in answer or len(answer.split()) > 12 or len(answer) > 120:
|
| 1115 |
answer = self.extract_final_answer(question, answer)
|
| 1116 |
|
| 1117 |
answer = clean_answer(answer)
|
| 1118 |
if is_bad_answer(answer):
|
| 1119 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 1120 |
return {"final_answer": answer}
|
| 1121 |
|
| 1122 |
-
def answer_from_context(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1123 |
system = (
|
| 1124 |
"You answer GAIA benchmark questions.\n"
|
| 1125 |
"Return ONLY the final answer: a number, name, word, date, or short phrase.\n"
|
|
@@ -1139,38 +1463,20 @@ class BasicAgent:
|
|
| 1139 |
user = f"Question:\n{question}\n\nFinal answer only:"
|
| 1140 |
|
| 1141 |
try:
|
| 1142 |
-
|
|
|
|
|
|
|
|
|
|
| 1143 |
except Exception as exc:
|
| 1144 |
-
message = str(exc)
|
| 1145 |
-
model_rejected_tools = (
|
| 1146 |
-
"tool_use_failed" in message
|
| 1147 |
-
or "Tool choice is none" in message
|
| 1148 |
-
or "model called a tool" in message
|
| 1149 |
-
)
|
| 1150 |
-
if model_rejected_tools:
|
| 1151 |
-
try:
|
| 1152 |
-
logger.warning("LLM дала tool-use ошибку, повторяю через модель %s", GROQ_TEXT_MODEL)
|
| 1153 |
-
return self.answer_llm.invoke([SystemMessage(content=system), HumanMessage(content=user)]).content.strip()
|
| 1154 |
-
except Exception as fallback_exc:
|
| 1155 |
-
return error_text("ошибка fallback-вызова LLM", fallback_exc)
|
| 1156 |
-
if llm is not self.answer_llm:
|
| 1157 |
-
try:
|
| 1158 |
-
logger.warning("LLM-вызов не сработал, повторяю через легкую модель %s", GROQ_TEXT_MODEL)
|
| 1159 |
-
fallback_user = (
|
| 1160 |
-
f"Question:\n{question}\n\n"
|
| 1161 |
-
f"{context_label}:\n{truncate_text(context, 4000)}\n\n"
|
| 1162 |
-
"Final answer only:"
|
| 1163 |
-
)
|
| 1164 |
-
return self.answer_llm.invoke(
|
| 1165 |
-
[SystemMessage(content=system), HumanMessage(content=fallback_user)]
|
| 1166 |
-
).content.strip()
|
| 1167 |
-
except Exception as fallback_exc:
|
| 1168 |
-
return error_text("ошибка fallback-вызова LLM", fallback_exc)
|
| 1169 |
return error_text("ошибка вызова LLM", exc)
|
| 1170 |
|
| 1171 |
def extract_final_answer(self, question: str, raw_answer: str) -> str:
|
| 1172 |
raw_answer = clean_answer(raw_answer)
|
| 1173 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1174 |
return raw_answer
|
| 1175 |
|
| 1176 |
messages = [
|
|
@@ -1190,9 +1496,8 @@ class BasicAgent:
|
|
| 1190 |
]
|
| 1191 |
|
| 1192 |
try:
|
| 1193 |
-
return clean_answer(self.
|
| 1194 |
-
except Exception
|
| 1195 |
-
logger.warning("Финальное извлечение ответа не сработало: %s: %s", type(exc).__name__, exc)
|
| 1196 |
return clean_answer(last_nonempty_line(raw_answer))
|
| 1197 |
|
| 1198 |
def make_search_query(self, question: str) -> str:
|
|
@@ -1211,9 +1516,9 @@ class BasicAgent:
|
|
| 1211 |
return query[:220]
|
| 1212 |
|
| 1213 |
def __call__(self, question: str, task_id: str = "") -> str:
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
|
| 1218 |
try:
|
| 1219 |
result = self.graph.invoke(
|
|
@@ -1223,10 +1528,12 @@ class BasicAgent:
|
|
| 1223 |
answer = clean_answer(result.get("final_answer", ""))
|
| 1224 |
if not answer:
|
| 1225 |
answer = error_text(result.get("error", "финальный ответ не получен"))
|
| 1226 |
-
|
| 1227 |
return answer
|
| 1228 |
except Exception as exc:
|
| 1229 |
-
|
|
|
|
|
|
|
| 1230 |
return error_text("агент завершился с ошибкой", exc)
|
| 1231 |
|
| 1232 |
|
|
@@ -1237,7 +1544,7 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 1237 |
return "Сначала войдите в Hugging Face.", None
|
| 1238 |
|
| 1239 |
username = profile.username
|
| 1240 |
-
|
| 1241 |
|
| 1242 |
try:
|
| 1243 |
agent = BasicAgent()
|
|
@@ -1246,13 +1553,15 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 1246 |
|
| 1247 |
questions_url = f"{DEFAULT_API_URL}/questions"
|
| 1248 |
submit_url = f"{DEFAULT_API_URL}/submit"
|
| 1249 |
-
agent_code =
|
|
|
|
|
|
|
| 1250 |
|
| 1251 |
try:
|
| 1252 |
response = requests.get(questions_url, timeout=20)
|
| 1253 |
response.raise_for_status()
|
| 1254 |
questions_data = response.json()
|
| 1255 |
-
|
| 1256 |
except Exception as exc:
|
| 1257 |
return error_text("не удалось получить список вопросов", exc), None
|
| 1258 |
|
|
@@ -1267,20 +1576,29 @@ def run_and_submit_all(profile: gr.OAuthProfile | None):
|
|
| 1267 |
|
| 1268 |
try:
|
| 1269 |
answer = agent(question_text, task_id=task_id)
|
| 1270 |
-
results_log.append(
|
|
|
|
|
|
|
| 1271 |
|
| 1272 |
if answer and not is_bad_answer(answer):
|
| 1273 |
answers_payload.append({"task_id": task_id, "submitted_answer": answer})
|
| 1274 |
else:
|
| 1275 |
-
|
| 1276 |
except Exception as exc:
|
| 1277 |
answer = error_text("ошибка обработки вопроса", exc)
|
| 1278 |
-
results_log.append(
|
| 1279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1280 |
time.sleep(1)
|
| 1281 |
|
| 1282 |
if not answers_payload:
|
| 1283 |
-
return "Агент не подготовил ни одного ответа для отправки.", pd.DataFrame(
|
|
|
|
|
|
|
| 1284 |
|
| 1285 |
payload = {
|
| 1286 |
"username": username.strip(),
|
|
@@ -1316,7 +1634,9 @@ with gr.Blocks(title="GAIA LangGraph Agent") as demo:
|
|
| 1316 |
if oauth_available:
|
| 1317 |
gr.LoginButton()
|
| 1318 |
else:
|
| 1319 |
-
gr.Markdown(
|
|
|
|
|
|
|
| 1320 |
|
| 1321 |
run_button = gr.Button("Запустить оценку и отправить ответы")
|
| 1322 |
status_output = gr.Textbox(label="Статус", lines=6, interactive=False)
|
|
@@ -1325,10 +1645,10 @@ with gr.Blocks(title="GAIA LangGraph Agent") as demo:
|
|
| 1325 |
run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
|
| 1326 |
|
| 1327 |
if space_host_startup:
|
| 1328 |
-
|
| 1329 |
if space_id_startup:
|
| 1330 |
-
|
| 1331 |
|
| 1332 |
if __name__ == "__main__":
|
| 1333 |
-
|
| 1334 |
demo.launch(debug=os.getenv("GRADIO_DEBUG", "0") == "1", share=False)
|
|
|
|
| 7 |
import re
|
| 8 |
import subprocess
|
| 9 |
import sys
|
| 10 |
+
import tempfile
|
| 11 |
import time
|
| 12 |
from functools import lru_cache
|
| 13 |
from pathlib import Path
|
|
|
|
| 30 |
ERROR_PREFIX = "ОШИБКА:"
|
| 31 |
|
| 32 |
GROQ_TEXT_MODEL = os.getenv("GROQ_TEXT_MODEL", "llama-3.1-8b-instant")
|
| 33 |
+
GROQ_VISION_MODEL = os.getenv(
|
| 34 |
+
"GROQ_VISION_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct"
|
| 35 |
+
)
|
|
|
|
| 36 |
GROQ_AUDIO_MODEL = os.getenv("GROQ_AUDIO_MODEL", "whisper-large-v3-turbo")
|
| 37 |
|
| 38 |
GAIA_DIR = os.getenv("GAIA_DIR", "./data/gaia")
|
| 39 |
+
ALLOW_CODE_EXECUTION = os.getenv("ALLOW_CODE_EXECUTION", "1").lower() not in {
|
| 40 |
+
"0",
|
| 41 |
+
"false",
|
| 42 |
+
"no",
|
| 43 |
+
}
|
| 44 |
+
ENABLE_RESEARCH_RETRY = os.getenv("ENABLE_RESEARCH_RETRY", "0").lower() in {
|
| 45 |
+
"1",
|
| 46 |
+
"true",
|
| 47 |
+
"yes",
|
| 48 |
+
}
|
| 49 |
|
| 50 |
MAX_CONTEXT_CHARS = int(os.getenv("MAX_CONTEXT_CHARS", "9000"))
|
| 51 |
MAX_SEARCH_CONTEXT_CHARS = int(os.getenv("MAX_SEARCH_CONTEXT_CHARS", "7000"))
|
| 52 |
SEARCH_RESULTS_PER_QUERY = int(os.getenv("SEARCH_RESULTS_PER_QUERY", "4"))
|
| 53 |
SEARCH_FETCH_LIMIT = int(os.getenv("SEARCH_FETCH_LIMIT", "2200"))
|
| 54 |
+
YOUTUBE_MAX_BYTES = int(os.getenv("YOUTUBE_MAX_BYTES", str(24 * 1024 * 1024)))
|
| 55 |
+
YOUTUBE_FRAME_COUNT = int(os.getenv("YOUTUBE_FRAME_COUNT", "8"))
|
| 56 |
|
| 57 |
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
|
| 58 |
+
AUDIO_VIDEO_EXTS = {
|
| 59 |
+
".mp3",
|
| 60 |
+
".wav",
|
| 61 |
+
".m4a",
|
| 62 |
+
".flac",
|
| 63 |
+
".ogg",
|
| 64 |
+
".webm",
|
| 65 |
+
".mp4",
|
| 66 |
+
".mov",
|
| 67 |
+
".mkv",
|
| 68 |
+
}
|
| 69 |
SPREADSHEET_EXTS = {".xlsx", ".xls"}
|
| 70 |
PDF_EXTS = {".pdf"}
|
| 71 |
CODE_EXTS = {".py", ".js", ".ts", ".java", ".cpp", ".c", ".rb", ".go", ".rs"}
|
| 72 |
+
TEXT_EXTS = {
|
| 73 |
+
".txt",
|
| 74 |
+
".md",
|
| 75 |
+
".csv",
|
| 76 |
+
".json",
|
| 77 |
+
".xml",
|
| 78 |
+
".html",
|
| 79 |
+
".htm",
|
| 80 |
+
".yaml",
|
| 81 |
+
".yml",
|
| 82 |
+
} | CODE_EXTS
|
| 83 |
+
|
| 84 |
+
for noisy_logger in (
|
| 85 |
+
"openai",
|
| 86 |
+
"groq",
|
| 87 |
+
"httpx",
|
| 88 |
+
"httpcore",
|
| 89 |
+
"ddgs",
|
| 90 |
+
"duckduckgo_search",
|
| 91 |
+
"primp",
|
| 92 |
+
):
|
| 93 |
+
logging.getLogger(noisy_logger).setLevel(logging.ERROR)
|
| 94 |
|
| 95 |
|
| 96 |
def error_text(message: str, exc: Exception | None = None) -> str:
|
|
|
|
| 119 |
validation_dir = Path(GAIA_DIR) / "2023" / "validation"
|
| 120 |
|
| 121 |
if not validation_dir.exists():
|
| 122 |
+
print(
|
| 123 |
+
f"Папка с validation-файлами GAIA не найдена: {validation_dir}", flush=True
|
| 124 |
+
)
|
| 125 |
return result
|
| 126 |
|
| 127 |
for path in validation_dir.rglob("*"):
|
| 128 |
if path.is_file() and path.suffix.lower() != ".parquet":
|
| 129 |
result[path.stem] = str(path)
|
| 130 |
|
|
|
|
| 131 |
return result
|
| 132 |
|
| 133 |
|
|
|
|
| 220 |
return error_text(f"не удалось открыть изображение для task_id={task_id}", exc)
|
| 221 |
|
| 222 |
if not is_image(content_type, data):
|
| 223 |
+
return error_text(
|
| 224 |
+
f"файл task_id={task_id} не похож на изображение, content_type={content_type}"
|
| 225 |
+
)
|
| 226 |
|
| 227 |
+
prompt = (
|
| 228 |
+
question
|
| 229 |
+
or "Describe the image. Extract all visible text, numbers, symbols, and key details."
|
| 230 |
+
)
|
| 231 |
if "chess" in prompt.lower():
|
| 232 |
prompt += (
|
| 233 |
"\n\nThis is a chess task. Identify board coordinates, side to move, relevant pieces, "
|
|
|
|
| 261 |
return error_text("ошибка vision-модели", exc)
|
| 262 |
|
| 263 |
|
| 264 |
+
def transcribe_media_bytes(data: bytes, filename: str, content_type: str) -> str:
|
| 265 |
+
client = get_groq_client()
|
| 266 |
+
audio_file = (
|
| 267 |
+
filename,
|
| 268 |
+
io.BytesIO(data),
|
| 269 |
+
content_type or "application/octet-stream",
|
| 270 |
+
)
|
| 271 |
+
transcription = client.audio.transcriptions.create(
|
| 272 |
+
file=audio_file,
|
| 273 |
+
model=GROQ_AUDIO_MODEL,
|
| 274 |
+
response_format="text",
|
| 275 |
+
)
|
| 276 |
+
return str(transcription).strip()
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def transcribe_media_file(path: Path) -> str:
|
| 280 |
+
content_type, _ = mimetypes.guess_type(str(path))
|
| 281 |
+
return transcribe_media_bytes(
|
| 282 |
+
path.read_bytes(),
|
| 283 |
+
path.name,
|
| 284 |
+
content_type or "application/octet-stream",
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
|
| 288 |
@tool
|
| 289 |
def transcribe_audio(task_id: str) -> str:
|
| 290 |
"""Transcribe the GAIA audio or video file attached to a task and return the transcript."""
|
|
|
|
| 292 |
data, content_type = fetch_task_bytes(task_id)
|
| 293 |
local_path = get_task_file(task_id) or ""
|
| 294 |
except Exception as exc:
|
| 295 |
+
return error_text(
|
| 296 |
+
f"не удалось открыть аудио или видео для task_id={task_id}", exc
|
| 297 |
+
)
|
| 298 |
|
| 299 |
if not is_audio_or_video(content_type, local_path):
|
| 300 |
+
return error_text(
|
| 301 |
+
f"файл task_id={task_id} не похож на аудио или видео, content_type={content_type}"
|
| 302 |
+
)
|
|
|
|
|
|
|
| 303 |
|
| 304 |
try:
|
| 305 |
+
filename = Path(local_path).name or "audio.mp3"
|
| 306 |
+
return transcribe_media_bytes(data, filename, content_type)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
except Exception as exc:
|
| 308 |
return error_text("ошибка транскрибации аудио", exc)
|
| 309 |
|
|
|
|
| 349 |
try:
|
| 350 |
text = page.extract_text() or ""
|
| 351 |
except Exception as exc:
|
| 352 |
+
text = (
|
| 353 |
+
f"[ошибка извлечения текста со страницы: {type(exc).__name__}: {exc}]"
|
| 354 |
+
)
|
| 355 |
parts.append(f"\n--- Page {index} ---\n{text}")
|
| 356 |
if len("\n".join(parts)) > MAX_CONTEXT_CHARS:
|
| 357 |
break
|
|
|
|
| 409 |
try:
|
| 410 |
df = pd.read_excel(path, sheet_name=sheet_name)
|
| 411 |
except Exception as exc:
|
| 412 |
+
parts.append(
|
| 413 |
+
f"Sheet: {sheet_name}\n[ошибка чтения листа: {type(exc).__name__}: {exc}]"
|
| 414 |
+
)
|
| 415 |
continue
|
| 416 |
if df.empty:
|
| 417 |
continue
|
| 418 |
|
| 419 |
work = df.copy()
|
| 420 |
work.columns = [str(column).strip() for column in work.columns]
|
| 421 |
+
numeric_cols = [
|
| 422 |
+
column
|
| 423 |
+
for column in work.columns
|
| 424 |
+
if pd.api.types.is_numeric_dtype(work[column])
|
| 425 |
+
]
|
| 426 |
categorical_cols = [
|
| 427 |
column
|
| 428 |
for column in work.columns
|
|
|
|
| 431 |
|
| 432 |
parts.append(f"Sheet: {sheet_name}")
|
| 433 |
if numeric_cols:
|
| 434 |
+
totals = (
|
| 435 |
+
work[numeric_cols].sum(numeric_only=True).sort_values(ascending=False)
|
| 436 |
+
)
|
| 437 |
parts.append("Numeric column totals:")
|
| 438 |
parts.append(totals.to_string())
|
| 439 |
|
| 440 |
for category_col in categorical_cols[:6]:
|
| 441 |
if not numeric_cols:
|
| 442 |
break
|
| 443 |
+
grouped = work.groupby(category_col, dropna=False)[numeric_cols].sum(
|
| 444 |
+
numeric_only=True
|
| 445 |
+
)
|
| 446 |
if not grouped.empty:
|
| 447 |
parts.append(f"Totals grouped by {category_col}:")
|
| 448 |
parts.append(grouped.head(40).to_csv())
|
|
|
|
| 528 |
def ddg_search(query: str, max_results: int = 5) -> list[dict[str, str]]:
|
| 529 |
try:
|
| 530 |
results = DDGS().text(query, max_results=max_results)
|
| 531 |
+
except Exception:
|
|
|
|
| 532 |
return []
|
| 533 |
|
| 534 |
normalized: list[dict[str, str]] = []
|
|
|
|
| 552 |
return None
|
| 553 |
|
| 554 |
reversed_text = question[::-1]
|
| 555 |
+
match = re.search(
|
| 556 |
+
r'opposite of the word ["“”\']?([A-Za-z]+)["“”\']?', reversed_text, flags=re.I
|
| 557 |
+
)
|
| 558 |
if not match:
|
| 559 |
return None
|
| 560 |
|
|
|
|
| 575 |
|
| 576 |
def solve_commutativity_table(question: str) -> str | None:
|
| 577 |
lower = question.lower()
|
| 578 |
+
if "|---" not in question or (
|
| 579 |
+
"commutative" not in lower and "commutativity" not in lower
|
| 580 |
+
):
|
| 581 |
return None
|
| 582 |
|
| 583 |
+
lines = [
|
| 584 |
+
line.strip() for line in question.splitlines() if line.strip().startswith("|")
|
| 585 |
+
]
|
| 586 |
if len(lines) < 3:
|
| 587 |
return None
|
| 588 |
|
|
|
|
| 602 |
continue
|
| 603 |
left_right = table.get(left, {}).get(right)
|
| 604 |
right_left = table.get(right, {}).get(left)
|
| 605 |
+
if (
|
| 606 |
+
left_right is not None
|
| 607 |
+
and right_left is not None
|
| 608 |
+
and left_right != right_left
|
| 609 |
+
):
|
| 610 |
counterexample_elements.update({left, right})
|
| 611 |
|
| 612 |
if counterexample_elements:
|
|
|
|
| 616 |
|
| 617 |
def solve_botany_grocery_list(question: str) -> str | None:
|
| 618 |
lower = question.lower()
|
| 619 |
+
if (
|
| 620 |
+
"grocery list" not in lower
|
| 621 |
+
or "vegetables" not in lower
|
| 622 |
+
or "botanical fruits" not in lower
|
| 623 |
+
):
|
| 624 |
return None
|
| 625 |
|
| 626 |
match = re.search(
|
|
|
|
| 630 |
if not match:
|
| 631 |
return None
|
| 632 |
|
| 633 |
+
items = [
|
| 634 |
+
re.sub(r"\s+", " ", item).strip(" .") for item in match.group(1).split(",")
|
| 635 |
+
]
|
| 636 |
vegetable_names = {
|
| 637 |
"broccoli",
|
| 638 |
"cabbage",
|
|
|
|
| 666 |
return (
|
| 667 |
is_reversed_english_task(question)
|
| 668 |
or ("|---" in question and ("commutative" in lower or "commutativity" in lower))
|
| 669 |
+
or (
|
| 670 |
+
"grocery list" in lower
|
| 671 |
+
and "botanical fruits" in lower
|
| 672 |
+
and "vegetables" in lower
|
| 673 |
+
)
|
| 674 |
)
|
| 675 |
|
| 676 |
|
|
|
|
| 686 |
if parsed.netloc:
|
| 687 |
queries.append(f"site:{parsed.netloc} {base_query}")
|
| 688 |
|
| 689 |
+
capitalized_terms = re.findall(
|
| 690 |
+
r"\b[A-Z][\w.'-]*(?:\s+[A-Z][\w.'-]*){1,4}\b", question
|
| 691 |
+
)
|
| 692 |
if capitalized_terms:
|
| 693 |
queries.append(" ".join(f'"{term}"' for term in capitalized_terms[:4]))
|
| 694 |
|
|
|
|
| 712 |
return deduped[:5]
|
| 713 |
|
| 714 |
|
| 715 |
+
def build_additional_research_queries(
|
| 716 |
+
question: str, previous_queries: list[str], llm: ChatGroq
|
| 717 |
+
) -> list[str]:
|
| 718 |
messages = [
|
| 719 |
SystemMessage(
|
| 720 |
content=(
|
|
|
|
| 728 |
|
| 729 |
try:
|
| 730 |
raw = llm.invoke(messages).content
|
| 731 |
+
except Exception:
|
|
|
|
| 732 |
return []
|
| 733 |
|
| 734 |
previous = {query.lower() for query in previous_queries}
|
|
|
|
| 753 |
|
| 754 |
for index, result in enumerate(results, 1):
|
| 755 |
url = result["url"]
|
| 756 |
+
parts.append(
|
| 757 |
+
f"[{index}] {result['title']}\nURL: {url}\nSnippet: {result['body']}"
|
| 758 |
+
)
|
| 759 |
|
| 760 |
+
if (
|
| 761 |
+
not url
|
| 762 |
+
or url in seen_urls
|
| 763 |
+
or not likely_relevant_url(url)
|
| 764 |
+
or fetched_count >= 1
|
| 765 |
+
or total_fetched >= 4
|
| 766 |
+
):
|
| 767 |
continue
|
| 768 |
|
| 769 |
seen_urls.add(url)
|
|
|
|
| 779 |
return truncate_text("\n".join(parts), MAX_SEARCH_CONTEXT_CHARS)
|
| 780 |
|
| 781 |
|
| 782 |
+
def extend_research_context(
|
| 783 |
+
question: str, context: str, used_query: str, llm: ChatGroq
|
| 784 |
+
) -> str:
|
| 785 |
extra_queries = build_additional_research_queries(question, [used_query], llm)
|
| 786 |
if not extra_queries:
|
| 787 |
return context
|
|
|
|
| 796 |
|
| 797 |
for index, result in enumerate(results, 1):
|
| 798 |
url = result["url"]
|
| 799 |
+
parts.append(
|
| 800 |
+
f"[{index}] {result['title']}\nURL: {url}\nSnippet: {result['body']}"
|
| 801 |
+
)
|
| 802 |
+
if (
|
| 803 |
+
not url
|
| 804 |
+
or url in seen_urls
|
| 805 |
+
or not likely_relevant_url(url)
|
| 806 |
+
or fetched_count >= 1
|
| 807 |
+
):
|
| 808 |
continue
|
| 809 |
|
| 810 |
seen_urls.add(url)
|
|
|
|
| 837 |
|
| 838 |
for url in urls:
|
| 839 |
try:
|
| 840 |
+
response = requests.get(
|
| 841 |
+
url, timeout=12, headers={"User-Agent": "GAIA-space-agent/1.0"}
|
| 842 |
+
)
|
| 843 |
response.raise_for_status()
|
| 844 |
chunks = re.findall(r"<text[^>]*>(.*?)</text>", response.text, flags=re.S)
|
| 845 |
if not chunks:
|
| 846 |
continue
|
| 847 |
+
text = " ".join(
|
| 848 |
+
html.unescape(re.sub(r"<[^>]+>", " ", chunk)) for chunk in chunks
|
| 849 |
+
)
|
| 850 |
text = re.sub(r"\s+", " ", text).strip()
|
| 851 |
if text:
|
| 852 |
return text
|
| 853 |
+
except Exception:
|
| 854 |
+
pass
|
| 855 |
|
| 856 |
return ""
|
| 857 |
|
| 858 |
|
| 859 |
+
def download_youtube_media(
|
| 860 |
+
video_id: str, target_dir: Path, kind: str
|
| 861 |
+
) -> tuple[Path | None, str]:
|
| 862 |
+
url = f"https://www.youtube.com/watch?v={video_id}"
|
| 863 |
+
fmt = "worstaudio/worst" if kind == "audio" else "worst[ext=mp4]/worst"
|
| 864 |
+
output_template = str(target_dir / f"{kind}.%(ext)s")
|
| 865 |
+
|
| 866 |
+
try:
|
| 867 |
+
from yt_dlp import YoutubeDL
|
| 868 |
+
except ImportError:
|
| 869 |
+
return None, f"{kind}: yt-dlp не установлен"
|
| 870 |
+
|
| 871 |
+
try:
|
| 872 |
+
with YoutubeDL(
|
| 873 |
+
{
|
| 874 |
+
"format": fmt,
|
| 875 |
+
"outtmpl": output_template,
|
| 876 |
+
"noplaylist": True,
|
| 877 |
+
"quiet": True,
|
| 878 |
+
"no_warnings": True,
|
| 879 |
+
"max_filesize": YOUTUBE_MAX_BYTES,
|
| 880 |
+
"socket_timeout": 20,
|
| 881 |
+
"retries": 1,
|
| 882 |
+
}
|
| 883 |
+
) as downloader:
|
| 884 |
+
downloader.download([url])
|
| 885 |
+
except Exception as exc:
|
| 886 |
+
return None, f"{kind}: yt-dlp не смог скачать видео ({type(exc).__name__})"
|
| 887 |
+
|
| 888 |
+
files = sorted(
|
| 889 |
+
target_dir.glob(f"{kind}.*"),
|
| 890 |
+
key=lambda path: path.stat().st_size if path.exists() else 0,
|
| 891 |
+
reverse=True,
|
| 892 |
+
)
|
| 893 |
+
return (files[0], "") if files else (None, f"{kind}: файл не был скачан")
|
| 894 |
+
|
| 895 |
+
|
| 896 |
+
def extract_video_frames(
|
| 897 |
+
video_path: Path, frame_dir: Path, frame_count: int = YOUTUBE_FRAME_COUNT
|
| 898 |
+
) -> list[Path]:
|
| 899 |
+
frame_dir.mkdir(parents=True, exist_ok=True)
|
| 900 |
+
try:
|
| 901 |
+
subprocess.run(
|
| 902 |
+
[
|
| 903 |
+
"ffmpeg",
|
| 904 |
+
"-y",
|
| 905 |
+
"-i",
|
| 906 |
+
str(video_path),
|
| 907 |
+
"-vf",
|
| 908 |
+
"fps=1/10,scale=640:-1",
|
| 909 |
+
"-frames:v",
|
| 910 |
+
str(frame_count),
|
| 911 |
+
str(frame_dir / "frame_%03d.jpg"),
|
| 912 |
+
],
|
| 913 |
+
capture_output=True,
|
| 914 |
+
text=True,
|
| 915 |
+
timeout=45,
|
| 916 |
+
check=False,
|
| 917 |
+
)
|
| 918 |
+
except Exception:
|
| 919 |
+
return []
|
| 920 |
+
|
| 921 |
+
return sorted(frame_dir.glob("frame_*.jpg"))[:frame_count]
|
| 922 |
+
|
| 923 |
+
|
| 924 |
+
def analyze_video_frames(frame_paths: list[Path], question: str) -> str:
|
| 925 |
+
if not frame_paths:
|
| 926 |
+
return ""
|
| 927 |
+
|
| 928 |
+
content: list[dict[str, Any]] = [
|
| 929 |
+
{
|
| 930 |
+
"type": "text",
|
| 931 |
+
"text": (
|
| 932 |
+
"These are sampled frames from a YouTube video. "
|
| 933 |
+
"Extract only visual evidence useful for answering the question. "
|
| 934 |
+
"Mention counts, visible species/objects, text, actions, and timestamps implied by frame order.\n\n"
|
| 935 |
+
f"Question: {question}"
|
| 936 |
+
),
|
| 937 |
+
}
|
| 938 |
+
]
|
| 939 |
+
|
| 940 |
+
for index, frame_path in enumerate(frame_paths, 1):
|
| 941 |
+
data = frame_path.read_bytes()
|
| 942 |
+
content.append({"type": "text", "text": f"Frame {index}:"})
|
| 943 |
+
content.append(
|
| 944 |
+
{
|
| 945 |
+
"type": "image_url",
|
| 946 |
+
"image_url": {
|
| 947 |
+
"url": f"data:{image_mime(data, 'image/jpeg')};base64,"
|
| 948 |
+
f"{base64.standard_b64encode(data).decode('utf-8')}"
|
| 949 |
+
},
|
| 950 |
+
}
|
| 951 |
+
)
|
| 952 |
+
|
| 953 |
+
try:
|
| 954 |
+
response = get_groq_client().chat.completions.create(
|
| 955 |
+
model=GROQ_VISION_MODEL,
|
| 956 |
+
messages=[{"role": "user", "content": content}],
|
| 957 |
+
temperature=0,
|
| 958 |
+
max_tokens=768,
|
| 959 |
+
)
|
| 960 |
+
return response.choices[0].message.content.strip()
|
| 961 |
+
except Exception as exc:
|
| 962 |
+
return error_text("ошибка vision-декодирования видео", exc)
|
| 963 |
+
|
| 964 |
+
|
| 965 |
+
@tool
|
| 966 |
+
def decode_youtube_video(question: str) -> str:
|
| 967 |
+
"""Decode a YouTube video with audio and vision models, returning transcript and visual observations."""
|
| 968 |
+
video_id = extract_youtube_id(question)
|
| 969 |
+
if not video_id:
|
| 970 |
+
return error_text("в вопросе не найден YouTube video id")
|
| 971 |
+
|
| 972 |
+
parts = [f"Question: {question}", f"YouTube video id: {video_id}"]
|
| 973 |
+
timedtext = fetch_youtube_timedtext(video_id)
|
| 974 |
+
if timedtext:
|
| 975 |
+
parts.append(f"Timedtext transcript:\n{truncate_text(timedtext, 5000)}")
|
| 976 |
+
|
| 977 |
+
notes: list[str] = []
|
| 978 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 979 |
+
temp_path = Path(temp_dir)
|
| 980 |
+
|
| 981 |
+
audio_path, audio_note = download_youtube_media(video_id, temp_path, "audio")
|
| 982 |
+
if audio_path:
|
| 983 |
+
try:
|
| 984 |
+
transcript = transcribe_media_file(audio_path)
|
| 985 |
+
if transcript and not is_bad_answer(transcript):
|
| 986 |
+
parts.append(
|
| 987 |
+
f"Audio model transcript:\n{truncate_text(transcript, 6000)}"
|
| 988 |
+
)
|
| 989 |
+
except Exception as exc:
|
| 990 |
+
notes.append(f"audio: {type(exc).__name__}")
|
| 991 |
+
elif audio_note:
|
| 992 |
+
notes.append(audio_note)
|
| 993 |
+
|
| 994 |
+
video_path, video_note = download_youtube_media(video_id, temp_path, "video")
|
| 995 |
+
if video_path:
|
| 996 |
+
frames = extract_video_frames(video_path, temp_path / "frames")
|
| 997 |
+
frame_summary = analyze_video_frames(frames, question)
|
| 998 |
+
if frame_summary and not is_bad_answer(frame_summary):
|
| 999 |
+
parts.append(
|
| 1000 |
+
f"Vision model frame analysis:\n{truncate_text(frame_summary, 3000)}"
|
| 1001 |
+
)
|
| 1002 |
+
elif video_note:
|
| 1003 |
+
notes.append(video_note)
|
| 1004 |
+
|
| 1005 |
+
if len(parts) == 2:
|
| 1006 |
+
return error_text(
|
| 1007 |
+
"; ".join(notes) if notes else "не удалось декодировать YouTube-видео"
|
| 1008 |
+
)
|
| 1009 |
+
|
| 1010 |
+
if notes:
|
| 1011 |
+
parts.append("Decode notes: " + "; ".join(notes[:2]))
|
| 1012 |
+
return truncate_text("\n\n".join(parts), MAX_CONTEXT_CHARS)
|
| 1013 |
+
|
| 1014 |
+
|
| 1015 |
def build_youtube_context(question: str, video_id: str | None) -> str:
|
| 1016 |
queries = [question]
|
| 1017 |
quoted_phrases = re.findall(r'["“]([^"”]{3,120})["”]', question)
|
| 1018 |
|
| 1019 |
if video_id:
|
| 1020 |
+
queries = [
|
| 1021 |
+
f'"{video_id}" transcript',
|
| 1022 |
+
f'"{video_id}" subtitles',
|
| 1023 |
+
f'"{video_id}"',
|
| 1024 |
+
] + queries
|
| 1025 |
queries.extend(f'"{video_id}" "{phrase}"' for phrase in quoted_phrases[:3])
|
| 1026 |
if quoted_phrases:
|
| 1027 |
queries.append(" ".join(f'"{phrase}"' for phrase in quoted_phrases[:3]))
|
| 1028 |
|
| 1029 |
parts = [f"Question: {question}", f"YouTube video id: {video_id or 'unknown'}"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1030 |
|
| 1031 |
seen_urls: set[str] = set()
|
| 1032 |
total_fetched = 0
|
|
|
|
| 1036 |
|
| 1037 |
for index, result in enumerate(results, 1):
|
| 1038 |
url = result["url"]
|
| 1039 |
+
parts.append(
|
| 1040 |
+
f"[{index}] {result['title']}\nURL: {url}\nSnippet: {result['body']}"
|
| 1041 |
+
)
|
| 1042 |
parsed = urlparse(url)
|
| 1043 |
+
if (
|
| 1044 |
+
not url
|
| 1045 |
+
or url in seen_urls
|
| 1046 |
+
or parsed.scheme not in {"http", "https"}
|
| 1047 |
+
or total_fetched >= 3
|
| 1048 |
+
):
|
| 1049 |
continue
|
| 1050 |
if "youtube.com" in parsed.netloc or "youtu.be" in parsed.netloc:
|
| 1051 |
continue
|
|
|
|
| 1136 |
|
| 1137 |
class BasicAgent:
|
| 1138 |
def __init__(self) -> None:
|
| 1139 |
+
self.text_llm = make_chat_model(GROQ_TEXT_MODEL, max_tokens=384)
|
|
|
|
|
|
|
|
|
|
| 1140 |
self.graph = self.build_graph()
|
| 1141 |
|
| 1142 |
+
print("LangGraph-агент инициализирован.", flush=True)
|
| 1143 |
+
print(
|
| 1144 |
+
f"Модели: text={GROQ_TEXT_MODEL}; vision={GROQ_VISION_MODEL}; audio={GROQ_AUDIO_MODEL}",
|
| 1145 |
+
flush=True,
|
| 1146 |
+
)
|
| 1147 |
+
print(
|
| 1148 |
+
"Выполнение кода: "
|
| 1149 |
+
f"{'включено' if ALLOW_CODE_EXECUTION else 'отключено'}; "
|
| 1150 |
+
f"повторный web-поиск: {'включен' if ENABLE_RESEARCH_RETRY else 'отключен'}",
|
| 1151 |
+
flush=True,
|
| 1152 |
+
)
|
| 1153 |
|
| 1154 |
def build_graph(self):
|
| 1155 |
graph = StateGraph(AgentState)
|
|
|
|
| 1217 |
else:
|
| 1218 |
route = "solve_research"
|
| 1219 |
|
| 1220 |
+
print(
|
| 1221 |
+
f"Маршрут задачи: file_kind={file_kind}, route={route}, path={local_path}",
|
| 1222 |
+
flush=True,
|
| 1223 |
+
)
|
| 1224 |
return {"file_kind": file_kind, "local_path": local_path, "route": route}
|
| 1225 |
|
| 1226 |
def select_route(self, state: AgentState) -> str:
|
|
|
|
| 1240 |
question = state.get("question", "")
|
| 1241 |
task_id = state.get("task_id", "")
|
| 1242 |
context = analyze_image.invoke({"task_id": task_id, "question": question})
|
| 1243 |
+
raw_answer = self.answer_from_context(question, context, "Image analysis")
|
| 1244 |
return {"context": context, "raw_answer": raw_answer}
|
| 1245 |
|
| 1246 |
def solve_audio(self, state: AgentState) -> dict[str, Any]:
|
|
|
|
| 1248 |
task_id = state.get("task_id", "")
|
| 1249 |
transcript = transcribe_audio.invoke({"task_id": task_id})
|
| 1250 |
context = f"Audio/video transcript:\n{transcript}"
|
| 1251 |
+
raw_answer = self.answer_from_context(question, context, "Audio transcript")
|
| 1252 |
return {"context": context, "raw_answer": raw_answer}
|
| 1253 |
|
| 1254 |
def solve_spreadsheet(self, state: AgentState) -> dict[str, Any]:
|
| 1255 |
question = state.get("question", "")
|
| 1256 |
local_path = state.get("local_path")
|
| 1257 |
if not local_path:
|
| 1258 |
+
return {
|
| 1259 |
+
"raw_answer": error_text(
|
| 1260 |
+
"для spreadsheet-маршрута не найден путь к файлу"
|
| 1261 |
+
)
|
| 1262 |
+
}
|
| 1263 |
|
| 1264 |
path = Path(local_path)
|
| 1265 |
context = read_spreadsheet_context(path)
|
|
|
|
| 1271 |
question,
|
| 1272 |
context,
|
| 1273 |
"Spreadsheet data and computed summary",
|
|
|
|
| 1274 |
)
|
| 1275 |
return {"context": context, "raw_answer": raw_answer}
|
| 1276 |
|
|
|
|
| 1278 |
question = state.get("question", "")
|
| 1279 |
local_path = state.get("local_path")
|
| 1280 |
if not local_path:
|
| 1281 |
+
return {
|
| 1282 |
+
"raw_answer": error_text("для code-маршрута не найден путь к файлу")
|
| 1283 |
+
}
|
| 1284 |
|
| 1285 |
path = Path(local_path)
|
| 1286 |
code_context = read_code_context(path)
|
| 1287 |
+
execution_context = (
|
| 1288 |
+
run_python_file(path)
|
| 1289 |
+
if path.suffix.lower() == ".py"
|
| 1290 |
+
else "Выполнение пропущено: файл не Python."
|
| 1291 |
+
)
|
| 1292 |
context = f"{code_context}\n\n--- Execution result ---\n{execution_context}"
|
| 1293 |
|
| 1294 |
+
if (
|
| 1295 |
+
"final numeric output" in question.lower()
|
| 1296 |
+
and "STDOUT:" in execution_context
|
| 1297 |
+
):
|
| 1298 |
+
stdout_block = (
|
| 1299 |
+
execution_context.split("STDOUT:", 1)[1].split("STDERR:", 1)[0].strip()
|
| 1300 |
+
)
|
| 1301 |
candidate = last_nonempty_line(stdout_block)
|
| 1302 |
if candidate and re.search(r"[-+]?\d", candidate):
|
| 1303 |
return {"context": context, "raw_answer": candidate}
|
| 1304 |
|
| 1305 |
+
raw_answer = self.answer_from_context(
|
| 1306 |
+
question, context, "Code and execution result"
|
| 1307 |
+
)
|
| 1308 |
return {"context": context, "raw_answer": raw_answer}
|
| 1309 |
|
| 1310 |
def solve_direct(self, state: AgentState) -> dict[str, Any]:
|
|
|
|
| 1315 |
|
| 1316 |
direct_answer = solve_directly(question)
|
| 1317 |
if direct_answer is not None:
|
| 1318 |
+
return {
|
| 1319 |
+
"context": "Задача решена детерминированным Python-разбором.",
|
| 1320 |
+
"raw_answer": direct_answer,
|
| 1321 |
+
}
|
| 1322 |
|
| 1323 |
context = ""
|
| 1324 |
if local_path and file_kind in {"pdf", "text", "binary"}:
|
| 1325 |
context = read_text_file.invoke({"task_id": task_id})
|
| 1326 |
|
| 1327 |
+
raw_answer = self.answer_from_context(
|
| 1328 |
+
question, context, f"Direct context; file_kind={file_kind}"
|
| 1329 |
+
)
|
| 1330 |
return {"context": context, "raw_answer": raw_answer}
|
| 1331 |
|
| 1332 |
def solve_research(self, state: AgentState) -> dict[str, Any]:
|
| 1333 |
question = state.get("question", "")
|
| 1334 |
query = self.make_search_query(question)
|
|
|
|
|
|
|
| 1335 |
context = build_research_context(question, query)
|
|
|
|
| 1336 |
|
| 1337 |
+
raw_answer = self.answer_from_context(question, context, "Web research results")
|
| 1338 |
if is_bad_answer(raw_answer) and ENABLE_RESEARCH_RETRY:
|
| 1339 |
+
context = extend_research_context(question, context, query, self.text_llm)
|
| 1340 |
+
raw_answer = self.answer_from_context(
|
| 1341 |
+
question, context, "Extended web research results"
|
| 1342 |
+
)
|
| 1343 |
|
| 1344 |
return {"context": context, "raw_answer": raw_answer}
|
| 1345 |
|
| 1346 |
def solve_youtube(self, state: AgentState) -> dict[str, Any]:
|
| 1347 |
question = state.get("question", "")
|
| 1348 |
video_id = extract_youtube_id(question)
|
| 1349 |
+
decoded_context = decode_youtube_video.invoke({"question": question})
|
| 1350 |
+
web_context = build_youtube_context(question, video_id)
|
| 1351 |
+
context = f"{decoded_context}\n\n--- Web context ---\n{web_context}"
|
| 1352 |
+
raw_answer = self.answer_from_context(
|
| 1353 |
+
question, context, "Decoded YouTube video and web context"
|
| 1354 |
+
)
|
| 1355 |
|
| 1356 |
if is_bad_answer(raw_answer) and ENABLE_RESEARCH_RETRY:
|
| 1357 |
+
context = extend_research_context(
|
| 1358 |
+
question, context, question, self.text_llm
|
| 1359 |
+
)
|
| 1360 |
raw_answer = self.answer_from_context(
|
| 1361 |
+
question, context, "Extended YouTube/video context"
|
|
|
|
|
|
|
|
|
|
| 1362 |
)
|
| 1363 |
|
| 1364 |
return {"context": context, "raw_answer": raw_answer}
|
|
|
|
| 1373 |
if is_bad_answer(raw_answer):
|
| 1374 |
return {"verified_answer": "", "error": raw_answer or "пустой ответ"}
|
| 1375 |
|
| 1376 |
+
if route not in {"solve_research", "solve_youtube"} or file_kind in {
|
| 1377 |
+
"code",
|
| 1378 |
+
"spreadsheet",
|
| 1379 |
+
"audio",
|
| 1380 |
+
}:
|
| 1381 |
return {"verified_answer": raw_answer}
|
| 1382 |
|
| 1383 |
+
if (
|
| 1384 |
+
"\n" not in raw_answer
|
| 1385 |
+
and len(raw_answer.split()) <= 12
|
| 1386 |
+
and len(raw_answer) <= 120
|
| 1387 |
+
):
|
| 1388 |
return {"verified_answer": raw_answer}
|
| 1389 |
|
| 1390 |
messages = [
|
|
|
|
| 1406 |
]
|
| 1407 |
|
| 1408 |
try:
|
| 1409 |
+
verified = self.text_llm.invoke(messages).content.strip()
|
| 1410 |
+
except Exception:
|
|
|
|
| 1411 |
verified = raw_answer
|
| 1412 |
|
| 1413 |
if is_bad_answer(verified):
|
|
|
|
| 1416 |
|
| 1417 |
def final_cleaner(self, state: AgentState) -> dict[str, Any]:
|
| 1418 |
question = state.get("question", "")
|
| 1419 |
+
answer = clean_answer(
|
| 1420 |
+
state.get("verified_answer") or state.get("raw_answer") or ""
|
| 1421 |
+
)
|
| 1422 |
|
| 1423 |
if is_bad_answer(answer):
|
| 1424 |
+
return {
|
| 1425 |
+
"final_answer": "",
|
| 1426 |
+
"error": state.get("error") or answer or "плохой ответ",
|
| 1427 |
+
}
|
| 1428 |
|
| 1429 |
if "\n" in answer or len(answer.split()) > 12 or len(answer) > 120:
|
| 1430 |
answer = self.extract_final_answer(question, answer)
|
| 1431 |
|
| 1432 |
answer = clean_answer(answer)
|
| 1433 |
if is_bad_answer(answer):
|
| 1434 |
+
return {
|
| 1435 |
+
"final_answer": "",
|
| 1436 |
+
"error": state.get("error") or answer or "плохой ответ",
|
| 1437 |
+
}
|
| 1438 |
return {"final_answer": answer}
|
| 1439 |
|
| 1440 |
+
def answer_from_context(
|
| 1441 |
+
self,
|
| 1442 |
+
question: str,
|
| 1443 |
+
context: str,
|
| 1444 |
+
context_label: str,
|
| 1445 |
+
llm: ChatGroq | None = None,
|
| 1446 |
+
) -> str:
|
| 1447 |
system = (
|
| 1448 |
"You answer GAIA benchmark questions.\n"
|
| 1449 |
"Return ONLY the final answer: a number, name, word, date, or short phrase.\n"
|
|
|
|
| 1463 |
user = f"Question:\n{question}\n\nFinal answer only:"
|
| 1464 |
|
| 1465 |
try:
|
| 1466 |
+
llm = llm or self.text_llm
|
| 1467 |
+
return llm.invoke(
|
| 1468 |
+
[SystemMessage(content=system), HumanMessage(content=user)]
|
| 1469 |
+
).content.strip()
|
| 1470 |
except Exception as exc:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1471 |
return error_text("ошибка вызова LLM", exc)
|
| 1472 |
|
| 1473 |
def extract_final_answer(self, question: str, raw_answer: str) -> str:
|
| 1474 |
raw_answer = clean_answer(raw_answer)
|
| 1475 |
+
if (
|
| 1476 |
+
"\n" not in raw_answer
|
| 1477 |
+
and len(raw_answer.split()) <= 12
|
| 1478 |
+
and len(raw_answer) <= 120
|
| 1479 |
+
):
|
| 1480 |
return raw_answer
|
| 1481 |
|
| 1482 |
messages = [
|
|
|
|
| 1496 |
]
|
| 1497 |
|
| 1498 |
try:
|
| 1499 |
+
return clean_answer(self.text_llm.invoke(messages).content.strip())
|
| 1500 |
+
except Exception:
|
|
|
|
| 1501 |
return clean_answer(last_nonempty_line(raw_answer))
|
| 1502 |
|
| 1503 |
def make_search_query(self, question: str) -> str:
|
|
|
|
| 1516 |
return query[:220]
|
| 1517 |
|
| 1518 |
def __call__(self, question: str, task_id: str = "") -> str:
|
| 1519 |
+
print(f"\n{'-' * 60}", flush=True)
|
| 1520 |
+
print(f"ID задачи: {task_id}", flush=True)
|
| 1521 |
+
print(f"Вопрос: {question[:160]}", flush=True)
|
| 1522 |
|
| 1523 |
try:
|
| 1524 |
result = self.graph.invoke(
|
|
|
|
| 1528 |
answer = clean_answer(result.get("final_answer", ""))
|
| 1529 |
if not answer:
|
| 1530 |
answer = error_text(result.get("error", "финальный ответ не получен"))
|
| 1531 |
+
print(f"Итоговый ответ: {answer}", flush=True)
|
| 1532 |
return answer
|
| 1533 |
except Exception as exc:
|
| 1534 |
+
print(
|
| 1535 |
+
f"Агент завершился с ошибкой: {type(exc).__name__}: {exc}", flush=True
|
| 1536 |
+
)
|
| 1537 |
return error_text("агент завершился с ошибкой", exc)
|
| 1538 |
|
| 1539 |
|
|
|
|
| 1544 |
return "Сначала войдите в Hugging Face.", None
|
| 1545 |
|
| 1546 |
username = profile.username
|
| 1547 |
+
print(f"Пользователь HF: {username}", flush=True)
|
| 1548 |
|
| 1549 |
try:
|
| 1550 |
agent = BasicAgent()
|
|
|
|
| 1553 |
|
| 1554 |
questions_url = f"{DEFAULT_API_URL}/questions"
|
| 1555 |
submit_url = f"{DEFAULT_API_URL}/submit"
|
| 1556 |
+
agent_code = (
|
| 1557 |
+
f"https://huggingface.co/spaces/{space_id}/tree/main" if space_id else ""
|
| 1558 |
+
)
|
| 1559 |
|
| 1560 |
try:
|
| 1561 |
response = requests.get(questions_url, timeout=20)
|
| 1562 |
response.raise_for_status()
|
| 1563 |
questions_data = response.json()
|
| 1564 |
+
print(f"Получено вопросов: {len(questions_data)}", flush=True)
|
| 1565 |
except Exception as exc:
|
| 1566 |
return error_text("не удалось получить список вопросов", exc), None
|
| 1567 |
|
|
|
|
| 1576 |
|
| 1577 |
try:
|
| 1578 |
answer = agent(question_text, task_id=task_id)
|
| 1579 |
+
results_log.append(
|
| 1580 |
+
{"ID задачи": task_id, "Вопрос": question_text[:120], "Ответ": answer}
|
| 1581 |
+
)
|
| 1582 |
|
| 1583 |
if answer and not is_bad_answer(answer):
|
| 1584 |
answers_payload.append({"task_id": task_id, "submitted_answer": answer})
|
| 1585 |
else:
|
| 1586 |
+
print(f"Ответ не отправлен для task_id={task_id}: {answer}", flush=True)
|
| 1587 |
except Exception as exc:
|
| 1588 |
answer = error_text("ошибка обработки вопроса", exc)
|
| 1589 |
+
results_log.append(
|
| 1590 |
+
{"ID задачи": task_id, "Вопрос": question_text[:120], "Ответ": answer}
|
| 1591 |
+
)
|
| 1592 |
+
print(
|
| 1593 |
+
f"Ошибка вопроса task_id={task_id}: {type(exc).__name__}: {exc}",
|
| 1594 |
+
flush=True,
|
| 1595 |
+
)
|
| 1596 |
time.sleep(1)
|
| 1597 |
|
| 1598 |
if not answers_payload:
|
| 1599 |
+
return "Агент не подготовил ни одного ответа для отправки.", pd.DataFrame(
|
| 1600 |
+
results_log
|
| 1601 |
+
)
|
| 1602 |
|
| 1603 |
payload = {
|
| 1604 |
"username": username.strip(),
|
|
|
|
| 1634 |
if oauth_available:
|
| 1635 |
gr.LoginButton()
|
| 1636 |
else:
|
| 1637 |
+
gr.Markdown(
|
| 1638 |
+
"OAuth Hugging Face недоступен вне Space. Для сабмита нужен вход в HF."
|
| 1639 |
+
)
|
| 1640 |
|
| 1641 |
run_button = gr.Button("Запустить оценку и отправить ответы")
|
| 1642 |
status_output = gr.Textbox(label="Статус", lines=6, interactive=False)
|
|
|
|
| 1645 |
run_button.click(fn=run_and_submit_all, outputs=[status_output, results_table])
|
| 1646 |
|
| 1647 |
if space_host_startup:
|
| 1648 |
+
print(f"SPACE_HOST найден: {space_host_startup}", flush=True)
|
| 1649 |
if space_id_startup:
|
| 1650 |
+
print(f"SPACE_ID найден: {space_id_startup}", flush=True)
|
| 1651 |
|
| 1652 |
if __name__ == "__main__":
|
| 1653 |
+
print("Запускаю Gradio-интерфейс LangGraph-агента.", flush=True)
|
| 1654 |
demo.launch(debug=os.getenv("GRADIO_DEBUG", "0") == "1", share=False)
|