File size: 10,320 Bytes
95bd65f 149d5fe 95bd65f 149d5fe 95bd65f aa8ebe7 95bd65f 96b6f4c 95bd65f aa8ebe7 96b6f4c 95bd65f 1537cc7 95bd65f e496a7f 95bd65f 1bb8179 95bd65f 1bb8179 aa8ebe7 95bd65f 1bb8179 95bd65f aa8ebe7 95bd65f 1537cc7 95bd65f 1537cc7 6204978 96b6f4c d0c5a3e 1537cc7 1334f2d 5c305e9 6de36de 1537cc7 aa8ebe7 1537cc7 6de36de def9a35 6de36de 96b6f4c 95bd65f 975238c 23e5850 95bd65f 149d5fe 95bd65f 1edc197 4df88d3 95bd65f e496a7f 95bd65f 149d5fe 95bd65f 149d5fe 95bd65f 149d5fe 95bd65f 149d5fe 95bd65f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | import base64
import os
import io
import contextlib
import requests
from typing import TypedDict, Annotated
from langchain_core.messages import AnyMessage
from langgraph.graph import START, StateGraph, add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_community.tools import tool, DuckDuckGoSearchRun
from langchain_community.utilities import DuckDuckGoSearchAPIWrapper
from langchain_google_genai import ChatGoogleGenerativeAI
# from pathlib import Path
# import tempfile
from dotenv import load_dotenv
import time
import random
# constants
API_URL = "https://agents-course-unit4-scoring.hf.space"
QUESTIONS_URL = f"{API_URL}/questions"
FILES_URL = f"{API_URL}/files"
SUBMIT_URL = f"{API_URL}/submit"
load_dotenv()
class AgentState(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
# file_path: str | None
# task_id: str | None
# url: str | None
def build_gemini_llm():
if not os.environ.get("GOOGLE_API_KEY"):
raise ValueError("GOOGLE_API_KEY environment variable is not set.")
return ChatGoogleGenerativeAI(model = "gemini-3.7-flash", temperature = 0, max_output_tokens = 1025, include_thoughts=True)
# @tool
# def extract_text_from_image(img_path: str) -> str:
# """
# Describe the image and extract any text in it.
# Args:
# img_path (str): the path to the image file.
# """
# all_text = ""
# try:
# # Read image and encode as base64
# with open(img_path, "rb") as image_file:
# image_bytes = image_file.read()
# image_base64 = base64.b64encode(image_bytes).decode("utf-8")
# # Prepare the prompt including the base64 image data
# message = [
# HumanMessage(
# content=[
# {
# "type": "text",
# "text": (
# "Describe the image and extract any text in it."
# ),
# },
# {
# "type": "image_url",
# "image_url": {
# "url": f"data:image/png;base64,{image_base64}"
# },
# },
# ]
# )
# ]
# response = model.invoke(message)
# # Append extracted text
# all_text += response.text + "\n\n"
# return all_text.strip()
# except Exception as e:
# # A butler should handle errors gracefully
# error_msg = f"Error extracting text: {str(e)}"
# print(error_msg)
# return ""
# @tool
# def download_and_read_file(task_id: str) -> str:
# """
# Download and read the file attached to the GAIA task its contents.
# Always call this first if there is a file attached to a GAIA Task.
# Args:
# task_id (str): The ID of the GAIA task.
# Returns:
# str: The contents of the file as a string.
# """
# try:
# # Download the file from the GAIA API
# response = requests.get(f"{FILES_URL}/{task_id}", timeout = 10)
# response.raise_for_status()
# # Determine the file type and read its contents
# content_disposition = response.headers.get("content-disposition", "")
# content_type = response.headers.get("content-type", "")
# filename = None
# if "filename=" in content_disposition:
# filename = content_disposition.split("filename=")[1].strip('"')
# if not filename:
# filename = f"{task_id}.bin"
# ext = Path(filename).suffix.lower()
# if ext in(".txt", ".py", ".json", ".md", ".ymal", ".html", ".xml", ""):
# return response.text
# if ext == ".xlsx" or "xlsx" in content_type:
# import pandas as pd
# with tempfile.NamedTemporaryFile(suffix=".xlsx", delete=False) as file:
# file.write(response.content)
# temp_path = file.name
# read_file = pd.read_excel(temp_path)
# return read_file.to_string()
# if ext == ".csv" or "csv" in content_type:
# import pandas as pd
# with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as file:
# file.write(response.content)
# temp_path = file.name
# read_file = pd.read_csv(temp_path)
# return read_file.to_string()
# if ext == ".csv" or "csv" in content_type:
# import pandas as pd
# with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as file:
# file.write(response.content)
# temp_path = file.name
# read_file = pd.read_csv(temp_path)
# return read_file.to_string()
# if ext == ".jpg" or ext == ".jpeg" or ext == ".png" or "image" in content_type:
# from PIL import Image
# with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as file:
# file.write(response.content)
# temp_path = file.name
# return extract_text_from_image(temp_path)
# # Unsupported file type
# return (
# f"Unsupported file type: {content_type}. "
# "I downloaded the file successfully, but I don't know "
# "how to extract its contents."
# )
# except requests.RequestException as e:
# return f"Failed to download file: {e}"
# except Exception as e:
# return f"Failed to read file: {e}"
# except Exception as e:
# return f"error downloading or reading file: {str(e)}"
@tool
def wikipedia_search(query: str) -> str:
"""
Search Wikipedia for factual and encyclopedic information.
Use this tool FIRST for:
- people
- historical events
- countries and places
- musicians, artists, movies, books
- scientific concepts
- biographies
- general factual knowledge
Wikipedia is preferred for stable, well-known topics.
Args:
query: Keywords to search on Wikipedia.
"""
url = "https://en.wikipedia.org/w/api.php"
params = {
"action": "query",
"list": "search",
"srsearch": query,
"format": "json",
"srlimit": 3,
"utf8": 1,
}
headers = {
"User-Agent": "MyLangGraphAgent/1.0"
}
max_retries = 3
for attempt in range(max_retries):
try:
response = requests.get(
url,
params=params,
headers=headers,
timeout=10,
)
print(
f"Wikipedia status={response.status_code}, "
f"content-type={response.headers.get('content-type')}"
)
response.raise_for_status()
data = response.json()
results = data.get("query", {}).get("search", [])
if not results:
return f"No Wikipedia results found for '{query}'."
MAX_CONTENT_LENGTH = 3000
return "\n\n---\n\n".join(
f"Title: {item['title']}\n"
f"Snippet: {item.get('snippet', '')}"
for item in results
)
except requests.exceptions.RequestException as e:
print(
f"Wikipedia request failed "
f"attempt {attempt + 1}/{max_retries}: {e}"
)
except requests.exceptions.JSONDecodeError:
print(
f"Wikipedia returned non-JSON response. "
f"Status={response.status_code}"
)
print("Response preview:")
print(response.text[:500])
if attempt < max_retries - 1:
delay = 2 ** attempt + random.random()
print(f"Retrying in {delay:.2f}s")
time.sleep(delay)
return (
f"Wikipedia search temporarily failed for '{query}'. "
"Please use another search source."
)
# fix wikipedia engine builds invalid URL for region="wt-wt"(default) issue from duckducksearchrun
search_wrapper = DuckDuckGoSearchAPIWrapper(
region="us-en",
backend="duckduckgo",
)
search_ddgs = DuckDuckGoSearchRun(
api_wrapper=search_wrapper
)
@tool
def search_web(query: str) -> str:
"""
Search the public web for information not suitable for Wikipedia.
Use this tool for:
- recent news
- current events
- latest information
- official websites
- product information
- information that may have changed recently
Do NOT use this tool as the first choice for stable
encyclopedic information that can be found on Wikipedia.
Args:
query: Keywords to search, only keywords and spaces.
"""
try:
result = search_ddgs.invoke(query)
if not result:
return f"No web results found for: {query}"
print("ddgs result:")
print(result)
return result
except Exception as e:
print(f"DuckDuckGo search failed for {query}: {e}")
return (
f"Web search failed for query: {query}\n"
f"Error: {type(e).__name__}: {e}\n"
"Please try a different search query."
)
model = build_gemini_llm()
tools = [
search_web,
wikipedia_search
]
model_with_tools = model.bind_tools(tools)
def assistant(state: AgentState):
response = model_with_tools.invoke(state["messages"])
print("\n===== Thinking Section =====")
print(response.content[0].get("thinking").strip())
print("===============================\n")
print("\n===== ASSISTANT RESPONSE =====")
print("TYPE:", type(response))
print("CONTENT:", response.content)
print("TOOL CALLS:", response.tool_calls)
print("===============================\n")
return {
"messages": [response],
# "file_path": state["file_path"],
# "task_id": state["task_id"],
# "url": state["url"]
}
builder = StateGraph(AgentState)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "assistant")
builder.add_conditional_edges("assistant", tools_condition)
builder.add_edge("tools", "assistant")
graph = builder.compile()
|