| """ |
| Answer Formatter - Final answer formatting according to GAIA requirements |
| |
| The Answer Formatter is responsible for: |
| 1. Taking the draft answer and formatting it according to GAIA rules |
| 2. Extracting the final answer from comprehensive responses |
| 3. Ensuring exact-match compliance |
| 4. Handling different answer types (numbers, strings, lists) |
| """ |
|
|
| import re |
| from typing import Dict, Any |
| from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage |
| from langgraph.types import Command |
| from langchain_groq import ChatGroq |
| from observability import agent_span |
| from dotenv import load_dotenv |
|
|
| load_dotenv("env.local") |
|
|
|
|
| def extract_final_answer(text: str) -> str: |
| """ |
| Extract the final answer from text following GAIA formatting rules. |
| |
| GAIA Rules: |
| • Single number → write the number only (no commas, units, or other symbols) |
| • Single string/phrase → write the text only; omit articles and abbreviations unless explicitly required |
| • List → separate elements with a single comma and a space |
| • Never include surrounding text, quotes, brackets, or markdown |
| """ |
| |
| if not text or not text.strip(): |
| return "" |
| |
| |
| text = text.strip() |
| |
| |
| answer_patterns = [ |
| r"final answer[:\s]*(.+?)(?:\n|$)", |
| r"answer[:\s]*(.+?)(?:\n|$)", |
| r"result[:\s]*(.+?)(?:\n|$)", |
| r"conclusion[:\s]*(.+?)(?:\n|$)" |
| ] |
| |
| for pattern in answer_patterns: |
| match = re.search(pattern, text, re.IGNORECASE | re.MULTILINE) |
| if match: |
| text = match.group(1).strip() |
| break |
| |
| |
| prefixes_to_remove = [ |
| "the answer is", "it is", "this is", "that is", |
| "final answer:", "answer:", "result:", "conclusion:", |
| "therefore", "thus", "so", "hence" |
| ] |
| |
| for prefix in prefixes_to_remove: |
| if text.lower().startswith(prefix.lower()): |
| text = text[len(prefix):].strip() |
| |
| |
| text = re.sub(r'^["\'\[\(]|["\'\]\)]$', '', text) |
| text = re.sub(r'^\*\*|\*\*$', '', text) |
| text = re.sub(r'^`|`$', '', text) |
| |
| |
| |
| |
| number_match = re.match(r'^-?\d+(?:\.\d+)?$', text.strip()) |
| if number_match: |
| |
| num = float(text.strip()) if '.' in text else int(text.strip()) |
| return str(int(num)) if num == int(num) else str(num) |
| |
| |
| if ',' in text: |
| items = [item.strip() for item in text.split(',')] |
| |
| cleaned_items = [] |
| for item in items: |
| item = re.sub(r'^["\'\[\(]|["\'\]\)]$', '', item.strip()) |
| if item: |
| cleaned_items.append(item) |
| return ', '.join(cleaned_items) |
| |
| |
| |
| words = text.split() |
| if len(words) > 1 and words[0].lower() in ['the', 'a', 'an']: |
| |
| remaining = ' '.join(words[1:]) |
| if remaining and len(remaining) > 2: |
| text = remaining |
| |
| return text.strip() |
|
|
|
|
| def load_formatter_prompt() -> str: |
| """Load the formatting prompt""" |
| try: |
| with open("archive/prompts/verification_prompt.txt", "r") as f: |
| return f.read() |
| except FileNotFoundError: |
| return """ |
| You are a final answer formatter ensuring compliance with GAIA benchmark requirements. |
| |
| Your task is to extract the precise final answer from a comprehensive response. |
| |
| CRITICAL FORMATTING RULES: |
| • Single number → write the number only (no commas, units, or symbols) |
| • Single string/phrase → write the text only; omit articles unless required |
| • List → separate elements with comma and space |
| • NEVER include surrounding text like "Final Answer:", quotes, brackets, or markdown |
| • The response must contain ONLY the answer itself |
| |
| Examples: |
| Question: "What is 25 + 17?" |
| Draft: "After calculating, the answer is 42." |
| Formatted: "42" |
| |
| Question: "What is the capital of France?" |
| Draft: "The capital of France is Paris." |
| Formatted: "Paris" |
| |
| Question: "List the first 3 prime numbers" |
| Draft: "The first three prime numbers are 2, 3, and 5." |
| Formatted: "2, 3, 5" |
| |
| Extract ONLY the final answer following these rules exactly. |
| """ |
|
|
|
|
| def answer_formatter(state: Dict[str, Any]) -> Command: |
| """ |
| Answer Formatter node that creates GAIA-compliant final answers. |
| |
| Takes the draft_answer and formats it according to GAIA requirements. |
| Returns Command to END the workflow. |
| """ |
| |
| print("📝 Answer Formatter: Creating final formatted answer...") |
| |
| try: |
| |
| formatter_prompt = load_formatter_prompt() |
| |
| |
| llm = ChatGroq( |
| model="llama-3.3-70b-versatile", |
| temperature=0.0, |
| max_tokens=512 |
| ) |
| |
| |
| with agent_span( |
| "formatter", |
| metadata={ |
| "draft_answer_length": len(state.get("draft_answer", "")), |
| "user_id": state.get("user_id", "unknown"), |
| "session_id": state.get("session_id", "unknown") |
| } |
| ) as span: |
| |
| |
| draft_answer = state.get("draft_answer", "") |
| |
| if not draft_answer: |
| final_answer = "No answer could be generated." |
| else: |
| |
| messages = state.get("messages", []) |
| user_query = "" |
| for msg in messages: |
| if isinstance(msg, HumanMessage): |
| user_query = msg.content |
| break |
| |
| |
| formatting_request = f""" |
| Extract the final answer from this comprehensive response following GAIA formatting rules: |
| |
| Original Question: {user_query} |
| |
| Draft Response: |
| {draft_answer} |
| |
| Instructions: |
| 1. Identify the core answer within the draft response |
| 2. Remove all explanatory text, prefixes, and formatting |
| 3. Apply GAIA formatting rules exactly |
| 4. Return ONLY the final answer |
| |
| What is the properly formatted final answer? |
| """ |
| |
| |
| formatting_messages = [ |
| SystemMessage(content=formatter_prompt), |
| HumanMessage(content=formatting_request) |
| ] |
| |
| |
| response = llm.invoke(formatting_messages) |
| |
| |
| final_answer = extract_final_answer(response.content) |
| |
| |
| if not final_answer or len(final_answer) < 1: |
| print("⚠️ LLM formatting failed, using direct extraction") |
| final_answer = extract_final_answer(draft_answer) |
| |
| |
| if not final_answer: |
| final_answer = "Unable to extract a clear answer." |
| |
| print(f"📝 Answer Formatter: Final answer = '{final_answer}'") |
| |
| |
| if span: |
| span.update_trace(output={"final_answer": final_answer}) |
| |
| |
| return Command( |
| goto="__end__", |
| update={ |
| "final_answer": final_answer |
| } |
| ) |
| |
| except Exception as e: |
| print(f"❌ Answer Formatter Error: {e}") |
| |
| |
| return Command( |
| goto="__end__", |
| update={ |
| "final_answer": f"Error formatting answer: {str(e)}" |
| } |
| ) |