Spaces:
Sleeping
Sleeping
Fix issues based on testing
Browse files- agent.py +64 -3
- app.py +5 -3
- prompts/prompt.yaml +9 -18
- states.py +3 -1
- 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
| 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 |
-
#
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 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`
|
| 44 |
Action: `read_excel(file_path="...")`
|
| 45 |
-
Observation: (Dataframe
|
| 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 |
-
|
|
|
|
| 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(
|
| 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 |
|