GenericAgent / GeneralAgent.py
APRG's picture
Update GeneralAgent.py
45d767e verified
import os
import requests
import pandas as pd
from typing import Annotated
from typing_extensions import TypedDict
from langchain_google_genai import ChatGoogleGenerativeAI
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langchain_core.tools import tool
from langchain_core.messages import AIMessage, ToolMessage, HumanMessage, SystemMessage
from smolagents import DuckDuckGoSearchTool
import requests
from bs4 import BeautifulSoup
import wikipedia
import pandas as pd
# (Keep Constants as is)
# --- Constants ---
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"
# --- Basic Agent Definition ---
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
class OrderState(TypedDict):
"""State representing the customer's order conversation."""
messages: Annotated[list, add_messages]
order: list[str]
finished: bool
# System instruction for the Agent
SYSINT = (
"system",
"You are a general AI assistant. I will ask you a question."
"The question requires a tool to solve. You must attempt to use at least one of the available tools before returning an answer."
"Report your thoughts, and finish your answer with the following template: "
"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."
"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."
"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."
"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."
"If a tool required for task completion is not functioning, return 0."
)
WELCOME_MSG = "Welcome to my general-purpose AI agent. Type `q` to quit. How shall I fail to serve you today?"
@tool
def wikipedia_search_tool(title: str) -> str:
"""Provides an excerpt from a Wikipedia article with the given title."""
try:
page = wikipedia.page(title, auto_suggest=False)
return page.content[:3000]
except Exception as e:
return f"Error during processing: {e}"
@tool
def media_tool(file_path: str) -> str:
"""Used for deciphering video and audio files."""
return "This tool hasn't been implemented yet. Please return 0 if the task cannot be solved without knowing the contents of this file."
@tool
def internet_search_tool(search_query: str) -> str:
"""Does a google search with using the input as the search query. Returns a long batch of textual information related to the query."""
try:
search_tool = DuckDuckGoSearchTool()
result = search_tool(search_query)
return result
except Exception as e:
return f"Error during processing: {e}"
@tool
def webscraper_tool(url: str) -> str:
"""Returns the page's html content from the input url."""
try:
response = requests.get(url, stream=True)
if response.status_code == 200:
soup = BeautifulSoup(response.content, 'html.parser')
html_text = soup.get_text()
return html_text
else:
return f"Failed to retrieve the webpage. Status code: {response.status_code}"
except Exception as e:
return f"Error during processing: {e}"
@tool
def read_excel_tool(file_path: str) -> str:
"""Returns the contents of an Excel file as a Pandas dataframe."""
try:
df = pd.read_excel(file_path, engine = "openpyxl")
return df.to_string(index=False)
except Exception as e:
return f"Error during processing: {e}"
class AgenticAI:
def __init__(self):
# initialize LLM
self.llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash")
# prepare tool list
self.tools = [
wikipedia_search_tool,
media_tool,
internet_search_tool,
webscraper_tool,
read_excel_tool,
]
# bind tools
self.llm_with_tools = self.llm.bind_tools(self.tools)
# standalone ToolNode for any non-interactive tools (none here)
self.tool_node = ToolNode([])
# build state graph
self.graph = StateGraph(OrderState)
self.graph.add_node("agent", self._agent_node)
self.graph.add_node("interactive_tools", self._interactive_tools_node)
self.graph.add_node("human", self._human_node)
# routing
self.graph.add_conditional_edges("agent", self._maybe_route_to_tools)
self.graph.add_conditional_edges("human", self._maybe_exit_human_node)
self.graph.add_edge("interactive_tools", "agent")
self.graph.add_edge(START, "human")
self.chat_graph = self.graph.compile()
def ask(self, human_input: str) -> str:
"""
Take a single human input, run through the full agent+tool graph,
return the AI's reply, and discard any stored human/chat history.
"""
# build initial messages
init_msgs = [
SystemMessage(content=SYSINT),
HumanMessage(content=human_input)
]
state = {"messages": init_msgs, "order": [], "finished": False}
try:
final_state = self.chat_graph.invoke(state, {"recursion_limit": 15})
# last message should be from the AI
ai_msg = final_state["messages"][-1]
return ai_msg.content
except Exception as e:
return f"Error during processing: {e}"
# --- internal node functions (mirror your original code) ---
def _agent_node(self, state: OrderState) -> OrderState:
print(f"Messagelist sent to agent node: {[msg.content for msg in state.get('messages', [])]}")
defaults = {"order": [], "finished": False}
msgs = state.get("messages", [])
if not msgs:
# no prior messages: seed with system + empty AI message
return {**defaults, "messages": [SystemMessage(SYSINT), AIMessage(content="")]}
try:
# always ensure system prompt is first
msgs = [SystemMessage(SYSINT)] + msgs
new_output = self.llm_with_tools.invoke(msgs)
return {**defaults, "messages": [new_output]}
except Exception as e:
return {**defaults, "messages": [AIMessage(content=f"I'm having trouble: {e}")]}
def _interactive_tools_node(self, state: OrderState) -> OrderState:
tool_msg = state["messages"][-1]
outbound_msgs = []
for tool_call in tool_msg.tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call["args"]
if tool_name == "wikipedia_search_tool":
try:
print(f"called wikipedia with {str(tool_args)}")
page = wikipedia.page(tool_args.get("title"), auto_suggest=False)
response = page.content[:3000]
except Exception as e:
response = e
elif tool_name == "media_tool":
try:
print(f"called media with {str(tool_args)}")
response = "This tool hasn't been implemented yet. Please return 0 if the task cannot be solved without knowing the contents of this file."
except Exception as e:
response = e
elif tool_name == "internet_search_tool":
try:
print(f"called internet with {str(tool_args)}")
question = tool_args.get("search_query")
search_tool = DuckDuckGoSearchTool()
response = search_tool(question)[:3000]
except Exception as e:
response = e
elif tool_name == "webscraper_tool":
try:
print(f"called webscraper with {str(tool_args)}")
url = tool_args.get("url")
response = requests.get(url, stream=True)
if response.status_code == 200:
soup = BeautifulSoup(response.content, 'html.parser')
html_text = soup.get_text()
response = html_text
else:
response = f"Failed to retrieve the webpage. Status code: {response.status_code}"
except Exception as e:
response = e
elif tool_name == "read_excel_tool":
try:
print(f"called excel with {str(tool_args)}")
path = tool_args.get("file_path")
df = pd.read_excel(path, engine = "openpyxl")
response = df
except Exception as e:
response = e
else:
response = f'Unknown tool call: {tool_name}'
outbound_msgs.append(
ToolMessage(
content=response,
name=tool_name,
tool_call_id=tool_call["id"],
)
)
return {"messages": outbound_msgs, "order": state.get("order", []), "finished": False}
def _human_node(self, state: OrderState) -> OrderState:
print(f"Messagelist sent to human node: {[msg.content for msg in state.get('messages', [])]}")
last = state["messages"][-1]
if isinstance(last, HumanMessage) and last.content.strip().lower() in {"q", "quit", "exit", "goodbye"}:
state["finished"] = True
return state
def _maybe_route_to_tools(self, state: OrderState) -> str:
msgs = state.get("messages", [])
if state.get("finished"):
print("from agent GOTO End node")
return END
last = msgs[-1]
if hasattr(last, "tool_calls") and last.tool_calls:
print("from agent GOTO tools node")
# go run interactive tools
return "interactive_tools"
# else, end conversation
print("tool call failed, quitting")
return END
def _maybe_exit_human_node(self, state: OrderState) -> str:
if state.get("finished"):
print("from human GOTO End node")
return END
last = state["messages"][-1]
# if AIMessage then end after one turn
print("from human GOTO agent node or quit")
return END if isinstance(last, AIMessage) else "agent"