APRG commited on
Commit
2a15979
·
verified ·
1 Parent(s): 25cdda8

Upload GeneralAgent.py

Browse files
Files changed (1) hide show
  1. GeneralAgent.py +223 -0
GeneralAgent.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import pandas as pd
4
+
5
+ from typing import Annotated
6
+ from typing_extensions import TypedDict
7
+ from langchain_google_genai import ChatGoogleGenerativeAI
8
+ from langgraph.graph import StateGraph, START, END
9
+ from langgraph.graph.message import add_messages
10
+ from langgraph.prebuilt import ToolNode
11
+ from langchain_core.tools import tool
12
+ from langchain_core.messages import AIMessage, ToolMessage, HumanMessage, SystemMessage
13
+
14
+ from smolagents import DuckDuckGoSearchTool
15
+ import requests
16
+ from bs4 import BeautifulSoup
17
+ import wikipedia
18
+ import pandas as pd
19
+
20
+ # (Keep Constants as is)
21
+ # --- Constants ---
22
+ DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
23
+
24
+ # --- Basic Agent Definition ---
25
+ # ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
26
+ class OrderState(TypedDict):
27
+ """State representing the customer's order conversation."""
28
+ messages: Annotated[list, add_messages]
29
+ order: list[str]
30
+ finished: bool
31
+
32
+ # System instruction for the Agent
33
+ SYSINT = (
34
+ "system",
35
+ "You are a general AI assistant. I will ask you a question."
36
+ "The question requires a tool to solve. You must attempt to use at least one of the available tools before returning an answer."
37
+ "Report your thoughts, and finish your answer with the following template: "
38
+ "FINAL ANSWER: [YOUR FINAL ANSWER]. YOUR FINAL ANSWER should be a number OR as few words as possible OR a comma separated list of numbers and/or strings."
39
+ "If you are asked for a number, don't use comma to write your number neither use units such as $ or percent sign unless specified otherwise."
40
+ "If you are asked for a string, don't use articles, neither abbreviations (e.g. for cities), and write the digits in plain text unless specified otherwise."
41
+ "If you are asked for a comma separated list, apply the above rules depending of whether the element to be put in the list is a number or a string."
42
+ "If a tool required for task completion is not functioning, return 0."
43
+ )
44
+
45
+ WELCOME_MSG = "Welcome to my general-purpose AI agent. Type `q` to quit. How shall I fail to serve you today?"
46
+
47
+ @tool
48
+ def wikipedia_search_tool(title: str) -> str:
49
+ """Provides an excerpt from a Wikipedia article with the given title."""
50
+ page = wikipedia.page(title, auto_suggest=False)
51
+ return page.content[:3000]
52
+
53
+ @tool
54
+ def media_tool(file_path: str) -> str:
55
+ """Used for deciphering video and audio files."""
56
+ return "This tool hasn't been implemented yet. Please return 0 if the task cannot be solved without knowing the contents of this file."
57
+
58
+ @tool
59
+ def internet_search_tool(search_query: str) -> str:
60
+ """Does a google search with using the input as the search query. Returns a long batch of textual information related to the query."""
61
+ search_tool = DuckDuckGoSearchTool()
62
+ result = search_tool(search_query)
63
+ return result
64
+
65
+ @tool
66
+ def webscraper_tool(url: str) -> str:
67
+ """Returns the page's html content from the input url."""
68
+ response = requests.get(url, stream=True)
69
+ if response.status_code == 200:
70
+ soup = BeautifulSoup(response.content, 'html.parser')
71
+ html_text = soup.get_text()
72
+ return html_text
73
+ else:
74
+ raise Exception(f"Failed to retrieve the webpage. Status code: {response.status_code}")
75
+
76
+ @tool
77
+ def read_excel_tool(file_path: str) -> str:
78
+ """Returns the contents of an Excel file as a Pandas dataframe."""
79
+ df = pd.read_excel(file_path, engine = "openpyxl")
80
+ return df.to_string(index=False)
81
+
82
+ class AgenticAI:
83
+ def __init__(self):
84
+ # initialize LLM
85
+ self.llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
86
+ # prepare tool list
87
+ self.tools = [
88
+ wikipedia_search_tool,
89
+ media_tool,
90
+ internet_search_tool,
91
+ webscraper_tool,
92
+ read_excel_tool,
93
+ ]
94
+ # bind tools
95
+ self.llm_with_tools = self.llm.bind_tools(self.tools)
96
+ # standalone ToolNode for any non-interactive tools (none here)
97
+ self.tool_node = ToolNode([])
98
+ # build state graph
99
+ self.graph = StateGraph(OrderState)
100
+ self.graph.add_node("agent", self._agent_node)
101
+ self.graph.add_node("interactive_tools", self._interactive_tools_node)
102
+ self.graph.add_node("human", self._human_node)
103
+ # routing
104
+ self.graph.add_conditional_edges("agent", self._maybe_route_to_tools)
105
+ self.graph.add_conditional_edges("human", self._maybe_exit_human_node)
106
+ self.graph.add_edge("interactive_tools", "agent")
107
+ self.graph.add_edge(START, "human")
108
+ self.chat_graph = self.graph.compile()
109
+
110
+ def ask(self, human_input: str) -> str:
111
+ """
112
+ Take a single human input, run through the full agent+tool graph,
113
+ return the AI's reply, and discard any stored human/chat history.
114
+ """
115
+ # build initial messages
116
+ init_msgs = [
117
+ SystemMessage(content=SYSINT),
118
+ HumanMessage(content=human_input)
119
+ ]
120
+ state = {"messages": init_msgs, "order": [], "finished": False}
121
+ try:
122
+ final_state = self.chat_graph.invoke(state, {"recursion_limit": 10})
123
+ # last message should be from the AI
124
+ ai_msg = final_state["messages"][-1]
125
+ return ai_msg.content
126
+ except Exception as e:
127
+ return f"Error during processing: {e}"
128
+
129
+ # --- internal node functions (mirror your original code) ---
130
+ def _agent_node(self, state: OrderState) -> OrderState:
131
+ print(f"Messagelist sent to agent node: {[msg.content for msg in state.get('messages', [])]}")
132
+ defaults = {"order": [], "finished": False}
133
+ msgs = state.get("messages", [])
134
+ if not msgs:
135
+ # no prior messages: seed with system + empty AI message
136
+ return {**defaults, "messages": [SystemMessage(SYSINT), AIMessage(content="")]}
137
+
138
+ try:
139
+ # always ensure system prompt is first
140
+ msgs = [SystemMessage(SYSINT)] + msgs
141
+ new_output = self.llm_with_tools.invoke(msgs)
142
+ return {**defaults, "messages": [new_output]}
143
+ except Exception as e:
144
+ return {**defaults, "messages": [AIMessage(content=f"I'm having trouble: {e}")]}
145
+
146
+ def _interactive_tools_node(self, state: OrderState) -> OrderState:
147
+ tool_msg = state["messages"][-1]
148
+ outbound_msgs = []
149
+ for tool_call in tool_msg.tool_calls:
150
+ tool_name = tool_call["name"]
151
+ tool_args = tool_call["args"]
152
+
153
+ if tool_name == "wikipedia_search_tool":
154
+ print(f"called wikipedia with {str(tool_args)}")
155
+ page = wikipedia.page(tool_args.get("title"), auto_suggest=False)
156
+ response = page.content[:3000]
157
+ elif tool_name == "media_tool":
158
+ print(f"called media with {str(tool_args)}")
159
+ response = "This tool hasn't been implemented yet. Please return 0 if the task cannot be solved without knowing the contents of this file."
160
+ elif tool_name == "internet_search_tool":
161
+ print(f"called internet with {str(tool_args)}")
162
+ question = tool_args.get("search_query")
163
+ search_tool = DuckDuckGoSearchTool()
164
+ response = search_tool(question)[:3000]
165
+ elif tool_name == "webscraper_tool":
166
+ print(f"called webscraper with {str(tool_args)}")
167
+ url = tool_args.get("url")
168
+ response = requests.get(url, stream=True)
169
+ if response.status_code == 200:
170
+ soup = BeautifulSoup(response.content, 'html.parser')
171
+ html_text = soup.get_text()
172
+ response = html_text
173
+ else:
174
+ response = Exception(f"Failed to retrieve the webpage. Status code: {response.status_code}")
175
+ elif tool_name == "read_excel_tool":
176
+ print(f"called excel with {str(tool_args)}")
177
+ path = tool_args.get("file_path")
178
+ df = pd.read_excel(path, engine = "openpyxl")
179
+ response = df
180
+
181
+ else:
182
+ raise NotImplementedError(f'Unknown tool call: {tool_name}')
183
+
184
+ outbound_msgs.append(
185
+ ToolMessage(
186
+ content=response,
187
+ name=tool_name,
188
+ tool_call_id=tool_call["id"],
189
+ )
190
+ )
191
+
192
+ return {"messages": outbound_msgs, "order": state.get("order", []), "finished": False}
193
+
194
+ def _human_node(self, state: OrderState) -> OrderState:
195
+ print(f"Messagelist sent to human node: {[msg.content for msg in state.get('messages', [])]}")
196
+ last = state["messages"][-1]
197
+ if isinstance(last, HumanMessage) and last.content.strip().lower() in {"q", "quit", "exit", "goodbye"}:
198
+ state["finished"] = True
199
+ return state
200
+
201
+ def _maybe_route_to_tools(self, state: OrderState) -> str:
202
+ msgs = state.get("messages", [])
203
+ if state.get("finished"):
204
+ print("from agent GOTO End node")
205
+ return END
206
+
207
+ last = msgs[-1]
208
+ if hasattr(last, "tool_calls") and last.tool_calls:
209
+ print("from agent GOTO tools node")
210
+ # go run interactive tools
211
+ return "interactive_tools"
212
+ # else, end conversation
213
+ print("tool call failed, quitting")
214
+ return END
215
+
216
+ def _maybe_exit_human_node(self, state: OrderState) -> str:
217
+ if state.get("finished"):
218
+ print("from human GOTO End node")
219
+ return END
220
+ last = state["messages"][-1]
221
+ # if AIMessage then end after one turn
222
+ print("from human GOTO agent node or quit")
223
+ return END if isinstance(last, AIMessage) else "agent"