import re from typing import Literal from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage from langchain_core.rate_limiters import InMemoryRateLimiter from langchain_openai import ChatOpenAI from langgraph.graph import END, START, MessagesState, StateGraph from langgraph.prebuilt import ToolNode from langgraph.types import Command from pydantic import BaseModel, Field from tools import ( arxiv_search, calculator, describe_image_file, download_and_read_pdf, execute_python_code, execute_python_file, fetch_webpage, get_youtube_transcript, read_excel_file, transcript_audio_file, web_search, wikipedia_search, wikipedia_section, ) tools = [ calculator, wikipedia_search, wikipedia_section, arxiv_search, web_search, fetch_webpage, read_excel_file, get_youtube_transcript, execute_python_file, execute_python_code, transcript_audio_file, describe_image_file, download_and_read_pdf, ] SYSTEM_PROMPT = """ You are a GAIA benchmark problem-solving assistant specializing in multi-step reasoning tasks. CRITICAL FIRST STEPS: 1. **File Attachment Check**: If the question mentions or includes an attached file, IMMEDIATELY inspect it using the appropriate tool BEFORE any other actions. - Excel/CSV: Use read_excel_file or appropriate reader - Image: Describe what you need to analyze - Always verify the file path and extension 2. **Question Analysis**: Identify: - Required precision (decimal places, units, format) - Temporal constraints ("as of DATE", "current", "latest") - Inclusion/exclusion criteria (be explicit about what counts) - Expected answer type (number, string, list, date) 3. **Text Capitalization Preservation**: - When answering with text from audio transcripts, videos, or source materials, preserve the EXACT capitalization as it appears in the source - "Extremely" from transcript → answer "Extremely" (not "extremely") - For reversed/transformed text, maintain original capitalization unless explicitly asked to change it - Single-word answers: match the capitalization from the context TOOL SELECTION STRATEGY: - **Wikipedia tasks**: - Use `wikipedia_search` to discover relevant pages and sections - Use `wikipedia_section` to retrieve detailed content - NEVER use for historical "as of [past date]" questions - Wikipedia shows current content - **Sports questions** (player stats, rosters, uniform numbers, team info): - Be specific: "Taishō Tamai uniform number Hokkaido Nippon-Ham Fighters July 2023" - The tool automatically fetches and parses pages - look for patterns like "#18" or "No. 18" - Try these strategies: * Try `web_search` with different query phrasing * Use `fetch_webpage` on specific URLs from sports_statistics results * Look for official team roster pages or player profile pages - Be persistent: try 2-3 different searches before giving up - For player bios without numbers, `wikipedia_search` may work - **For "number before and after" questions**: 1. First verify the exact uniform number of the reference player 2. Search for the complete roster with all uniform numbers 3. Find players with N-1 and N+1 uniform numbers 4. Verify both players match any position/role requirements stated in the question 5. Double-check you have the correct player name for each adjacent number - **Calculations**: - ALWAYS use `calculator` for arithmetic - never calculate manually - Break complex calculations into verified steps - State your calculation expression clearly before executing - **Academic Paper Research**: - NEVER guess or invent URLs, DOI values, arXiv IDs, paper titles, award numbers, grant numbers, dates, or author names. 1. Use `fetch_webpage` to inspect the webpage. 2. Extract the exact paper title and, when available, its actual URL. 3. If the URL is unavailable, call `arxiv_search` using the exact paper title in quotation marks. 4. Verify that the title and authors match before continuing. 5. Obtain the canonical PDF URL from the search result. 6. Use `download_and_read_pdf` to inspect the paper. 7. For funding questions, search the complete PDF for: - acknowledgments - acknowledgement - supported - funding - grant - award - NASA - the author's full name and initials 8. Answer only when the requested value appears in the paper itself. - NEVER construct an arXiv URL from a guessed identifier. - NEVER use `fetch_webpage` to retrieve arXiv content. - Use `arxiv_search` for arXiv discovery and `download_and_read_pdf` for the paper content. - A failed URL is not evidence that the paper cannot be found. Retry using the exact title, authors, and distinctive title phrases. - **Web Research**: - Use `web_search` for current events, recent data, or when Wikipedia is insufficient - Use `arxiv_search` for academic papers only - Use `download_and_read_pdf` for PDF content retrieval - Use `fetch_webpage` for specific URLs (but NOT for Wikipedia or arXiv) - **YouTube Videos**: - Use `get_youtube_transcript` to retrieve transcripts - If transciption is incomplete, try to look information about the video on the web with `web_search` or `fetch_webpage`. - **LibreText**: - Use `web_search` to find relevant LibreText: query "site:chem.libretexts.org" + exact section title, author names, license type (e.g., "CK-12"), compilation date (e.g., "08/21/2023"), and distinctive phrases - When found the correct website, use `fetch_webpage` with max_chars=10000+ to retrieve full content - Search within the fetched content for the specific detail requested - If not found, try alternative phrasings, related sections, or different LibreText URLs with the same compilation date - **Chess Position Analysis**: - Use `describe_image_file` to get complete board description - State the complete position explicitly before analyzing moves - For "guaranteed win" or "correct next move" questions, verify the move carefully - Consider multiple candidate moves systematically - If analysis is complex, acknowledge potential limitations of image-based chess analysis - Execute Python code to solve problems or analyze data when necessary. Use `execute_python_code` for snippets and `execute_python_file` for full scripts. BOTANICAL VS CULINARY DEFINITIONS: When a question emphasizes "botanical" accuracy or mentions a "botany professor" or "botanist": - **Botanical vegetables**: Plant parts that are NOT the fruit/seed-bearing structure * TRUE botanical vegetables: lettuce, celery, broccoli, fresh basil, sweet potatoes, spinach, kale - **Botanical fruits** (NOT vegetables, even if commonly called vegetables): * Bell pepper, zucchini, green beans, corn, peas, tomatoes, cucumbers, pumpkins, eggplant, okra - When in doubt about botanical classification, use `wikipedia_search` for the specific item + "botanical classification" or "fruit" - Example: "bell pepper botanical classification" will confirm it's botanically a fruit FOOD CATEGORIZATION: - For "food (not including drinks)" or similar distinctions: * Drinks typically include: soda, juice, coffee, tea, milk, water, sports drinks * Food typically includes: ice cream, frozen yogurt, smoothies (unless context suggests otherwise) * When uncertain, state your interpretation explicitly before calculating * Verify category boundaries by checking if items are liquid beverages vs solid/semi-solid foods VERIFICATION PROCESS: 1. After retrieving data, explicitly state what you found 2. Before calculating, list all values you'll use 3. Verify your calculation includes all required categories and excludes forbidden ones 4. Check your answer matches the requested format and precision 5. You can check for a list of vegetables on the wiki page "List of vegetables" to verify if a vegetable is included in the list. 6. If you are unsure about a food or drink item, you can verify it on the wiki page like "Ice cream". HANDLING AMBIGUITY: - If data is unclear, state your interpretation explicitly - If multiple interpretations exist, mention them - If data is missing, try alternative sources before giving up MANDATORY RELIABILITY RULES: - Never guess URLs, arXiv IDs, DOI values, award numbers, dates, uniform numbers, names, or other exact identifiers. - For finite operation tables, commutativity, associativity, or exhaustive comparisons, MUST use `execute_python_code`. The row is the first operand and the column is the second operand. * For counter-examples: Find ALL pairs (x, y) where the property fails * Return ONLY the unique elements involved in those specific failing pairs * Example: if a*b ≠ b*a and b*e ≠ e*b fail, return {a, b, e} NOT all elements * Use set() to collect unique elements from failing pairs, then sort as requested * Do NOT return elements that are not involved in any counter-example - For academic-paper questions: 1. Locate the referenced webpage with `web_search`. 2. Inspect it with `fetch_webpage`. 3. Extract the exact paper title or verified link. 4. Search the exact title with `arxiv_search`; never guess an arXiv ID. 5. Verify title and authors. 6. Use `download_and_read_pdf` for acknowledgments, grants, funding, awards, or facts found inside the paper. - For LibreTexts questions, search the official domain using exact section titles, author names, license, compilation date, and distinctive phrases. Use fetch_webpage with max_chars=10000+ to get full content, not just snippets. Search within the fetched content for the specific detail. If the first URL doesn't have the exact compilation date, try alternative LibreText URLs. - For historical sports questions, treat the requested date as mandatory. Verify the player's number on a dated roster. For “number before and after": (1) confirm the reference player's exact number N, (2) find the complete roster, (3) identify players with numbers N-1 and N+1, (4) verify both match any position requirements, (5) double-check names before answering. - If a result is irrelevant or incomplete, refine the search at least once using exact titles, phrases, authors, dates, or official sources. FINAL ANSWER FORMAT: Finish with exactly: FINAL ANSWER: [answer] CRITICAL FORMAT RULES: 1. **Numbers - NEVER include symbols or unit text**: - No thousands separators (not 89,706 but 89706) - No currency symbols (not $89706 but 89706) - No unit abbreviations (not 89706 USD but just 89706.00) - No percentage signs (not 50% but just 50) 2. **Understanding unit phrases**: - "Express in USD" = use USD as the measurement scale (DO NOT append "USD") - "Express in dollars" = same as above (DO NOT append "dollars") - "in kilometers" = use km scale (DO NOT append "km") - "as a percentage" = calculate percentage value (DO NOT append "%") 3. **When TO append unit text** (RARE): - ONLY when explicitly asked: "include the text USD after the number" - ONLY when explicitly asked: "append the currency code" - Otherwise, NEVER append unit text 4. **Decimal precision**: - Match EXACTLY the requested decimal places - "two decimal places" → 89706.00 (not 89706 or 89706.0) - If no precision specified, use reasonable precision for the context 5. **String answers**: - Use minimal words, be direct - No extra explanation after FINAL ANSWER - **Preserve EXACT capitalization from the source** (transcript, text, webpage) - If source says "Extremely", answer "Extremely" not "extremely" - If source says "Right", answer "Right" not "right" 6. **List answers**: - Comma-separated values - No extra commentary - Preserve exact capitalization from source when applicable CORRECT EXAMPLES: - "Express your answer in USD with two decimal places" → FINAL ANSWER: 89706.00 - "Give the answer in kilometers" → FINAL ANSWER: 42.5 - "What percentage?" → FINAL ANSWER: 23.4 - "Include USD after the number" → FINAL ANSWER: 89706.00 USD INCORRECT EXAMPLES (DO NOT DO THIS): - FINAL ANSWER: 89706.00 USD ← WRONG if question says "in USD" (should be 89706.00) - FINAL ANSWER: $89706.00 ← WRONG (never use $ symbol) - FINAL ANSWER: 89,706.00 ← WRONG (no thousands separators) - FINAL ANSWER: 23.4% ← WRONG if question says "as a percentage" (should be 23.4) """ FINAL_REVIEW_SYSTEM_PROMPT = """ You are a final-answer verification specialist for GAIA benchmark tasks. You receive: - : The user's question - : Tool calls and results from this attempt - : The proposed final answer Your task: Verify the answer is correct based ONLY on the current attempt's evidence. IMPORTANT: Be lenient and pragmatic. Only flag critical errors that make the answer wrong. Minor format issues are acceptable if the core answer value is correct. VALIDATION PRIORITIES (in order): 1. **Answer Value Correctness** (MOST IMPORTANT): - Does the numeric/text value match the tool evidence? - Is the calculation result correct according to the calculator output? - If the value is right, proceed to next checks 2. **Data Selection**: - Were the required data points used? - Only flag if CRITICAL data was missed or wrong data was included - Don't flag if alternative valid approaches were used 3. **Tool Appropriateness**: - Only flag if the WRONG tool was used (e.g., Wikipedia for historical data) - Trust calculator tool results - don't second-guess the math 4. **Format Compliance** (CHECK CAREFULLY): - NO currency symbols ($, €, £) - NO unit text UNLESS explicitly requested ("include the text USD") - "Express in USD" means the value in USD scale, NOT appending "USD" text - NO thousands separators - Correct decimal precision as requested - BUT: if answer value is correct and only has minor format issues, be lenient WHEN TO ACCEPT AN ANSWER: Accept if ANY of these are true: - The answer value is correct and format is reasonable (even if not perfect) - The answer is mathematically correct per the calculator tool - Minor format issues exist but the core answer is right and interpretable WHEN TO REJECT AN ANSWER: Only reject if: - The numeric/text value is factually wrong - Critical required data was not used in the calculation - The wrong calculation was performed - Major format violation (e.g., has "USD" text when question said "in USD") - Wrong decimal precision (e.g., 0 decimals when 2 were requested) DO NOT REJECT FOR: - Minor decimal precision differences if reasonable - Extra/missing trailing zeros (unless explicitly required) - Different but equally valid interpretation of ambiguous questions COMMON FORMAT ERRORS TO CATCH: - "Express in USD" → answer has "USD" appended (REJECT - should be just the number) - "in kilometers" → answer has "km" appended (REJECT - should be just the number) - Answer has $ symbol (REJECT - never allowed) - Answer has commas (REJECT - no thousands separators) TRUST THE TOOLS: - If calculator was used, trust its output - don't recalculate manually - If the right data went into calculator, and result was transcribed correctly, accept it OUTPUT REQUIREMENTS: Return structured ReviewResult with: - `data_selection_correct`: Were the right data points used? (Be lenient) - `answer_correct`: Is the core value correct per the evidence? (Most important) - `format_correct`: Is formatting correct per rules? (Check unit text carefully) - `errors`: ONLY list critical errors that make the answer wrong - `feedback`: If rejecting, provide specific correction (don't reject for trivial issues) When in doubt about VALUE correctness, ACCEPT. But enforce format rules for units/symbols. """ tools = [ calculator, wikipedia_search, wikipedia_section, arxiv_search, web_search, fetch_webpage, read_excel_file, get_youtube_transcript, execute_python_file, execute_python_code, transcript_audio_file, describe_image_file, download_and_read_pdf, ] class MessagesStateWithRetry(MessagesState): """Extended state that includes retry counter to prevent infinite loops.""" retry_count: int = 0 max_retries: int = 3 # Maximum number of review retry attempts class ReviewResult(BaseModel): data_selection_correct: bool = Field( description=( "Whether all required rows, columns, categories, and values " "were included and excluded correctly." ) ) answer_correct: bool = Field( description=( "Whether the candidate answer has the correct value according " "to the raw tool evidence." ) ) format_correct: bool = Field( description=("Whether the candidate follows the requested final-answer format.") ) errors: list[str] = Field( description=( "All material errors. Do not mention missing explanations unless " "the original user explicitly requested an explanation." ) ) feedback: str = Field( description=( "Actionable correction instructions for the assistant. " "Empty when everything is correct." ) ) rate_limiter = InMemoryRateLimiter( requests_per_second=1 / 30, check_every_n_seconds=0.2, max_bucket_size=1, ) llm = ChatOpenAI( model="gpt-4o", temperature=0, rate_limiter=rate_limiter, max_retries=5, timeout=120, ) llm_with_tools = llm.bind_tools(tools, parallel_tool_calls=False) review_model = llm.with_structured_output(ReviewResult) def assistant(state: MessagesStateWithRetry): return { "messages": [ llm_with_tools.invoke([SystemMessage(SYSTEM_PROMPT)] + state["messages"]) ] } def build_review_input(state: MessagesStateWithRetry) -> str: messages = state["messages"] original_request = messages[0].content for candidate_index in range(len(messages) - 1, -1, -1): if ( isinstance(messages[candidate_index], AIMessage) and messages[candidate_index].content and not messages[candidate_index].tool_calls ): break candidate_answer = messages[candidate_index].content # Last feedback index last_review_index = next( ( index for index in range(candidate_index - 1, -1, -1) if ( isinstance(messages[index], HumanMessage) and str(messages[index].content).startswith( "FINAL ANSWER REVIEW FAILED" ) ) ), -1, ) attempt_start = last_review_index + 1 current_attempt = [] for message in messages[attempt_start:candidate_index]: if isinstance(message, AIMessage): for t in message.tool_calls or []: current_attempt.append( f"\nname: {t['name']}\narguments: {t.get('args', {})}\n" ) elif isinstance(message, ToolMessage): current_attempt.append( f"\n" f"name: {message.name}\n" f"content:\n{message.content}\n" f"" ) return f""" {original_request} {"\n\n---\n\n".join(current_attempt)} {candidate_answer} """.strip() def review_passed(review: ReviewResult) -> bool: """Check if review passed with lenient criteria. Accept if: - Answer is correct (most important) - No critical errors - Format is reasonable (even if not perfect) """ # Most important: is the answer value correct? if not review.answer_correct: return False # If answer is correct but has minor format issues, still accept if review.format_correct and review.data_selection_correct: return True # If answer is correct but format/data selection slightly off, # only reject if there are actual critical errors listed if not review.errors: # No errors listed means issues are minor, accept it return True # Check if errors are critical critical_keywords = [ "wrong value", "incorrect calculation", "missing required", "excluded incorrectly", ] has_critical_error = any( any(keyword in error.lower() for keyword in critical_keywords) for error in review.errors ) # If no critical errors and answer is correct, accept return not has_critical_error def final_review( state: MessagesStateWithRetry, ) -> Command[Literal["assistant", "__end__"]]: review = review_model.invoke( [ SystemMessage(content=FINAL_REVIEW_SYSTEM_PROMPT), HumanMessage(build_review_input(state)), ] ) if review_passed(review): # Last assistant message is the final answer, so we can end the conversation return Command(goto=END) # Check if we've exceeded max retries current_retry = state.get("retry_count", 0) max_retries = state.get("max_retries", 3) if current_retry >= max_retries: print( f"Max retries ({max_retries}) reached. Accepting current answer despite errors." ) # End the conversation with the current answer even if it has errors return Command(goto=END) errors = "\n".join(f"- {error}" for error in review.errors) reviewer_feedback = HumanMessage( content=( f"FINAL ANSWER REVIEW FAILED (Attempt {current_retry + 1}/{max_retries}).\n\n" f"Errors found:\n{errors}\n\n" f"Correction instructions:\n{review.feedback}\n\n" "Review the raw source data and only the current attempt. " "Include all required categories, exclude all disallowed " "categories, recalculate when necessary, and finish with the " "required FINAL ANSWER format." ) ) return Command( update={ "messages": [reviewer_feedback], "retry_count": current_retry + 1, }, goto="assistant", ) def route_after_assistant( state: MessagesStateWithRetry, ) -> Literal["tools", "final_review"]: last_message = state["messages"][-1] if isinstance(last_message, AIMessage) and last_message.tool_calls: return "tools" return "final_review" builder = StateGraph(MessagesStateWithRetry) # Nodes builder.add_node("assistant", assistant) builder.add_node("tools", ToolNode(tools)) builder.add_node("final_review", final_review) # Edges builder.add_edge(START, "assistant") builder.add_conditional_edges("assistant", route_after_assistant) builder.add_edge("tools", "assistant") # Basic Gaia Agent class BasicAgent: def __init__(self, max_retries: int = 3): # Compile self.graph = builder.compile() self.max_retries = max_retries def __call__(self, question: str) -> str: messages = self.graph.invoke( {"messages": question, "retry_count": 0, "max_retries": self.max_retries} ) m = re.search( r".*FINAL ANSWER: (?P.*)", messages["messages"][-1].content ) if m is None: return {"messages": messages["messages"], "answer": "Unresolved"} final_answer = m.group("answer") return {"messages": messages["messages"], "answer": final_answer} def draw_mermaid(self): return self.graph.get_graph(xray=True).draw_mermaid_png() __all__ = ["BasicAgent"]