KPatelis commited on
Commit
6d5c14c
·
verified ·
1 Parent(s): ad9f1b5

Fix issues based on testing

Browse files
Files changed (5) hide show
  1. agent.py +64 -3
  2. app.py +5 -3
  3. prompts/prompt.yaml +9 -18
  4. states.py +3 -1
  5. tools.py +1 -1
agent.py CHANGED
@@ -8,11 +8,14 @@ This module defines a LangGraph agent that can:
8
  """
9
 
10
  import os
 
11
  import bm25s
12
  import requests
13
  from pathlib import Path
14
  from dotenv import load_dotenv
15
 
 
 
16
  from langgraph.graph import START, END, StateGraph
17
  from langgraph.prebuilt import tools_condition, ToolNode
18
 
@@ -76,6 +79,21 @@ _system_prompt = load_prompt("prompts/prompt.yaml")
76
  _thinking_enabled = config["models"]["llm"]["parameters"].get("thinking_enabled", True)
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  # ============================================
80
  # Graph Nodes
81
  # ============================================
@@ -265,10 +283,47 @@ def processor_node(state: AgentState) -> AgentState:
265
  full_messages.extend(messages)
266
 
267
  response = agent_with_tools.invoke(full_messages)
268
-
269
  return {"messages": [response]}
270
 
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  # ============================================
273
  # Graph Construction
274
  # ============================================
@@ -285,14 +340,20 @@ def agent_graph():
285
  workflow.add_node("reranker_node", reranker_node)
286
  workflow.add_node("processor_node", processor_node)
287
  workflow.add_node("tools", ToolNode(tools_list))
 
288
 
289
  # Add edges
290
  workflow.add_edge(START, "file_downloader_node")
291
  workflow.add_edge("file_downloader_node", "retriever_node")
292
  workflow.add_edge("retriever_node", "reranker_node")
293
  workflow.add_edge("reranker_node", "processor_node")
294
- workflow.add_edge("tools", "processor_node")
295
- workflow.add_conditional_edges("processor_node", tools_condition, {"tools": "tools", END: END})
 
 
 
 
 
296
 
297
 
298
  compiled = workflow.compile()
 
8
  """
9
 
10
  import os
11
+ import re
12
  import bm25s
13
  import requests
14
  from pathlib import Path
15
  from dotenv import load_dotenv
16
 
17
+ from pydantic import BaseModel, Field
18
+
19
  from langgraph.graph import START, END, StateGraph
20
  from langgraph.prebuilt import tools_condition, ToolNode
21
 
 
79
  _thinking_enabled = config["models"]["llm"]["parameters"].get("thinking_enabled", True)
80
 
81
 
82
+ class FinalAnswer(BaseModel):
83
+ """Strict GAIA-format answer extracted from the solver's reasoning."""
84
+ answer: str = Field(
85
+ description=(
86
+ "The raw answer value only. "
87
+ "Numbers: plain digits, no commas, no units, no symbols (write '1000000', not '1,000,000' or '$50'). "
88
+ "Strings: no articles ('a', 'an', 'the'), no markdown, no surrounding quotes, no trailing punctuation. "
89
+ "Lists: comma-separated with no extra spaces, in the order requested by the question."
90
+ )
91
+ )
92
+
93
+
94
+ formatter_llm = agent_llm.with_structured_output(FinalAnswer)
95
+
96
+
97
  # ============================================
98
  # Graph Nodes
99
  # ============================================
 
283
  full_messages.extend(messages)
284
 
285
  response = agent_with_tools.invoke(full_messages)
286
+
287
  return {"messages": [response]}
288
 
289
 
290
+ def formatter_node(state: AgentState) -> AgentState:
291
+ """Extract and reformat the solver's answer into a strict GAIA-compliant value."""
292
+ print("--- FORMATTER NODE ---")
293
+ messages = state.get("messages", [])
294
+ if not messages:
295
+ return {"final_answer": ""}
296
+
297
+ question = ""
298
+ for m in messages:
299
+ if isinstance(m, HumanMessage):
300
+ question = m.content
301
+ break
302
+
303
+ solver_output = messages[-1].content or ""
304
+
305
+ prompt = [
306
+ SystemMessage(content=(
307
+ "You extract the final answer from an agent's reasoning. "
308
+ "Apply the GAIA formatting rules exactly. "
309
+ "If the agent never produced an answer, return an empty string."
310
+ )),
311
+ HumanMessage(content=(
312
+ f"Question:\n{question}\n\n"
313
+ f"Agent reasoning and conclusion:\n{solver_output}\n\n"
314
+ "Extract the final answer."
315
+ )),
316
+ ]
317
+
318
+ try:
319
+ result = formatter_llm.invoke(prompt)
320
+ return {"final_answer": result.answer.strip()}
321
+ except Exception as e:
322
+ print(f"Formatter error: {e}")
323
+ match = re.search(r'FINAL ANSWER:\s*(.*)', solver_output, re.DOTALL | re.IGNORECASE)
324
+ return {"final_answer": (match.group(1).strip() if match else solver_output.strip())}
325
+
326
+
327
  # ============================================
328
  # Graph Construction
329
  # ============================================
 
340
  workflow.add_node("reranker_node", reranker_node)
341
  workflow.add_node("processor_node", processor_node)
342
  workflow.add_node("tools", ToolNode(tools_list))
343
+ workflow.add_node("formatter_node", formatter_node)
344
 
345
  # Add edges
346
  workflow.add_edge(START, "file_downloader_node")
347
  workflow.add_edge("file_downloader_node", "retriever_node")
348
  workflow.add_edge("retriever_node", "reranker_node")
349
  workflow.add_edge("reranker_node", "processor_node")
350
+ workflow.add_edge("tools", "processor_node")
351
+ workflow.add_conditional_edges(
352
+ "processor_node",
353
+ tools_condition,
354
+ {"tools": "tools", END: "formatter_node"},
355
+ )
356
+ workflow.add_edge("formatter_node", END)
357
 
358
 
359
  compiled = workflow.compile()
app.py CHANGED
@@ -24,9 +24,11 @@ class BasicAgent:
24
  "task_id": task_id,
25
  "file_name": file_name
26
  })
27
- content = response['messages'][-1].content
28
- match = re.search(r'FINAL ANSWER:\s*(.*)', content, re.DOTALL)
29
- fixed_answer = match.group(1).strip() if match else content.strip()
 
 
30
  print(f"Agent returning fixed answer: {fixed_answer}")
31
  return fixed_answer
32
 
 
24
  "task_id": task_id,
25
  "file_name": file_name
26
  })
27
+ fixed_answer = (response.get("final_answer") or "").strip()
28
+ if not fixed_answer:
29
+ content = response['messages'][-1].content
30
+ match = re.search(r'FINAL ANSWER:\s*(.*)', content, re.DOTALL | re.IGNORECASE)
31
+ fixed_answer = match.group(1).strip() if match else content.strip()
32
  print(f"Agent returning fixed answer: {fixed_answer}")
33
  return fixed_answer
34
 
prompts/prompt.yaml CHANGED
@@ -24,30 +24,21 @@ prompt: |
24
  4. **Observe**: Analyze the tool output. Does it answer the question?
25
  5. **Refine**: If the output is insufficient, adjust the plan and try a different angle.
26
 
27
- # CRITICAL OUTPUT RULES
28
- The automated scoring system is EXTREMELY STRICT. You must follow these formatting rules exactly:
29
- - **Numeric Answers**:
30
- - Output ONLY the number.
31
- - NO commas (e.g., write `1000000`, NOT `1,000,000`).
32
- - NO units or symbols (e.g., write `50`, NOT `$50` or `50%`, unless explicitly asked for the unit string).
33
- - **String Answers**:
34
- - Be concise.
35
- - NO articles (a, an, the).
36
- - NO abbreviations usually, unless standard (e.g., 'USA' might be okay, but 'Sept' for September is risky).
37
- - **Final Format**:
38
- - Your final line MUST be exactly: `FINAL ANSWER: <your_answer>`
39
- - Do not put proper sentences in the final answer, just the raw value.
40
 
41
  # EXAMPLE SCENARIOS
42
  - Question: "What is the sum of the 'Total' column in the attached file.xlsx?"
43
- Thought: I need to read the excel file first using `read_excel`. Then I will sum the values using `calculator`.
44
  Action: `read_excel(file_path="...")`
45
- Observation: (Dataframe output...)
46
- Thought: I have the numbers. Sum is 500 + 200...
47
- Action: `calculator(a=500, b=200, type="addition")`
48
  FINAL ANSWER: 700
49
 
50
  - Question: "Which city is the capital of France?"
51
  FINAL ANSWER: Paris
52
 
53
- Failure to follow these rules will result in a score of 0. Take a deep breath and think step by step.
 
24
  4. **Observe**: Analyze the tool output. Does it answer the question?
25
  5. **Refine**: If the output is insufficient, adjust the plan and try a different angle.
26
 
27
+ # FINAL ANSWER
28
+ When you have reached the answer, end your message with a single concluding line of the form:
29
+
30
+ FINAL ANSWER: <value>
31
+
32
+ A downstream formatter will normalise the value into the strict GAIA scoring format (no commas in numbers, no articles in strings, comma-separated lists, etc.) so prioritise correctness of the value itself over format pedantry. Do not wrap the line in markdown, do not add commentary after it, and do not restate it.
 
 
 
 
 
 
 
33
 
34
  # EXAMPLE SCENARIOS
35
  - Question: "What is the sum of the 'Total' column in the attached file.xlsx?"
36
+ Thought: I need to read the excel file first using `read_excel`, then check the column stats it returns.
37
  Action: `read_excel(file_path="...")`
38
+ Observation: (Dataframe + describe() stats…)
 
 
39
  FINAL ANSWER: 700
40
 
41
  - Question: "Which city is the capital of France?"
42
  FINAL ANSWER: Paris
43
 
44
+ Take a deep breath and think step by step.
states.py CHANGED
@@ -17,9 +17,11 @@ class AgentState(TypedDict):
17
  file_name: Name of the attached file (empty string if no file)
18
  file_path: Local filesystem path to the downloaded file (empty if no file or download failed)
19
  retrieved_docs: List of candidate documents from the retriever node
 
20
  """
21
  messages: Annotated[list[BaseMessage], add_messages]
22
  task_id: str
23
  file_name: str
24
  file_path: str
25
- retrieved_docs: List[Document]
 
 
17
  file_name: Name of the attached file (empty string if no file)
18
  file_path: Local filesystem path to the downloaded file (empty if no file or download failed)
19
  retrieved_docs: List of candidate documents from the retriever node
20
+ final_answer: GAIA-formatted answer produced by the formatter node
21
  """
22
  messages: Annotated[list[BaseMessage], add_messages]
23
  task_id: str
24
  file_name: str
25
  file_path: str
26
+ retrieved_docs: List[Document]
27
+ final_answer: str
tools.py CHANGED
@@ -72,7 +72,7 @@ def duck_web_search(query: str) -> str:
72
  Args:
73
  query: The search query.
74
  """
75
- search = _get_ddg().invoke(query=query)
76
 
77
  return {"duckduckgo_web_search": search}
78
 
 
72
  Args:
73
  query: The search query.
74
  """
75
+ search = _get_ddg().invoke(input=query)
76
 
77
  return {"duckduckgo_web_search": search}
78