jafhaponiuk commited on
Commit
a7a6d1e
·
verified ·
1 Parent(s): 18cb4ee

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +155 -139
agent.py CHANGED
@@ -1,45 +1,39 @@
1
- # agent.py
2
- from langchain_core.runnables import RunnableLambda, RunnableMap
3
- from langchain_core.output_parsers import StrOutputParser
4
- from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
5
- from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
 
 
 
 
 
6
  from langgraph.graph import StateGraph, END
7
- import json
8
- from typing import TypedDict, List, Dict, Any, Union # Import TypedDict and other types for better type hints
9
 
10
- from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace
11
- from tools import tools # Ensure 'tools' is a list of LangChain Tool objects
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  # Load the system prompt from file
14
  with open("system_prompt.txt", "r", encoding="utf-8") as f:
15
- SYSTEM_PROMPT = f.read()
16
-
17
- # Define the shared memory structure for the agent
18
- # Use TypedDict for better type hinting and explicit state schema
19
- class AgentState(TypedDict):
20
- """
21
- Represents the state of the agent at each step of the graph.
22
- All keys are required when creating an AgentState, but can be None or default values.
23
- """
24
- input: str # The original user query
25
- chat_history: List[Union[HumanMessage, AIMessage, ToolMessage]] # History of messages
26
- llm_response_raw: AIMessage # The raw AIMessage response from the LLM in the decision step
27
- parsed_action: Dict[str, Any] # The dictionary with the parsed action (tool_name, tool_args, or final_answer)
28
- tool_output: Any # The output from the execution of a tool (can be str or dict from tool)
29
- output: str # The final answer from the agent to the user (formatted for GAIA)
30
-
31
- # Initialize the language model (using Hugging Face Inference Endpoint)
32
- # Ensure that this URL is correct and your HF token is available in the environment.
33
- endpoint = HuggingFaceEndpoint(
34
- endpoint_url="https://api-inference.huggingface.co/models/Meta-Llama/llama-3-70b-instruct",
35
- temperature=0.01, # A little temperature can aid creativity, but for tool-use decisions, 0 is common.
36
- # It is crucial that your HF token is available as an environment variable HF_TOKEN
37
- )
38
- llm = ChatHuggingFace(llm=endpoint)
39
 
40
- # Helper to describe tools to the LLM
41
  def get_tool_descriptions(tool_list):
42
- """Generates a string of tool descriptions for the LLM's prompt."""
 
 
 
43
  descriptions = []
44
  for tool_item in tool_list:
45
  args_schema_str = ""
@@ -64,20 +58,31 @@ def get_tool_descriptions(tool_list):
64
  except Exception:
65
  args_schema_str = f"Arguments Schema: {tool_item.args_schema.__name__}"
66
 
67
-
68
  descriptions.append(f"- {tool_item.name}: {tool_item.description} {args_schema_str}")
69
  return "\n".join(descriptions)
70
 
71
- # Custom function to invoke tools by name and arguments
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  def invoke_tool(tool_name: str, tool_args: dict):
73
  """Invokes a tool with its name and a dictionary of arguments."""
74
  print(f"[{__name__}] Invoking tool: '{tool_name}' with args: {tool_args}")
75
- for tool_item in tools: # 'tools' here comes from the import tools.py
 
76
  if tool_item.name == tool_name:
77
  try:
78
- # LangChain's @tool decorated functions are designed to work well
79
- # when invoked with a dictionary of keyword arguments.
80
- # The tool_item.invoke() method handles mapping these to function parameters.
81
  result = tool_item.invoke(tool_args)
82
  print(f"[{__name__}] Tool '{tool_name}' returned: {result}")
83
  return result
@@ -85,95 +90,105 @@ def invoke_tool(tool_name: str, tool_args: dict):
85
  return f"Error executing tool '{tool_name}' with args {tool_args}: {str(e)}"
86
  raise ValueError(f"Tool '{tool_name}' not found")
87
 
 
 
88
  # 4. Node: Initial LLM call - Decide if tool is needed AND what arguments to use
89
  def call_llm_decide(state: AgentState) -> AgentState:
90
  """
91
  Prompts the LLM to decide on a tool and its arguments, or provide a direct answer.
92
  The LLM is expected to output a JSON string.
93
  """
94
- print(f"[{__name__}] call_llm_decide: Initial state received (keys): {list(state.keys())}") # DEBUG PRINT
95
- current_input = state["input"]
96
- chat_history = state.get("chat_history", []) # Use .get for optional keys initially (though 'chat_history' should always be there from initial_state)
97
-
98
- tool_descriptions = get_tool_descriptions(tools)
99
 
100
- # Inject tool_descriptions into the SYSTEM_PROMPT
101
- # The SYSTEM_PROMPT now has a placeholder {{tool_descriptions}}
102
- formatted_system_prompt = SYSTEM_PROMPT.replace("{{tool_descriptions}}", tool_descriptions)
 
 
103
 
104
- # CRUCIAL: The prompt needs to guide the LLM to output a specific JSON format
105
- # Llama 3 Instruct is generally good at following this if trained or prompted well.
106
  decision_prompt_template = ChatPromptTemplate.from_messages([
107
- ("system", formatted_system_prompt), # Use the formatted system prompt here
108
  MessagesPlaceholder(variable_name="chat_history"),
109
- ("human", "{input}"),
110
  ])
111
 
112
- chain = decision_prompt_template | llm
 
 
 
 
 
 
 
 
 
 
 
113
  response = chain.invoke({
114
  "input": current_input,
115
- "chat_history": chat_history
 
116
  })
117
 
118
  print(f"[{__name__}] LLM raw decision response: {response.content}")
119
 
120
- # Parse the LLM's JSON output
121
- parsed_action: Dict[str, Any] = {} # Explicitly type as Dict[str, Any]
122
  try:
123
  parsed_action = json.loads(response.content)
124
  except json.JSONDecodeError as e:
125
  print(f"[{__name__}] Error parsing LLM JSON output: {e}. Raw content: {response.content}")
126
- # Fallback: if parsing fails, assume it's a direct answer or an error.
127
- # This can happen if the LLM doesn't follow the JSON format strictly.
128
  parsed_action = {"action": "final_answer", "answer": f"Error: Could not parse LLM's action. Raw LLM output: {response.content[:200]}..."}
129
 
130
- # Add the LLM's raw response and parsed action to history for context
131
- # Note: We add the HumanMessage here so the chat_history for the LLM is complete before the next step.
132
  new_state = AgentState(
133
- input=current_input, # Always carry over input
134
  chat_history=chat_history + [HumanMessage(content=current_input), response],
135
- llm_response_raw=response, # Store the full AIMessage
136
- parsed_action=parsed_action, # Store the parsed dictionary
137
- tool_output="", # Initialize with empty string, will be populated by call_tool_node if needed
138
- output="" # Initialize output, will be populated by generate_final_answer
 
139
  )
140
- print(f"[{__name__}] call_llm_decide: State being returned (keys): {list(new_state.keys())}") # DEBUG PRINT
141
  return new_state
142
 
143
  # 5. Node: Execute Tool (if chosen)
144
  def call_tool_node(state: AgentState) -> AgentState:
145
  """Executes the chosen tool with the provided arguments."""
146
- print(f"[{__name__}] call_tool_node: State received (keys): {list(state.keys())}") # DEBUG PRINT
 
147
  parsed_action = state["parsed_action"]
148
  tool_name = parsed_action.get("tool_name")
149
  tool_args = parsed_action.get("tool_args", {})
150
-
151
- tool_output = ""
152
  tool_message = None
153
 
154
- if tool_name: # If a tool was indicated in the parsed action
155
  try:
156
- tool_output = invoke_tool(tool_name, tool_args)
157
- tool_message = ToolMessage(content=str(tool_output), name=tool_name)
 
 
158
  except Exception as e:
159
- tool_output = f"Error calling tool '{tool_name}': {str(e)}"
160
  tool_message = ToolMessage(content=tool_output, name="tool_error")
161
  else:
162
- # Fallback if somehow reached here without a tool_name
163
  tool_output = "No tool name provided by LLM, but reached tool execution node."
164
  tool_message = ToolMessage(content=tool_output, name="no_tool_fallback")
165
-
166
  print(f"[{__name__}] Tool '{tool_name}' output: {tool_output}")
167
 
168
  new_state = AgentState(
169
- input=state["input"], # Carry over from previous state
170
- chat_history=state["chat_history"] + [tool_message], # Add the tool's output to history
171
- llm_response_raw=state["llm_response_raw"], # Carry over from previous state
172
- parsed_action=state["parsed_action"], # Carry over from previous state
173
- tool_output=tool_output, # Store the actual output
174
- output="" # Will be populated by generate_final_answer
 
175
  )
176
- print(f"[{__name__}] call_tool_node: State being returned (keys): {list(new_state.keys())}") # DEBUG PRINT
177
  return new_state
178
 
179
  # 6. Node: Generate Final Answer (after tool or direct)
@@ -182,86 +197,90 @@ def generate_final_answer(state: AgentState) -> AgentState:
182
  Generates the final answer based on the conversation history,
183
  including any tool outputs, and formats it for GAIA evaluation.
184
  """
185
- print(f"[{__name__}] generate_final_answer: State received (keys): {list(state.keys())}") # DEBUG PRINT
186
  current_input = state["input"]
187
- chat_history = state["chat_history"] # chat_history should already be populated from previous nodes
188
- parsed_action = state["parsed_action"] # Get the action from the initial decision
 
189
 
190
  final_answer_content = ""
191
 
192
- # If the LLM already gave a final answer directly in the initial decision, use that.
193
- if parsed_action.get("action") == "final_answer" and "answer" in parsed_action:
194
  final_answer_content = parsed_action["answer"]
195
  print(f"[{__name__}] Using direct final answer from initial LLM decision: {final_answer_content}")
196
  else:
197
  # If a tool was used, or if the LLM didn't give a direct answer initially,
198
  # prompt the LLM again to synthesize the final answer.
199
- # The chat_history now contains the tool call and its output.
200
- # This prompt should guide the LLM to give the answer content ONLY.
201
  final_prompt_template = ChatPromptTemplate.from_messages([
202
- ("system", SYSTEM_PROMPT.replace("{{tool_descriptions}}", get_tool_descriptions(tools)) + "\n\n" # Re-inject descriptions
203
- "You have processed the user's request, possibly using tools. "
204
- "Now, synthesize all available information, including the results of any tool executions, "
205
- "to provide a concise and direct final answer to the user. "
206
- "If a tool was executed, use its output to formulate the answer. "
207
- "If no tool was executed (or it failed), try to answer based on your general knowledge or indicate the issue."
208
- "Crucially, provide ONLY the answer content, without any prefix, conversational filler, or apologies."
209
- "For numbers, do not include commas, currency symbols, or units unless explicitly requested."
210
- "For strings, write digits in plain text unless otherwise specified."
211
- "The final output will be prefixed by 'FINAL ANSWER:' by the system."),
212
  MessagesPlaceholder(variable_name="chat_history"), # This history now includes the tool call and its output
213
- ("human", "{input}"), # Still pass the original input for context
214
- # Explicitly indicate that the LLM should give only the answer
215
- AIMessage(content="Based on the above, your concise answer is:") # This acts as a strong steer for LLM to provide only the answer
216
  ])
217
- chain = final_prompt_template | llm | StrOutputParser()
 
 
 
 
 
 
 
 
 
 
 
 
218
  print(f"[{__name__}] Generating final answer content with chat history and tool output...")
219
- final_answer_content = chain.invoke({
220
  "input": current_input,
221
  "chat_history": chat_history # Full history including tool outputs
222
  })
223
  print(f"[{__name__}] Generated raw final answer content: {final_answer_content}")
224
 
225
- # **CRUCIAL GAIA REQUIREMENT: Add the "FINAL ANSWER:" prefix here**
226
  gaia_formatted_answer = f"FINAL ANSWER: {final_answer_content.strip()}"
227
  print(f"[{__name__}] GAIA formatted final answer: {gaia_formatted_answer}")
228
 
229
  new_state = AgentState(
230
- input=current_input, # Carry over from previous state
231
  chat_history=chat_history + [AIMessage(content=gaia_formatted_answer)],
232
- llm_response_raw=state.get("llm_response_raw", AIMessage(content="")), # Use .get with default for robustness
233
- parsed_action=state.get("parsed_action", {}), # Use .get with default
234
- tool_output=state.get("tool_output", ""), # Use .get with default
235
- output=gaia_formatted_answer # This is the string returned to the user/GAIA
 
236
  )
237
- print(f"[{__name__}] generate_final_answer: State being returned (keys): {list(new_state.keys())}") # DEBUG PRINT
238
  return new_state
239
 
240
  # 7. Router: Decide which path to take after initial LLM call
241
  def route_action(state: AgentState) -> str:
242
  """Routes the graph based on the LLM's parsed action."""
243
- print(f"[{__name__}] route_action: State received (keys): {list(state.keys())}") # DEBUG PRINT
244
- parsed_action = state["parsed_action"]
245
- action_type = parsed_action.get("action")
 
 
 
246
 
 
247
  if action_type == "tool":
248
  print(f"[{__name__}] Routing to 'execute_tool' based on LLM decision.")
249
  return "execute_tool"
250
  elif action_type == "final_answer":
251
  print(f"[{__name__}] Routing to 'generate_final_answer' directly based on LLM decision.")
252
- return "generate_final_answer" # Direct to final answer node if LLM decided directly
253
  else:
254
- print(f"[{__name__}] Unknown action type '{action_type}' or no action. Defaulting to 'generate_final_answer'.")
255
- # Fallback if the LLM didn't produce a recognizable action
256
  return "generate_final_answer" # Route to final answer, might be an error case
257
 
258
  # 8. Build the agent graph
259
- builder = StateGraph(AgentState) # Use the updated AgentState
260
 
261
  # Add nodes
262
  builder.add_node("initial_llm_decision", call_llm_decide)
263
  builder.add_node("execute_tool", call_tool_node)
264
- builder.add_node("generate_final_answer", generate_final_answer) # This node generates final answer
265
 
266
  # Set the entry point
267
  builder.set_entry_point("initial_llm_decision")
@@ -288,21 +307,23 @@ agent_executor = builder.compile()
288
  class BasicAgent:
289
  def __init__(self):
290
  self.agent = agent_executor
291
-
292
  def __call__(self, question: str) -> str:
293
  # The initial state for each new question
294
  initial_state: AgentState = { # Explicitly type the dictionary as AgentState
295
  "input": question,
296
  "chat_history": [],
297
- # Provide default empty/dummy values for all TypedDict keys
298
- "llm_response_raw": AIMessage(content=""),
299
- "parsed_action": {},
300
- "tool_output": "",
301
- "output": ""
302
  }
 
303
  # The invoke will run through the graph
304
  # LangGraph's invoke returns the final state
305
- final_state = self.agent.invoke(initial_state) # Pass the explicitly typed initial state
 
306
  # Extract the final output from the state
307
  return final_state.get("output", "No answer could be generated.")
308
 
@@ -310,35 +331,30 @@ if __name__ == "__main__":
310
  # Example Usage (for local testing)
311
  print("Testing BasicAgent locally...")
312
 
313
- # For local testing, ensure you have:
314
- # 1. A mock or real `tools.py` that provides `tools` (a list of LangChain Tool objects).
315
- # 2. A `system_prompt.txt` file (create it if not present, with example content below).
316
- # 3. Your Hugging Face token set as an environment variable (HF_TOKEN).
317
-
318
  try:
319
  agent = BasicAgent()
 
320
  print("\n--- Test 1: Simple question, should directly answer ---")
321
  response1 = agent("What is the capital of France?")
322
  print(f"Agent Response: {response1}")
323
-
324
  print("\n--- Test 2: Question requiring a tool (e.g., web_search) ---")
325
- # This will test if the agent can correctly call web_search
326
  # Make sure TAVILY_API_KEY is set in your environment for this to work
327
  response2 = agent("What is the current population of the United States? (as of today)")
328
  print(f"Agent Response: {response2}")
329
-
330
- print("\n--- Test 3: Math question (e.g., multiply) ---")
331
  response3 = agent("What is 15 multiplied by 23?")
332
  print(f"Agent Response: {response3}")
333
-
334
  print("\n--- Test 4: Question requiring Python code execution ---")
335
  response4 = agent("What is the result of the python code: `sum([x**2 for x in range(1, 5)])`?")
336
  print(f"Agent Response: {response4}")
337
-
338
  print("\n--- Test 5: Question with no clear tool, but needs a general answer ---")
339
  response5 = agent("What is the meaning of life?")
340
  print(f"Agent Response: {response5}")
341
 
342
  except Exception as e:
343
  print(f"\nError during local testing: {e}")
344
- print("Please ensure your HF_TOKEN and TAVILY_API_KEY are set, and 'tools.py' is correctly implemented.")
 
1
+ import operator
2
+ import os
3
+ import json # Required for json.loads in parse_llm_response
4
+ from typing import TypedDict, Annotated, List, Dict, Any, Union
5
+ from datetime import datetime
6
+
7
+ from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
8
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, HumanMessagePromptTemplate, SystemMessagePromptTemplate
9
+ from langchain_core.runnables import RunnableLambda, RunnableMap # Added RunnableMap for input mapping
10
+
11
  from langgraph.graph import StateGraph, END
 
 
12
 
13
+ # Use ChatGoogleGenerativeAI as per previous instructions for Gemini
14
+ from langchain_google_genai import ChatGoogleGenerativeAI
15
+
16
+ # Import tools_for_llm from your tools.py file
17
+ from tools import tools_for_llm # This imports the list of all your tools as tools_for_llm
18
+
19
+ # Load environment variables
20
+ from dotenv import load_dotenv
21
+ load_dotenv()
22
+
23
+ # Set up the LLM
24
+ # Ensure your GOOGLE_API_KEY is set as an environment variable
25
+ llm = ChatGoogleGenerativeAI(model="gemini-1.5-pro", temperature=0)
26
 
27
  # Load the system prompt from file
28
  with open("system_prompt.txt", "r", encoding="utf-8") as f:
29
+ SYSTEM_PROMPT_CONTENT = f.read() # Renamed to avoid confusion with variable names
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
+ # --- Helper to describe tools to the LLM ---
32
  def get_tool_descriptions(tool_list):
33
+ """Generates a string of tool descriptions for the LLM's prompt.
34
+ This function generates descriptions without any special escaping for LangChain's templating,
35
+ as the entire string will be passed as a single variable.
36
+ """
37
  descriptions = []
38
  for tool_item in tool_list:
39
  args_schema_str = ""
 
58
  except Exception:
59
  args_schema_str = f"Arguments Schema: {tool_item.args_schema.__name__}"
60
 
 
61
  descriptions.append(f"- {tool_item.name}: {tool_item.description} {args_schema_str}")
62
  return "\n".join(descriptions)
63
 
64
+ # --- Agent State Definition ---
65
+ class AgentState(TypedDict):
66
+ """
67
+ Represents the state of the agent at each step of the graph.
68
+ All keys are required when creating an AgentState, but can be None or default values.
69
+ """
70
+ input: str # The original user query
71
+ chat_history: Annotated[List[Union[HumanMessage, AIMessage, ToolMessage]], operator.add] # History of messages
72
+ llm_response_raw: Union[AIMessage, None] # The raw AIMessage response from the LLM in the decision step
73
+ parsed_action: Union[Dict[str, Any], None] # The dictionary with the parsed action (tool_name, tool_args, or final_answer)
74
+ tool_output: Union[Any, None] # The output from the execution of a tool (can be str or dict from tool)
75
+ output: Union[str, None] # The final answer from the agent to the user (formatted for GAIA)
76
+ tool_descriptions_str: str # Added to explicitly pass tool descriptions through state
77
+
78
+ # --- Custom function to invoke tools by name and arguments ---
79
  def invoke_tool(tool_name: str, tool_args: dict):
80
  """Invokes a tool with its name and a dictionary of arguments."""
81
  print(f"[{__name__}] Invoking tool: '{tool_name}' with args: {tool_args}")
82
+ # Use tools_for_llm, which is imported from tools.py
83
+ for tool_item in tools_for_llm:
84
  if tool_item.name == tool_name:
85
  try:
 
 
 
86
  result = tool_item.invoke(tool_args)
87
  print(f"[{__name__}] Tool '{tool_name}' returned: {result}")
88
  return result
 
90
  return f"Error executing tool '{tool_name}' with args {tool_args}: {str(e)}"
91
  raise ValueError(f"Tool '{tool_name}' not found")
92
 
93
+ # --- Nodes in the Agent Graph ---
94
+
95
  # 4. Node: Initial LLM call - Decide if tool is needed AND what arguments to use
96
  def call_llm_decide(state: AgentState) -> AgentState:
97
  """
98
  Prompts the LLM to decide on a tool and its arguments, or provide a direct answer.
99
  The LLM is expected to output a JSON string.
100
  """
101
+ print(f"[{__name__}] call_llm_decide: Initial state received (keys): {list(state.keys())}")
 
 
 
 
102
 
103
+ current_input = state["input"]
104
+ chat_history = state.get("chat_history", [])
105
+
106
+ # Generate the string of tool descriptions
107
+ tool_descriptions_string = get_tool_descriptions(tools_for_llm)
108
 
109
+ # Define the ChatPromptTemplate.
110
+ # 'tool_descriptions' is now an explicit input variable for the template.
111
  decision_prompt_template = ChatPromptTemplate.from_messages([
112
+ SystemMessagePromptTemplate.from_template(SYSTEM_PROMPT_CONTENT), # SYSTEM_PROMPT_CONTENT contains {{tool_descriptions}}
113
  MessagesPlaceholder(variable_name="chat_history"),
114
+ HumanMessagePromptTemplate.from_template("{input}"),
115
  ])
116
 
117
+ # Construct the chain for the LLM decision using RunnableMap for explicit input passing
118
+ chain = (
119
+ RunnableMap({
120
+ "input": operator.itemgetter("input"),
121
+ "chat_history": operator.itemgetter("chat_history"),
122
+ # Pass the generated tool descriptions string as the 'tool_descriptions' variable
123
+ "tool_descriptions": RunnableLambda(lambda x: tool_descriptions_string)
124
+ })
125
+ | decision_prompt_template
126
+ | llm
127
+ )
128
+
129
  response = chain.invoke({
130
  "input": current_input,
131
+ "chat_history": chat_history,
132
+ # 'tool_descriptions' is handled by the RunnableMap above, no need to pass here directly
133
  })
134
 
135
  print(f"[{__name__}] LLM raw decision response: {response.content}")
136
 
137
+ parsed_action: Dict[str, Any] = {}
 
138
  try:
139
  parsed_action = json.loads(response.content)
140
  except json.JSONDecodeError as e:
141
  print(f"[{__name__}] Error parsing LLM JSON output: {e}. Raw content: {response.content}")
 
 
142
  parsed_action = {"action": "final_answer", "answer": f"Error: Could not parse LLM's action. Raw LLM output: {response.content[:200]}..."}
143
 
 
 
144
  new_state = AgentState(
145
+ input=current_input,
146
  chat_history=chat_history + [HumanMessage(content=current_input), response],
147
+ llm_response_raw=response,
148
+ parsed_action=parsed_action,
149
+ tool_output=None, # Initialize with None
150
+ output=None, # Initialize with None
151
+ tool_descriptions_str=tool_descriptions_string # Store for subsequent nodes if needed
152
  )
153
+ print(f"[{__name__}] call_llm_decide: State being returned (keys): {list(new_state.keys())}")
154
  return new_state
155
 
156
  # 5. Node: Execute Tool (if chosen)
157
  def call_tool_node(state: AgentState) -> AgentState:
158
  """Executes the chosen tool with the provided arguments."""
159
+ print(f"[{__name__}] call_tool_node: State received (keys): {list(state.keys())}")
160
+
161
  parsed_action = state["parsed_action"]
162
  tool_name = parsed_action.get("tool_name")
163
  tool_args = parsed_action.get("tool_args", {})
164
+ tool_output = None # Initialize as None
 
165
  tool_message = None
166
 
167
+ if tool_name:
168
  try:
169
+ raw_tool_output = invoke_tool(tool_name, tool_args)
170
+ # Ensure tool_output is always a string for ToolMessage content
171
+ tool_output = str(raw_tool_output)
172
+ tool_message = ToolMessage(content=tool_output, name=tool_name)
173
  except Exception as e:
174
+ tool_output = f"Error executing tool '{tool_name}': {str(e)}"
175
  tool_message = ToolMessage(content=tool_output, name="tool_error")
176
  else:
 
177
  tool_output = "No tool name provided by LLM, but reached tool execution node."
178
  tool_message = ToolMessage(content=tool_output, name="no_tool_fallback")
179
+
180
  print(f"[{__name__}] Tool '{tool_name}' output: {tool_output}")
181
 
182
  new_state = AgentState(
183
+ input=state["input"],
184
+ chat_history=state["chat_history"] + [tool_message],
185
+ llm_response_raw=state["llm_response_raw"],
186
+ parsed_action=state["parsed_action"],
187
+ tool_output=tool_output,
188
+ output=None, # Will be populated by generate_final_answer
189
+ tool_descriptions_str=state["tool_descriptions_str"]
190
  )
191
+ print(f"[{__name__}] call_tool_node: State being returned (keys): {list(new_state.keys())}")
192
  return new_state
193
 
194
  # 6. Node: Generate Final Answer (after tool or direct)
 
197
  Generates the final answer based on the conversation history,
198
  including any tool outputs, and formats it for GAIA evaluation.
199
  """
200
+ print(f"[{__name__}] generate_final_answer: State received (keys): {list(state.keys())}")
201
  current_input = state["input"]
202
+ chat_history = state["chat_history"]
203
+ parsed_action = state["parsed_action"]
204
+ tool_descriptions_string = state["tool_descriptions_str"] # Use the stored descriptions
205
 
206
  final_answer_content = ""
207
 
208
+ if parsed_action and parsed_action.get("action") == "final_answer" and "answer" in parsed_action:
 
209
  final_answer_content = parsed_action["answer"]
210
  print(f"[{__name__}] Using direct final answer from initial LLM decision: {final_answer_content}")
211
  else:
212
  # If a tool was used, or if the LLM didn't give a direct answer initially,
213
  # prompt the LLM again to synthesize the final answer.
 
 
214
  final_prompt_template = ChatPromptTemplate.from_messages([
215
+ SystemMessagePromptTemplate.from_template(SYSTEM_PROMPT_CONTENT), # SYSTEM_PROMPT_CONTENT contains {{tool_descriptions}}
 
 
 
 
 
 
 
 
 
216
  MessagesPlaceholder(variable_name="chat_history"), # This history now includes the tool call and its output
217
+ HumanMessagePromptTemplate.from_template("{input}"), # Still pass the original input for context
218
+ AIMessage(content="Based on the above, your concise answer is:") # Strong steer for LLM
 
219
  ])
220
+
221
+ # Build chain for final answer generation, including RunnableMap
222
+ final_answer_chain = (
223
+ RunnableMap({
224
+ "input": operator.itemgetter("input"),
225
+ "chat_history": operator.itemgetter("chat_history"),
226
+ "tool_descriptions": RunnableLambda(lambda x: tool_descriptions_string) # Pass the generated descriptions
227
+ })
228
+ | final_prompt_template
229
+ | llm
230
+ | StrOutputParser()
231
+ )
232
+
233
  print(f"[{__name__}] Generating final answer content with chat history and tool output...")
234
+ final_answer_content = final_answer_chain.invoke({
235
  "input": current_input,
236
  "chat_history": chat_history # Full history including tool outputs
237
  })
238
  print(f"[{__name__}] Generated raw final answer content: {final_answer_content}")
239
 
240
+ # CRUCIAL GAIA REQUIREMENT: Add the "FINAL ANSWER:" prefix here
241
  gaia_formatted_answer = f"FINAL ANSWER: {final_answer_content.strip()}"
242
  print(f"[{__name__}] GAIA formatted final answer: {gaia_formatted_answer}")
243
 
244
  new_state = AgentState(
245
+ input=current_input,
246
  chat_history=chat_history + [AIMessage(content=gaia_formatted_answer)],
247
+ llm_response_raw=state.get("llm_response_raw", AIMessage(content="")),
248
+ parsed_action=state.get("parsed_action", {}),
249
+ tool_output=state.get("tool_output", None), # Use None as default
250
+ output=gaia_formatted_answer,
251
+ tool_descriptions_str=tool_descriptions_string
252
  )
253
+ print(f"[{__name__}] generate_final_answer: State being returned (keys): {list(new_state.keys())}")
254
  return new_state
255
 
256
  # 7. Router: Decide which path to take after initial LLM call
257
  def route_action(state: AgentState) -> str:
258
  """Routes the graph based on the LLM's parsed action."""
259
+ print(f"[{__name__}] route_action: State received (keys): {list(state.keys())}")
260
+ parsed_action = state.get("parsed_action") # Use .get for robustness
261
+
262
+ if not parsed_action: # Handle cases where parsing might have failed
263
+ print(f"[{__name__}] Parsed action is empty or None. Defaulting to 'generate_final_answer'.")
264
+ return "generate_final_answer" # Route to final answer, likely an error explanation
265
 
266
+ action_type = parsed_action.get("action")
267
  if action_type == "tool":
268
  print(f"[{__name__}] Routing to 'execute_tool' based on LLM decision.")
269
  return "execute_tool"
270
  elif action_type == "final_answer":
271
  print(f"[{__name__}] Routing to 'generate_final_answer' directly based on LLM decision.")
272
+ return "generate_final_answer"
273
  else:
274
+ print(f"[{__name__}] Unknown action type '{action_type}'. Defaulting to 'generate_final_answer'.")
 
275
  return "generate_final_answer" # Route to final answer, might be an error case
276
 
277
  # 8. Build the agent graph
278
+ builder = StateGraph(AgentState)
279
 
280
  # Add nodes
281
  builder.add_node("initial_llm_decision", call_llm_decide)
282
  builder.add_node("execute_tool", call_tool_node)
283
+ builder.add_node("generate_final_answer", generate_final_answer)
284
 
285
  # Set the entry point
286
  builder.set_entry_point("initial_llm_decision")
 
307
  class BasicAgent:
308
  def __init__(self):
309
  self.agent = agent_executor
310
+
311
  def __call__(self, question: str) -> str:
312
  # The initial state for each new question
313
  initial_state: AgentState = { # Explicitly type the dictionary as AgentState
314
  "input": question,
315
  "chat_history": [],
316
+ "llm_response_raw": None, # Should be initialized as None
317
+ "parsed_action": None, # Should be initialized as None
318
+ "tool_output": None, # Should be initialized as None
319
+ "output": None, # Should be initialized as None
320
+ "tool_descriptions_str": "" # Initialize this new key
321
  }
322
+
323
  # The invoke will run through the graph
324
  # LangGraph's invoke returns the final state
325
+ final_state = self.agent.invoke(initial_state)
326
+
327
  # Extract the final output from the state
328
  return final_state.get("output", "No answer could be generated.")
329
 
 
331
  # Example Usage (for local testing)
332
  print("Testing BasicAgent locally...")
333
 
 
 
 
 
 
334
  try:
335
  agent = BasicAgent()
336
+
337
  print("\n--- Test 1: Simple question, should directly answer ---")
338
  response1 = agent("What is the capital of France?")
339
  print(f"Agent Response: {response1}")
340
+
341
  print("\n--- Test 2: Question requiring a tool (e.g., web_search) ---")
 
342
  # Make sure TAVILY_API_KEY is set in your environment for this to work
343
  response2 = agent("What is the current population of the United States? (as of today)")
344
  print(f"Agent Response: {response2}")
345
+
346
+ print("\n--- Test 3: Math question (e.g., calculator tool) ---")
347
  response3 = agent("What is 15 multiplied by 23?")
348
  print(f"Agent Response: {response3}")
349
+
350
  print("\n--- Test 4: Question requiring Python code execution ---")
351
  response4 = agent("What is the result of the python code: `sum([x**2 for x in range(1, 5)])`?")
352
  print(f"Agent Response: {response4}")
353
+
354
  print("\n--- Test 5: Question with no clear tool, but needs a general answer ---")
355
  response5 = agent("What is the meaning of life?")
356
  print(f"Agent Response: {response5}")
357
 
358
  except Exception as e:
359
  print(f"\nError during local testing: {e}")
360
+ print("Please ensure your GOOGLE_API_KEY and TAVILY_API_KEY are set, and 'tools.py' is correctly implemented and exports 'tools_for_llm'.")