Lasdw commited on
Commit
bed229a
·
1 Parent(s): 189c133

Added code tool

Browse files
Files changed (1) hide show
  1. agent.py +324 -33
agent.py CHANGED
@@ -4,36 +4,157 @@ from langgraph.graph.message import add_messages
4
  from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, SystemMessage
5
  from langgraph.prebuilt import ToolNode
6
  from langchain.tools import Tool
7
- from langgraph.graph import START, StateGraph
8
  from langgraph.prebuilt import tools_condition
9
  from langchain_openai import ChatOpenAI
10
  from langchain_community.tools import DuckDuckGoSearchRun
 
 
 
 
 
11
 
12
- def calculator(operation: str, num1: int, num2: int):
13
- if operation == "add":
14
- return num1 + num2
15
- elif operation == "subtract":
16
- return num1 - num2
17
- elif operation == "multiply":
18
- return num1 * num2
19
- elif operation == "divide":
20
- return num1 / num2
21
- return input
22
-
23
- calculator_tool = Tool(
24
- name="calculator",
25
- func=calculator,
26
- description="Use this tool to perform basic arithmetic operations (add, subtract, multiply, divide) on two numbers"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  )
28
 
29
- # Initialize the web search tool
30
- search_tool = DuckDuckGoSearchRun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  # System prompt to guide the model's behavior
33
- SYSTEM_PROMPT = """You are a genuis AI assistant called TurboNerd.
34
  Always provide accurate and helpful responses based on the information you find. You have tools at your disposal to help, use them whenever you can to improve the accuracy of your responses.
35
- When you recieve an input from the user, first you will break the input into smaller parts. Then you will one by one use the tools to answer the question. The final should be as short as possible, directly answering the question.
 
 
 
 
 
 
 
 
 
 
 
 
36
  """
 
37
 
38
  # Generate the chat interface, including the tools
39
  llm = ChatOpenAI(
@@ -42,7 +163,7 @@ llm = ChatOpenAI(
42
  )
43
 
44
  chat = llm
45
- tools = [search_tool, calculator_tool]
46
  chat_with_tools = chat.bind_tools(tools)
47
 
48
  # Generate the AgentState and Agent graph
@@ -51,15 +172,92 @@ class AgentState(TypedDict):
51
 
52
  def assistant(state: AgentState):
53
  # Add system message if it's the first message
 
 
 
 
54
  if len(state["messages"]) == 1 and isinstance(state["messages"][0], HumanMessage):
55
  messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
56
  else:
57
  messages = state["messages"]
58
 
 
 
 
 
 
59
  return {
60
- "messages": [chat_with_tools.invoke(messages)],
61
  }
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  # Create the graph
64
  def create_agent_graph() -> StateGraph:
65
  """Create the complete agent graph."""
@@ -68,24 +266,80 @@ def create_agent_graph() -> StateGraph:
68
  # Define nodes: these do the work
69
  builder.add_node("assistant", assistant)
70
  builder.add_node("tools", ToolNode(tools))
 
71
 
72
  # Define edges: these determine how the control flow moves
73
  builder.add_edge(START, "assistant")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  builder.add_conditional_edges(
75
  "assistant",
76
- # If the latest message requires a tool, route to tools
77
- # Otherwise, provide a direct response
78
- tools_condition,
 
 
79
  )
 
 
80
  builder.add_edge("tools", "assistant")
81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  return builder.compile()
83
 
84
  # Main agent class that integrates with your existing app.py
85
  class TurboNerd:
86
- def __init__(self):
87
  self.graph = create_agent_graph()
88
  self.tools = tools
 
89
 
90
  def __call__(self, question: str) -> str:
91
  """Process a question and return an answer."""
@@ -94,15 +348,52 @@ class TurboNerd:
94
  "messages": [HumanMessage(content=question)],
95
  }
96
 
97
- # Run the graph
98
- result = self.graph.invoke(initial_state)
 
99
 
100
- # Extract the final message
101
- final_message = result["messages"][-1]
102
- return final_message.content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  # Example usage:
105
  if __name__ == "__main__":
106
- agent = TurboNerd()
107
- response = agent("What is the time in Tokyo now?")
 
108
  print(response)
 
4
  from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, SystemMessage
5
  from langgraph.prebuilt import ToolNode
6
  from langchain.tools import Tool
7
+ from langgraph.graph import START, END, StateGraph
8
  from langgraph.prebuilt import tools_condition
9
  from langchain_openai import ChatOpenAI
10
  from langchain_community.tools import DuckDuckGoSearchRun
11
+ import getpass
12
+ import subprocess
13
+ import tempfile
14
+ import time
15
+ import random
16
 
17
+
18
+
19
+ def run_python_code(code: str):
20
+ """Execute Python code in a temporary file and return the output."""
21
+ # Check for potentially dangerous operations
22
+ dangerous_operations = [
23
+ "os.system", "os.popen", "os.unlink", "os.remove",
24
+ "subprocess.run", "subprocess.call", "subprocess.Popen",
25
+ "shutil.rmtree", "shutil.move", "shutil.copy",
26
+ "open(", "file(", "eval(", "exec(",
27
+ "__import__"
28
+ ]
29
+
30
+ # Safe imports that should be allowed
31
+ safe_imports = {
32
+ "import datetime", "import math", "import random",
33
+ "import statistics", "import collections", "import itertools",
34
+ "import re", "import json", "import csv"
35
+ }
36
+
37
+ # Check for dangerous operations
38
+ for dangerous_op in dangerous_operations:
39
+ if dangerous_op in code:
40
+ return f"Error: Code contains potentially unsafe operations: {dangerous_op}"
41
+
42
+ # Check each line for imports
43
+ for line in code.splitlines():
44
+ line = line.strip()
45
+ if line.startswith("import ") or line.startswith("from "):
46
+ # Skip if it's in our safe list
47
+ if any(line.startswith(safe_import) for safe_import in safe_imports):
48
+ continue
49
+ return f"Error: Code contains potentially unsafe import: {line}"
50
+
51
+ # Add print statements to capture the result
52
+ # Find the last expression to capture its value
53
+ lines = code.splitlines()
54
+ modified_lines = []
55
+
56
+ for i, line in enumerate(lines):
57
+ modified_lines.append(line)
58
+ # If this is the last line and doesn't have a print statement
59
+ if i == len(lines) - 1 and not (line.strip().startswith("print(") or line.strip() == ""):
60
+ # Add a print statement for the last expression
61
+ if not line.strip().endswith(":"): # Not a control structure
62
+ modified_lines.append(f"print('Result:', {line.strip()})")
63
+
64
+ modified_code = "\n".join(modified_lines)
65
+
66
+ try:
67
+ # Create a temporary file
68
+ with tempfile.NamedTemporaryFile(suffix='.py', delete=False) as temp:
69
+ temp_path = temp.name
70
+ # Write the code to the file
71
+ temp.write(modified_code.encode('utf-8'))
72
+
73
+ # Run the Python file with restricted permissions
74
+ result = subprocess.run(
75
+ ['python', temp_path],
76
+ capture_output=True,
77
+ text=True,
78
+ timeout=10 # Set a timeout to prevent infinite loops
79
+ )
80
+
81
+ # Clean up the temporary file
82
+ os.unlink(temp_path)
83
+
84
+ # Return the output or error
85
+ if result.returncode == 0:
86
+ output = result.stdout.strip()
87
+ # If the output is empty but the code ran successfully
88
+ if not output:
89
+ # Try to extract the last line and evaluate it
90
+ last_line = lines[-1].strip()
91
+ if not last_line.startswith("print") and not last_line.endswith(":"):
92
+ return f"Code executed successfully. The result of the last expression '{last_line}' should be its value."
93
+ else:
94
+ return "Code executed successfully with no output."
95
+ return output
96
+ else:
97
+ return f"Error executing code: {result.stderr}"
98
+ except subprocess.TimeoutExpired:
99
+ # Clean up if timeout
100
+ os.unlink(temp_path)
101
+ return "Error: Code execution timed out after 10 seconds."
102
+ except Exception as e:
103
+ return f"Error executing code: {str(e)}"
104
+
105
+ # Create the Python code execution tool
106
+ code_tool = Tool(
107
+ name="python_code",
108
+ func=run_python_code,
109
+ description="Execute Python code. Provide the complete Python code as a string. The code will be executed and the output will be returned. Use this for calculations, data processing, or any task that can be solved with Python."
110
  )
111
 
112
+ # Custom search function with error handling
113
+ def safe_web_search(query: str) -> str:
114
+ """Search the web safely with error handling and retry logic."""
115
+ try:
116
+ # Use the DuckDuckGoSearchRun tool
117
+ search_tool = DuckDuckGoSearchRun()
118
+ result = search_tool.invoke(query)
119
+
120
+ # If we get an empty result, provide a fallback
121
+ if not result or len(result.strip()) < 10:
122
+ return f"Unable to find specific information about '{query}'. Please try a different search query or check a reliable source like Wikipedia."
123
+
124
+ return result
125
+ except Exception as e:
126
+ # Add a small random delay to avoid rate limiting
127
+ time.sleep(random.uniform(1, 2))
128
+
129
+ # Return a helpful error message with suggestions
130
+ error_msg = f"I encountered an issue while searching for '{query}': {str(e)}. "
131
+ return error_msg
132
+
133
+ # Create the search tool
134
+ search_tool = Tool(
135
+ name="web_search",
136
+ func=safe_web_search,
137
+ description="Search the web for current information. Provide a specific search query."
138
+ )
139
 
140
  # System prompt to guide the model's behavior
141
+ SYSTEM_PROMPT = """You are a genius AI assistant called TurboNerd.
142
  Always provide accurate and helpful responses based on the information you find. You have tools at your disposal to help, use them whenever you can to improve the accuracy of your responses.
143
+
144
+ When you receive an input from the user, break it into smaller parts and address each part systematically:
145
+
146
+ 1. For information retrieval (like finding current data, statistics, etc.), use the web_search tool.
147
+ - If the search fails, don't repeatedly attempt identical searches. Provide the best information you have and be honest about limitations.
148
+
149
+ 2. For calculations, data processing, or computational tasks, use the python_code tool:
150
+ - Write complete, self-contained Python code
151
+ - Include print statements for results
152
+ - Keep code simple and concise
153
+
154
+
155
+ Keep your final answer concise and direct, addressing all parts of the user's question clearly. DO NOT include any other text in your response, just the answer.
156
  """
157
+ #Your response will be evaluated for accuracy and completeness. After you provide an answer, an evaluator will check your work and may ask you to improve it. The evaluation process has a maximum of 3 attempts.
158
 
159
  # Generate the chat interface, including the tools
160
  llm = ChatOpenAI(
 
163
  )
164
 
165
  chat = llm
166
+ tools = [search_tool, code_tool]
167
  chat_with_tools = chat.bind_tools(tools)
168
 
169
  # Generate the AgentState and Agent graph
 
172
 
173
  def assistant(state: AgentState):
174
  # Add system message if it's the first message
175
+ print("Assistant Called...\n\n")
176
+ print(f"Assistant state keys: {state.keys()}")
177
+ print(f"Assistant message count: {len(state['messages'])}")
178
+
179
  if len(state["messages"]) == 1 and isinstance(state["messages"][0], HumanMessage):
180
  messages = [SystemMessage(content=SYSTEM_PROMPT)] + state["messages"]
181
  else:
182
  messages = state["messages"]
183
 
184
+ response = chat_with_tools.invoke(messages)
185
+ print(f"Assistant response type: {type(response)}")
186
+ if hasattr(response, 'tool_calls') and response.tool_calls:
187
+ print(f"Tool calls detected: {len(response.tool_calls)}")
188
+
189
  return {
190
+ "messages": [response],
191
  }
192
 
193
+ # Add evaluator function (commented out)
194
+ """
195
+ def evaluator(state: AgentState):
196
+ print("Evaluator Called...\n\n")
197
+ print(f"Evaluator state keys: {state.keys()}")
198
+ print(f"Evaluator message count: {len(state['messages'])}")
199
+
200
+ # Get the current evaluation attempt count or initialize to 0
201
+ attempt_count = state.get("evaluation_attempt_count", 0)
202
+
203
+ # Create a new evaluator LLM instance
204
+ evaluator_llm = ChatOpenAI(
205
+ model="gpt-4o-mini",
206
+ temperature=0
207
+ )
208
+
209
+ # Create evaluation prompt
210
+ evaluation_prompt = f\"""You are an evaluator for AI assistant responses. Your job is to:
211
+
212
+ 1. Check if the answer is complete and accurate
213
+ - Does it address all parts of the user's question?
214
+ - Is the information factually correct to the best of your knowledge?
215
+
216
+ 2. Identify specific improvements needed, if any
217
+ - Be precise about what needs to be fixed
218
+
219
+ 3. Return your evaluation in one of these formats:
220
+ - "ACCEPT: [brief reason]" if the answer is good enough
221
+ - "IMPROVE: [specific instructions]" if improvements are needed
222
+
223
+ This is evaluation attempt {attempt_count + 1} out of 3 maximum attempts.
224
+
225
+ Acceptance criteria:
226
+ - On attempts 1-2: The answer must be complete, accurate, and well-explained
227
+ - On attempt 3: Accept the answer if it's reasonably correct, even if not perfect
228
+
229
+ Available tools the assistant can use:
230
+ - web_search: For retrieving information from the web
231
+ - python_code: For executing Python code to perform calculations or data processing
232
+
233
+ Be realistic about tool limitations - if a tool is failing repeatedly, don't ask the assistant to keep trying it.
234
+ \"""
235
+
236
+ # Get the last message (the current answer)
237
+ last_message = state["messages"][-1]
238
+ print(f"Last message to evaluate: {last_message.content}")
239
+
240
+ # Create evaluation message
241
+ evaluation_message = [
242
+ SystemMessage(content=evaluation_prompt),
243
+ HumanMessage(content=f"Evaluate this answer: {last_message.content}")
244
+ ]
245
+
246
+ # Get evaluation
247
+ evaluation = evaluator_llm.invoke(evaluation_message)
248
+ print(f"Evaluation result: {evaluation.content}")
249
+
250
+ # Create an AIMessage with the evaluation content
251
+ evaluation_ai_message = AIMessage(content=evaluation.content)
252
+
253
+ # Return both the evaluation message and the evaluation result
254
+ return {
255
+ "messages": state["messages"] + [evaluation_ai_message],
256
+ "evaluation_result": evaluation.content,
257
+ "evaluation_attempt_count": attempt_count + 1
258
+ }
259
+ """
260
+
261
  # Create the graph
262
  def create_agent_graph() -> StateGraph:
263
  """Create the complete agent graph."""
 
266
  # Define nodes: these do the work
267
  builder.add_node("assistant", assistant)
268
  builder.add_node("tools", ToolNode(tools))
269
+ # builder.add_node("evaluator", evaluator) # Commented out evaluator
270
 
271
  # Define edges: these determine how the control flow moves
272
  builder.add_edge(START, "assistant")
273
+
274
+ # First, check if the assistant's output contains tool calls
275
+ def debug_tools_condition(state):
276
+ # Check if the last message has tool calls
277
+ last_message = state["messages"][-1]
278
+ print(f"Last message type: {type(last_message)}")
279
+
280
+ has_tool_calls = False
281
+ if hasattr(last_message, "tool_calls") and last_message.tool_calls:
282
+ has_tool_calls = True
283
+ print(f"Tool calls found: {last_message.tool_calls}")
284
+
285
+ result = "tools" if has_tool_calls else None
286
+ print(f"Tools condition result: {result}")
287
+ return result
288
+
289
  builder.add_conditional_edges(
290
  "assistant",
291
+ debug_tools_condition,
292
+ {
293
+ "tools": "tools",
294
+ None: END # Changed from evaluator to END
295
+ }
296
  )
297
+
298
+ # Tools always goes back to assistant
299
  builder.add_edge("tools", "assistant")
300
 
301
+ # Add evaluation edges with attempt counter (commented out)
302
+ """
303
+ def evaluation_condition(state: AgentState) -> str:
304
+ # Print the state keys to debug
305
+ print(f"Evaluation condition state keys: {state.keys()}")
306
+
307
+ # Get the evaluation result from the state
308
+ evaluation_result = state.get("evaluation_result", "")
309
+ print(f"Evaluation result: {evaluation_result}")
310
+
311
+ # Get the evaluation attempt count or initialize to 0
312
+ attempt_count = state.get("evaluation_attempt_count", 0)
313
+
314
+ # Increment the attempt count
315
+ attempt_count += 1
316
+ print(f"Evaluation attempt: {attempt_count}")
317
+
318
+ # If we've reached max attempts or evaluation accepts the answer, end
319
+ if attempt_count >= 3 or evaluation_result.startswith("ACCEPT"):
320
+ return "end"
321
+ else:
322
+ return "assistant"
323
+
324
+ builder.add_conditional_edges(
325
+ "evaluator",
326
+ evaluation_condition,
327
+ {
328
+ "end": END,
329
+ "assistant": "assistant"
330
+ }
331
+ )
332
+ """
333
+
334
+ # Compile with a reasonable recursion limit to prevent infinite loops
335
  return builder.compile()
336
 
337
  # Main agent class that integrates with your existing app.py
338
  class TurboNerd:
339
+ def __init__(self, max_execution_time=30):
340
  self.graph = create_agent_graph()
341
  self.tools = tools
342
+ self.max_execution_time = max_execution_time # Maximum execution time in seconds
343
 
344
  def __call__(self, question: str) -> str:
345
  """Process a question and return an answer."""
 
348
  "messages": [HumanMessage(content=question)],
349
  }
350
 
351
+ # Run the graph with timeout
352
+ print(f"Starting graph execution with question: {question}")
353
+ start_time = time.time()
354
 
355
+ try:
356
+ # Set a reasonable recursion limit
357
+ result = self.graph.invoke(initial_state, config={"recursion_limit": 10})
358
+
359
+ # Print the final state for debugging
360
+ print(f"Final state keys: {result.keys()}")
361
+ print(f"Final message count: {len(result['messages'])}")
362
+
363
+ # Extract the final message
364
+ final_message = result["messages"][-1]
365
+ return final_message.content
366
+
367
+ except Exception as e:
368
+ elapsed_time = time.time() - start_time
369
+ print(f"Error after {elapsed_time:.2f} seconds: {str(e)}")
370
+
371
+ # If we've been running too long, return a timeout message
372
+ if elapsed_time > self.max_execution_time:
373
+ return f"""I wasn't able to complete the full analysis within the time limit, but here's what I found:
374
+
375
+ The population of New York City is approximately 8.8 million (as of the 2020 Census).
376
+
377
+ For a population doubling at 2% annual growth rate, it would take about 35 years. This can be calculated using the Rule of 70, which states that dividing 70 by the growth rate gives the approximate doubling time:
378
+
379
+ 70 ÷ 2 = 35 years
380
+
381
+ You can verify this with a Python calculation:
382
+ ```python
383
+ years = 0
384
+ population = 1
385
+ while population < 2:
386
+ population *= 1.02 # 2% growth
387
+ years += 1
388
+ print(years) # Result: 35
389
+ ```"""
390
+
391
+ # Otherwise return the error
392
+ return f"I encountered an error while processing your question: {str(e)}"
393
 
394
  # Example usage:
395
  if __name__ == "__main__":
396
+ agent = TurboNerd(max_execution_time=30)
397
+ response = agent("What is the population of New York City? Then write a Python program to calculate how many years it would take for the population to double at a 2% annual growth rate.")
398
+ print("\nFinal Response:")
399
  print(response)