Files changed (1) hide show
  1. agent.py +131 -0
agent.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ReAct Agent powered by Groq (llama-3.3-70b-versatile).
3
+ Runs a Thought β†’ Action β†’ Observation loop until it reaches a Final Answer.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import json
9
+ from groq import Groq
10
+ from tools import TOOLS
11
+
12
+ # ── System prompt ─────────────────────────────────────────────────────────────
13
+ SYSTEM_PROMPT = """You are a precise, detail-oriented AI assistant solving questions from the GAIA benchmark.
14
+ You answer step-by-step using a ReAct loop: Thought, Action, Observation, repeat, then Final Answer.
15
+
16
+ Available tools:
17
+ {tool_descriptions}
18
+
19
+ FORMAT β€” always follow this exactly:
20
+ Thought: <your reasoning about what to do next>
21
+ Action: <tool_name>
22
+ Action Input: <input to the tool, as plain text>
23
+
24
+ After you receive an Observation, continue with another Thought/Action or:
25
+ Final Answer: <your concise, exact answer>
26
+
27
+ Rules:
28
+ - Use tools whenever you need external facts, calculations, or files.
29
+ - Final Answer must be concise and exact β€” no extra sentences.
30
+ - If the answer is a number, give just the number (with units if asked).
31
+ - If the answer is a list, comma-separate it.
32
+ - Never make up facts. If unsure, search again.
33
+ - Do NOT include the text "FINAL ANSWER:" in capitals β€” just write "Final Answer:".
34
+ """
35
+
36
+ def _build_tool_descriptions() -> str:
37
+ lines = []
38
+ for name, meta in TOOLS.items():
39
+ lines.append(f"- {name}: {meta['description']}")
40
+ return "\n".join(lines)
41
+
42
+
43
+ FILLED_SYSTEM = SYSTEM_PROMPT.format(tool_descriptions=_build_tool_descriptions())
44
+
45
+
46
+ class GAIAAgent:
47
+ def __init__(self, max_steps: int = 8):
48
+ self.client = Groq(api_key=os.environ.get("GROQ_API_KEY"))
49
+ self.model = "llama-3.3-70b-versatile"
50
+ self.max_steps = max_steps
51
+
52
+ def _call_llm(self, messages: list) -> str:
53
+ response = self.client.chat.completions.create(
54
+ model=self.model,
55
+ messages=messages,
56
+ temperature=0.0,
57
+ max_tokens=1024,
58
+ )
59
+ return response.choices[0].message.content.strip()
60
+
61
+ def _parse_action(self, text: str):
62
+ """
63
+ Returns (tool_name, tool_input) if an Action is present, else None.
64
+ """
65
+ action_match = re.search(r"Action:\s*(.+)", text)
66
+ input_match = re.search(r"Action Input:\s*([\s\S]+?)(?=\nThought:|\nAction:|\nObservation:|\nFinal Answer:|$)", text)
67
+ if action_match and input_match:
68
+ tool = action_match.group(1).strip()
69
+ tool_input = input_match.group(1).strip()
70
+ return tool, tool_input
71
+ return None
72
+
73
+ def _parse_final_answer(self, text: str):
74
+ match = re.search(r"Final Answer:\s*([\s\S]+)", text, re.IGNORECASE)
75
+ if match:
76
+ return match.group(1).strip()
77
+ return None
78
+
79
+ def _run_tool(self, tool_name: str, tool_input: str) -> str:
80
+ if tool_name not in TOOLS:
81
+ return f"Unknown tool '{tool_name}'. Available tools: {', '.join(TOOLS.keys())}"
82
+ try:
83
+ return TOOLS[tool_name]["fn"](tool_input)
84
+ except Exception as e:
85
+ return f"Tool error: {e}"
86
+
87
+ def __call__(self, question: str, task_id: str = "") -> str:
88
+ """Run the ReAct loop and return the final answer string."""
89
+ messages = [
90
+ {"role": "system", "content": FILLED_SYSTEM},
91
+ {"role": "user", "content": (
92
+ f"Task ID: {task_id}\n\nQuestion: {question}\n\n"
93
+ "Begin your ReAct loop. If the question mentions an attached file, "
94
+ f"use get_task_file with task_id='{task_id}' first."
95
+ )},
96
+ ]
97
+
98
+ for step in range(self.max_steps):
99
+ llm_output = self._call_llm(messages)
100
+ messages.append({"role": "assistant", "content": llm_output})
101
+
102
+ # Check for final answer first
103
+ final = self._parse_final_answer(llm_output)
104
+ if final:
105
+ return final
106
+
107
+ # Check for action
108
+ action_result = self._parse_action(llm_output)
109
+ if action_result:
110
+ tool_name, tool_input = action_result
111
+ observation = self._run_tool(tool_name, tool_input)
112
+ obs_msg = f"Observation: {observation}"
113
+ messages.append({"role": "user", "content": obs_msg})
114
+ else:
115
+ # LLM didn't produce a valid action or final answer β€” nudge it
116
+ messages.append({
117
+ "role": "user",
118
+ "content": (
119
+ "Please continue. Either use a tool (Action + Action Input) "
120
+ "or provide your Final Answer."
121
+ )
122
+ })
123
+
124
+ # Exhausted steps β€” ask for best guess
125
+ messages.append({
126
+ "role": "user",
127
+ "content": "You've reached the step limit. Provide your best Final Answer now.",
128
+ })
129
+ last = self._call_llm(messages)
130
+ final = self._parse_final_answer(last)
131
+ return final if final else last.strip()