Tomkuijpers2232 commited on
Commit
cff4a0e
·
verified ·
1 Parent(s): d324c68

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +306 -268
agent.py CHANGED
@@ -1,6 +1,6 @@
1
  import os
2
  from dotenv import load_dotenv
3
- from typing import List, Dict, Any, Optional
4
  from langgraph.graph import START, StateGraph, MessagesState
5
  from langgraph.graph.message import add_messages
6
  from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, SystemMessage
@@ -15,77 +15,152 @@ from langchain_google_genai import ChatGoogleGenerativeAI
15
  from langchain_tavily import TavilySearch
16
  import tempfile
17
  import pandas as pd
 
 
 
 
 
 
 
18
 
19
  load_dotenv()
20
 
21
- # ReAct System Prompt
22
- REACT_SYSTEM_PROMPT = """You are a research assistant that uses ReAct (Reasoning + Acting) methodology. For each question, follow this systematic approach:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- **THINK**: First, analyze the question carefully. What type of information do you need? What tools might help?
 
 
25
 
26
- **ACT**: Use available tools to gather information. Search thoroughly and verify facts from multiple sources when possible.
 
 
 
 
27
 
28
- **OBSERVE**: Analyze the results from your tools. Are they complete and reliable? Do you need more information?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- **REASON**: Synthesize all information gathered. Check for consistency and identify any gaps or uncertainties.
 
 
 
31
 
32
- **VERIFY**: Before providing your final answer, double-check your reasoning and ensure you have sufficient evidence.
 
 
 
 
 
33
 
34
- For each question:
35
- 1. Break down what you're looking for
36
- 2. Use tools systematically to gather comprehensive information
37
- 3. Cross-reference information when possible
38
- 4. Be honest about limitations - if you cannot find reliable information, say so
39
- 5. Only provide confident answers when you have verified evidence
 
 
40
 
41
- When you cannot access certain content (videos, audio, images without tools), clearly state this limitation.
42
 
43
- Always finish with: FINAL ANSWER: [YOUR FINAL ANSWER]
 
 
 
 
44
 
45
- Your final answer should be:
46
- - A number (without commas or units unless specified)
47
- - As few words as possible for strings (no articles, no abbreviations for cities, spell out digits)
48
- - A comma-separated list following the above rules for each element
 
49
 
50
- Be thorough in your research but honest about uncertainty. Quality and accuracy are more important than speed.
51
  """
52
 
 
 
 
53
  @tool
54
- def multiply(a:int, b:int) -> int:
55
- """
56
- Multiply two numbers
57
- """
58
  return a * b
59
 
60
  @tool
61
- def add(a:int, b:int) -> int:
62
- """
63
- Add two numbers
64
- """
65
  return a + b
66
 
67
  @tool
68
- def subtract(a:int, b:int) -> int:
69
- """
70
- Subtract two numbers
71
- """
72
  return a - b
73
 
74
  @tool
75
- def divide(a:int, b:int) -> int:
76
- """
77
- Divide two numbers
78
- """
79
  return a / b
80
 
 
81
  @tool
82
  def wikidata_search(query: str) -> str:
83
- """
84
- Search for information on Wikipedia and return maximum 2 results.
85
-
86
- Args:
87
- query: The search query.
88
- """
89
  loader = WikipediaLoader(query=query, load_max_docs=2)
90
  docs = loader.load()
91
  formatted_search_docs = "\n\n---\n\n".join(
@@ -95,23 +170,14 @@ def wikidata_search(query: str) -> str:
95
  ])
96
  return {"wiki_results": formatted_search_docs}
97
 
98
- # Initialize Tavily Search Tool
99
- tavily_search_tool = TavilySearch(
100
- max_results=3,
101
- topic="general",
102
- )
103
-
104
- # Initialize YouTube Search Tool
105
  youtube_search_tool = YouTubeSearchTool()
106
 
 
107
  @tool
108
  def save_and_read_file(content: str, filename: Optional[str] = None) -> str:
109
- """
110
- Save content to a file and return the path.
111
- Args:
112
- content (str): the content to save to the file
113
- filename (str, optional): the name of the file. If not provided, a random name file will be created.
114
- """
115
  temp_dir = tempfile.gettempdir()
116
  if filename is None:
117
  temp_file = tempfile.NamedTemporaryFile(delete=False, dir=temp_dir)
@@ -124,32 +190,22 @@ def save_and_read_file(content: str, filename: Optional[str] = None) -> str:
124
 
125
  return f"File saved to {filepath}. You can read this file to process its contents."
126
 
127
-
128
  @tool
129
  def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
130
- """
131
- Download a file from a URL and save it to a temporary location.
132
- Args:
133
- url (str): the URL of the file to download.
134
- filename (str, optional): the name of the file. If not provided, a random name file will be created.
135
- """
136
  try:
137
- # Parse URL to get filename if not provided
138
  if not filename:
139
  path = urlparse(url).path
140
  filename = os.path.basename(path)
141
  if not filename:
142
  filename = f"downloaded_{uuid.uuid4().hex[:8]}"
143
 
144
- # Create temporary file
145
  temp_dir = tempfile.gettempdir()
146
  filepath = os.path.join(temp_dir, filename)
147
 
148
- # Download the file
149
  response = requests.get(url, stream=True)
150
  response.raise_for_status()
151
 
152
- # Save the file
153
  with open(filepath, "wb") as f:
154
  for chunk in response.iter_content(chunk_size=8192):
155
  f.write(chunk)
@@ -158,100 +214,75 @@ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
158
  except Exception as e:
159
  return f"Error downloading file: {str(e)}"
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
 
162
  @tool
163
  def extract_text_from_image(image_path: str) -> str:
164
- """
165
- Extract text from an image using OCR library pytesseract (if available).
166
- Args:
167
- image_path (str): the path to the image file.
168
- """
169
  try:
170
- # Open the image
171
  image = Image.open(image_path)
172
-
173
- # Extract text from the image
174
  text = pytesseract.image_to_string(image)
175
-
176
  return f"Extracted text from image:\n\n{text}"
177
  except Exception as e:
178
  return f"Error extracting text from image: {str(e)}"
179
 
180
-
181
  @tool
182
  def analyze_csv_file(file_path: str, query: str) -> str:
183
- """
184
- Analyze a CSV file using pandas and answer a question about it.
185
- Args:
186
- file_path (str): the path to the CSV file.
187
- query (str): Question about the data
188
- """
189
  try:
190
- # Read the CSV file
191
  df = pd.read_csv(file_path)
192
-
193
- # Run various analyses based on the query
194
  result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
195
  result += f"Columns: {', '.join(df.columns)}\n\n"
196
-
197
- # Add summary statistics
198
  result += "Summary statistics:\n"
199
  result += str(df.describe())
200
-
201
  return result
202
-
203
  except Exception as e:
204
  return f"Error analyzing CSV file: {str(e)}"
205
 
206
-
207
  @tool
208
  def analyze_excel_file(file_path: str, query: str) -> str:
209
- """
210
- Analyze an Excel file using pandas and answer a question about it.
211
- Args:
212
- file_path (str): the path to the Excel file.
213
- query (str): Question about the data
214
- """
215
  try:
216
- # Read the Excel file
217
  df = pd.read_excel(file_path)
218
-
219
- # Run various analyses based on the query
220
- result = (
221
- f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
222
- )
223
  result += f"Columns: {', '.join(df.columns)}\n\n"
224
-
225
- # Add summary statistics
226
  result += "Summary statistics:\n"
227
  result += str(df.describe())
228
-
229
  return result
230
-
231
  except Exception as e:
232
  return f"Error analyzing Excel file: {str(e)}"
233
 
234
-
235
- ### ============== IMAGE PROCESSING AND GENERATION TOOLS =============== ###
236
- import os
237
- import io
238
- import base64
239
- import uuid
240
- from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter
241
-
242
- # Helper functions for image processing
243
  def encode_image(image_path: str) -> str:
244
  """Convert an image file to base64 string."""
245
  with open(image_path, "rb") as image_file:
246
  return base64.b64encode(image_file.read()).decode("utf-8")
247
 
248
-
249
  def decode_image(base64_string: str) -> Image.Image:
250
  """Convert a base64 string to a PIL Image."""
251
  image_data = base64.b64decode(base64_string)
252
  return Image.open(io.BytesIO(image_data))
253
 
254
-
255
  def save_image(image: Image.Image, directory: str = "image_outputs") -> str:
256
  """Save a PIL Image to disk and return the path."""
257
  os.makedirs(directory, exist_ok=True)
@@ -262,13 +293,7 @@ def save_image(image: Image.Image, directory: str = "image_outputs") -> str:
262
 
263
  @tool
264
  def analyze_image(image_base64: str) -> Dict[str, Any]:
265
- """
266
- Analyze basic properties of an image (size, mode, color analysis, thumbnail preview).
267
- Args:
268
- image_base64 (str): Base64 encoded image string
269
- Returns:
270
- Dictionary with analysis result
271
- """
272
  try:
273
  img = decode_image(image_base64)
274
  width, height = img.size
@@ -301,42 +326,29 @@ def analyze_image(image_base64: str) -> Dict[str, Any]:
301
  except Exception as e:
302
  return {"error": str(e)}
303
 
304
-
305
  @tool
306
  def transform_image(
307
  image_base64: str, operation: str, params: Optional[Dict[str, Any]] = None
308
  ) -> Dict[str, Any]:
309
- """
310
- Apply transformations: resize, rotate, crop, flip, brightness, contrast, blur, sharpen, grayscale.
311
- Args:
312
- image_base64 (str): Base64 encoded input image
313
- operation (str): Transformation operation
314
- params (Dict[str, Any], optional): Parameters for the operation
315
- Returns:
316
- Dictionary with transformed image (base64)
317
- """
318
  try:
319
  img = decode_image(image_base64)
320
  params = params or {}
321
 
322
  if operation == "resize":
323
- img = img.resize(
324
- (
325
- params.get("width", img.width // 2),
326
- params.get("height", img.height // 2),
327
- )
328
- )
329
  elif operation == "rotate":
330
  img = img.rotate(params.get("angle", 90), expand=True)
331
  elif operation == "crop":
332
- img = img.crop(
333
- (
334
- params.get("left", 0),
335
- params.get("top", 0),
336
- params.get("right", img.width),
337
- params.get("bottom", img.height),
338
- )
339
- )
340
  elif operation == "flip":
341
  if params.get("direction", "horizontal") == "horizontal":
342
  img = img.transpose(Image.FLIP_LEFT_RIGHT)
@@ -362,20 +374,11 @@ def transform_image(
362
  except Exception as e:
363
  return {"error": str(e)}
364
 
365
-
366
  @tool
367
  def draw_on_image(
368
  image_base64: str, drawing_type: str, params: Dict[str, Any]
369
  ) -> Dict[str, Any]:
370
- """
371
- Draw shapes (rectangle, circle, line) or text onto an image.
372
- Args:
373
- image_base64 (str): Base64 encoded input image
374
- drawing_type (str): Drawing type
375
- params (Dict[str, Any]): Drawing parameters
376
- Returns:
377
- Dictionary with result image (base64)
378
- """
379
  try:
380
  img = decode_image(image_base64)
381
  draw = ImageDraw.Draw(img)
@@ -395,16 +398,12 @@ def draw_on_image(
395
  width=params.get("width", 2),
396
  )
397
  elif drawing_type == "line":
398
- draw.line(
399
- (
400
- params["start_x"],
401
- params["start_y"],
402
- params["end_x"],
403
- params["end_y"],
404
- ),
405
- fill=color,
406
- width=params.get("width", 2),
407
- )
408
  elif drawing_type == "text":
409
  font_size = params.get("font_size", 20)
410
  try:
@@ -427,7 +426,6 @@ def draw_on_image(
427
  except Exception as e:
428
  return {"error": str(e)}
429
 
430
-
431
  @tool
432
  def generate_simple_image(
433
  image_type: str,
@@ -435,15 +433,7 @@ def generate_simple_image(
435
  height: int = 500,
436
  params: Optional[Dict[str, Any]] = None,
437
  ) -> Dict[str, Any]:
438
- """
439
- Generate a simple image (gradient, noise, pattern, chart).
440
- Args:
441
- image_type (str): Type of image
442
- width (int), height (int)
443
- params (Dict[str, Any], optional): Specific parameters
444
- Returns:
445
- Dictionary with generated image (base64)
446
- """
447
  try:
448
  params = params or {}
449
 
@@ -457,33 +447,20 @@ def generate_simple_image(
457
 
458
  if direction == "horizontal":
459
  for x in range(width):
460
- r = int(
461
- start_color[0] + (end_color[0] - start_color[0]) * x / width
462
- )
463
- g = int(
464
- start_color[1] + (end_color[1] - start_color[1]) * x / width
465
- )
466
- b = int(
467
- start_color[2] + (end_color[2] - start_color[2]) * x / width
468
- )
469
  draw.line([(x, 0), (x, height)], fill=(r, g, b))
470
  else:
471
  for y in range(height):
472
- r = int(
473
- start_color[0] + (end_color[0] - start_color[0]) * y / height
474
- )
475
- g = int(
476
- start_color[1] + (end_color[1] - start_color[1]) * y / height
477
- )
478
- b = int(
479
- start_color[2] + (end_color[2] - start_color[2]) * y / height
480
- )
481
  draw.line([(0, y), (width, y)], fill=(r, g, b))
482
 
483
  elif image_type == "noise":
484
  noise_array = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8)
485
  img = Image.fromarray(noise_array, "RGB")
486
-
487
  else:
488
  return {"error": f"Unsupported image_type {image_type}"}
489
 
@@ -494,20 +471,11 @@ def generate_simple_image(
494
  except Exception as e:
495
  return {"error": str(e)}
496
 
497
-
498
  @tool
499
  def combine_images(
500
  images_base64: List[str], operation: str, params: Optional[Dict[str, Any]] = None
501
  ) -> Dict[str, Any]:
502
- """
503
- Combine multiple images (collage, stack, blend).
504
- Args:
505
- images_base64 (List[str]): List of base64 images
506
- operation (str): Combination type
507
- params (Dict[str, Any], optional)
508
- Returns:
509
- Dictionary with combined image (base64)
510
- """
511
  try:
512
  images = [decode_image(b64) for b64 in images_base64]
513
  params = params or {}
@@ -540,87 +508,157 @@ def combine_images(
540
  except Exception as e:
541
  return {"error": str(e)}
542
 
543
-
544
- @tool
545
- def download_task_file(task_id: str, api_url: str = "https://agents-course-unit4-scoring.hf.space") -> str:
546
- """
547
- Download a file associated with a task from the evaluation API.
548
- Args:
549
- task_id (str): The task ID to download the file for
550
- api_url (str): The base API URL (defaults to the evaluation server)
551
- """
552
- try:
553
- # Construct the file download URL
554
- file_url = f"{api_url}/files/{task_id}"
555
 
556
- # Create temporary file
557
- temp_dir = tempfile.gettempdir()
558
- filename = f"task_{task_id}.png" # Most files are images
559
- filepath = os.path.join(temp_dir, filename)
 
 
560
 
561
- # Download the file
562
- response = requests.get(file_url, stream=True)
563
- response.raise_for_status()
564
 
565
- # Save the file
566
- with open(filepath, "wb") as f:
567
- for chunk in response.iter_content(chunk_size=8192):
568
- f.write(chunk)
569
 
570
- return f"Task file downloaded to {filepath}. You can now analyze this file."
571
- except Exception as e:
572
- return f"Error downloading task file: {str(e)}"
573
-
 
 
 
 
 
574
 
575
- tools = [multiply, add, subtract, divide, wikidata_search, tavily_search_tool, youtube_search_tool, combine_images, analyze_image, transform_image, draw_on_image, generate_simple_image, analyze_csv_file, analyze_excel_file, save_and_read_file, download_file_from_url, extract_text_from_image, download_task_file]
 
 
 
 
 
576
 
577
- def build_graph():
578
- llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", api_key=os.getenv("GOOGLE_API_KEY"))
579
- llm_with_tools = llm.bind_tools(tools)
580
 
581
- def agent_node(state: MessagesState) -> MessagesState:
582
- """This is the agent node with ReAct methodology"""
583
- messages = state["messages"]
 
 
 
 
 
584
 
585
- # Add system prompt if not already present
586
- if not messages or not isinstance(messages[0], SystemMessage):
587
- messages = [SystemMessage(content=REACT_SYSTEM_PROMPT)] + messages
588
-
589
- return {"messages": [llm_with_tools.invoke(messages)]}
590
-
591
-
592
-
593
- builder = StateGraph(MessagesState)
594
- builder.add_node("agent", agent_node)
595
- builder.add_node("tools", ToolNode(tools))
596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
 
598
- builder.add_edge(START, "agent")
599
- builder.add_conditional_edges("agent", tools_condition)
600
- builder.add_edge("tools", "agent")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
 
602
- return builder.compile()
603
 
604
  class LangGraphAgent:
605
  def __init__(self):
606
- self.graph = build_graph()
607
- print("LangGraphAgent initialized with tools.")
608
 
609
  def __call__(self, question: str) -> str:
610
- """Run the agent on a question and return the answer"""
611
- try:
612
- messages = [HumanMessage(content=question)]
613
- result = self.graph.invoke({"messages": messages})
614
- for m in result["messages"]:
615
- m.pretty_print()
616
- return result["messages"][-1].content
617
- except Exception as e:
618
- return f"Error: {str(e)}"
619
 
620
  if __name__ == "__main__":
621
  agent = LangGraphAgent()
622
  question = "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia."
623
  answer = agent(question)
 
624
 
625
 
626
 
 
1
  import os
2
  from dotenv import load_dotenv
3
+ from typing import List, Dict, Any, Optional, Literal
4
  from langgraph.graph import START, StateGraph, MessagesState
5
  from langgraph.graph.message import add_messages
6
  from langchain_core.messages import AnyMessage, HumanMessage, AIMessage, SystemMessage
 
15
  from langchain_tavily import TavilySearch
16
  import tempfile
17
  import pandas as pd
18
+ import numpy as np
19
+ import requests
20
+ from urllib.parse import urlparse
21
+ import uuid
22
+ from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter
23
+ import base64
24
+ import io
25
 
26
  load_dotenv()
27
 
28
+ # ============== SYSTEM PROMPTS FOR SPECIALIZED AGENTS ============== #
29
+
30
+ COORDINATOR_SYSTEM_PROMPT = """You are a Coordinator Agent that orchestrates multiple specialized agents to solve complex tasks.
31
+
32
+ Your role is to:
33
+ 1. Analyze incoming requests and determine which specialized agents are needed
34
+ 2. Break down complex tasks into subtasks for different agents
35
+ 3. Coordinate between agents when needed
36
+ 4. Synthesize final answers from multiple agent responses
37
+
38
+ Available specialized agents:
39
+ - Research Agent: Wikipedia, web search, YouTube search
40
+ - Math Agent: Basic mathematical calculations
41
+ - Data Analysis Agent: CSV/Excel analysis, OCR text extraction
42
+ - Image Processing Agent: Image analysis, transformation, generation
43
+ - File Management Agent: File operations, downloads, saves
44
+
45
+ When you receive a task:
46
+ 1. THINK: What type of task is this? Which agents do I need?
47
+ 2. ROUTE: Send subtasks to appropriate agents
48
+ 3. COORDINATE: Manage dependencies between agent tasks
49
+ 4. SYNTHESIZE: Combine results into a final answer
50
+
51
+ Always provide a clear, comprehensive final answer.
52
+ """
53
+
54
+ RESEARCH_AGENT_PROMPT = """You are a Research Agent specialized in information gathering and search.
55
+
56
+ Your expertise includes:
57
+ - Wikipedia searches for encyclopedic information
58
+ - Web searches for current information and facts
59
+ - YouTube searches for video content
60
+
61
+ Follow ReAct methodology:
62
+ 1. THINK: What information do I need to find?
63
+ 2. ACT: Use appropriate search tools systematically
64
+ 3. OBSERVE: Analyze and verify search results
65
+ 4. SYNTHESIZE: Provide comprehensive, accurate information
66
+
67
+ Be thorough in your research and cross-reference sources when possible.
68
+ """
69
+
70
+ MATH_AGENT_PROMPT = """You are a Math Agent specialized in mathematical calculations and operations.
71
 
72
+ Your expertise includes:
73
+ - Basic arithmetic operations (add, subtract, multiply, divide)
74
+ - Mathematical reasoning and problem-solving
75
 
76
+ Follow ReAct methodology:
77
+ 1. THINK: What calculations are needed?
78
+ 2. ACT: Perform calculations systematically
79
+ 3. VERIFY: Double-check your work
80
+ 4. PROVIDE: Clear numerical answers
81
 
82
+ Always show your work and verify calculations.
83
+ """
84
+
85
+ DATA_ANALYSIS_AGENT_PROMPT = """You are a Data Analysis Agent specialized in processing and analyzing structured data.
86
+
87
+ Your expertise includes:
88
+ - CSV file analysis and statistics
89
+ - Excel file processing
90
+ - OCR text extraction from images
91
+ - Data interpretation and insights
92
+
93
+ Follow ReAct methodology:
94
+ 1. THINK: What type of data analysis is needed?
95
+ 2. ACT: Use appropriate analysis tools
96
+ 3. OBSERVE: Examine data patterns and statistics
97
+ 4. INTERPRET: Provide meaningful insights
98
 
99
+ Focus on accuracy and provide clear data-driven insights.
100
+ """
101
+
102
+ IMAGE_PROCESSING_AGENT_PROMPT = """You are an Image Processing Agent specialized in image analysis, manipulation, and generation.
103
 
104
+ Your expertise includes:
105
+ - Image analysis (properties, colors, content)
106
+ - Image transformations (resize, rotate, crop, filters)
107
+ - Drawing and annotation on images
108
+ - Simple image generation
109
+ - Combining multiple images
110
 
111
+ Follow ReAct methodology:
112
+ 1. THINK: What image processing is required?
113
+ 2. ACT: Apply appropriate image operations
114
+ 3. OBSERVE: Verify results and quality
115
+ 4. DELIVER: Provide processed images with explanations
116
+
117
+ Focus on quality and user requirements.
118
+ """
119
 
120
+ FILE_MANAGEMENT_AGENT_PROMPT = """You are a File Management Agent specialized in file operations and data handling.
121
 
122
+ Your expertise includes:
123
+ - Saving and reading files
124
+ - Downloading files from URLs
125
+ - Downloading task files from APIs
126
+ - File format handling
127
 
128
+ Follow ReAct methodology:
129
+ 1. THINK: What file operations are needed?
130
+ 2. ACT: Perform file operations safely
131
+ 3. VERIFY: Confirm successful operations
132
+ 4. REPORT: Provide clear status and file paths
133
 
134
+ Ensure secure and reliable file handling.
135
  """
136
 
137
+ # ============== TOOL DEFINITIONS (grouped by agent) ============== #
138
+
139
+ # Math Agent Tools
140
  @tool
141
+ def multiply(a: int, b: int) -> int:
142
+ """Multiply two numbers"""
 
 
143
  return a * b
144
 
145
  @tool
146
+ def add(a: int, b: int) -> int:
147
+ """Add two numbers"""
 
 
148
  return a + b
149
 
150
  @tool
151
+ def subtract(a: int, b: int) -> int:
152
+ """Subtract two numbers"""
 
 
153
  return a - b
154
 
155
  @tool
156
+ def divide(a: int, b: int) -> float:
157
+ """Divide two numbers"""
 
 
158
  return a / b
159
 
160
+ # Research Agent Tools
161
  @tool
162
  def wikidata_search(query: str) -> str:
163
+ """Search for information on Wikipedia and return maximum 2 results."""
 
 
 
 
 
164
  loader = WikipediaLoader(query=query, load_max_docs=2)
165
  docs = loader.load()
166
  formatted_search_docs = "\n\n---\n\n".join(
 
170
  ])
171
  return {"wiki_results": formatted_search_docs}
172
 
173
+ # Initialize search tools
174
+ tavily_search_tool = TavilySearch(max_results=3, topic="general")
 
 
 
 
 
175
  youtube_search_tool = YouTubeSearchTool()
176
 
177
+ # File Management Agent Tools
178
  @tool
179
  def save_and_read_file(content: str, filename: Optional[str] = None) -> str:
180
+ """Save content to a file and return the path."""
 
 
 
 
 
181
  temp_dir = tempfile.gettempdir()
182
  if filename is None:
183
  temp_file = tempfile.NamedTemporaryFile(delete=False, dir=temp_dir)
 
190
 
191
  return f"File saved to {filepath}. You can read this file to process its contents."
192
 
 
193
  @tool
194
  def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
195
+ """Download a file from a URL and save it to a temporary location."""
 
 
 
 
 
196
  try:
 
197
  if not filename:
198
  path = urlparse(url).path
199
  filename = os.path.basename(path)
200
  if not filename:
201
  filename = f"downloaded_{uuid.uuid4().hex[:8]}"
202
 
 
203
  temp_dir = tempfile.gettempdir()
204
  filepath = os.path.join(temp_dir, filename)
205
 
 
206
  response = requests.get(url, stream=True)
207
  response.raise_for_status()
208
 
 
209
  with open(filepath, "wb") as f:
210
  for chunk in response.iter_content(chunk_size=8192):
211
  f.write(chunk)
 
214
  except Exception as e:
215
  return f"Error downloading file: {str(e)}"
216
 
217
+ @tool
218
+ def download_task_file(task_id: str, api_url: str = "https://agents-course-unit4-scoring.hf.space") -> str:
219
+ """Download a file associated with a task from the evaluation API."""
220
+ try:
221
+ file_url = f"{api_url}/files/{task_id}"
222
+ temp_dir = tempfile.gettempdir()
223
+ filename = f"task_{task_id}.png"
224
+ filepath = os.path.join(temp_dir, filename)
225
+
226
+ response = requests.get(file_url, stream=True)
227
+ response.raise_for_status()
228
+
229
+ with open(filepath, "wb") as f:
230
+ for chunk in response.iter_content(chunk_size=8192):
231
+ f.write(chunk)
232
+
233
+ return f"Task file downloaded to {filepath}. You can now analyze this file."
234
+ except Exception as e:
235
+ return f"Error downloading task file: {str(e)}"
236
 
237
+ # Data Analysis Agent Tools
238
  @tool
239
  def extract_text_from_image(image_path: str) -> str:
240
+ """Extract text from an image using OCR."""
 
 
 
 
241
  try:
242
+ import pytesseract
243
  image = Image.open(image_path)
 
 
244
  text = pytesseract.image_to_string(image)
 
245
  return f"Extracted text from image:\n\n{text}"
246
  except Exception as e:
247
  return f"Error extracting text from image: {str(e)}"
248
 
 
249
  @tool
250
  def analyze_csv_file(file_path: str, query: str) -> str:
251
+ """Analyze a CSV file using pandas and answer a question about it."""
 
 
 
 
 
252
  try:
 
253
  df = pd.read_csv(file_path)
 
 
254
  result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
255
  result += f"Columns: {', '.join(df.columns)}\n\n"
 
 
256
  result += "Summary statistics:\n"
257
  result += str(df.describe())
 
258
  return result
 
259
  except Exception as e:
260
  return f"Error analyzing CSV file: {str(e)}"
261
 
 
262
  @tool
263
  def analyze_excel_file(file_path: str, query: str) -> str:
264
+ """Analyze an Excel file using pandas and answer a question about it."""
 
 
 
 
 
265
  try:
 
266
  df = pd.read_excel(file_path)
267
+ result = f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
 
 
 
 
268
  result += f"Columns: {', '.join(df.columns)}\n\n"
 
 
269
  result += "Summary statistics:\n"
270
  result += str(df.describe())
 
271
  return result
 
272
  except Exception as e:
273
  return f"Error analyzing Excel file: {str(e)}"
274
 
275
+ # Image Processing Agent Tools - Helper functions
 
 
 
 
 
 
 
 
276
  def encode_image(image_path: str) -> str:
277
  """Convert an image file to base64 string."""
278
  with open(image_path, "rb") as image_file:
279
  return base64.b64encode(image_file.read()).decode("utf-8")
280
 
 
281
  def decode_image(base64_string: str) -> Image.Image:
282
  """Convert a base64 string to a PIL Image."""
283
  image_data = base64.b64decode(base64_string)
284
  return Image.open(io.BytesIO(image_data))
285
 
 
286
  def save_image(image: Image.Image, directory: str = "image_outputs") -> str:
287
  """Save a PIL Image to disk and return the path."""
288
  os.makedirs(directory, exist_ok=True)
 
293
 
294
  @tool
295
  def analyze_image(image_base64: str) -> Dict[str, Any]:
296
+ """Analyze basic properties of an image."""
 
 
 
 
 
 
297
  try:
298
  img = decode_image(image_base64)
299
  width, height = img.size
 
326
  except Exception as e:
327
  return {"error": str(e)}
328
 
 
329
  @tool
330
  def transform_image(
331
  image_base64: str, operation: str, params: Optional[Dict[str, Any]] = None
332
  ) -> Dict[str, Any]:
333
+ """Apply transformations: resize, rotate, crop, flip, brightness, contrast, blur, sharpen, grayscale."""
 
 
 
 
 
 
 
 
334
  try:
335
  img = decode_image(image_base64)
336
  params = params or {}
337
 
338
  if operation == "resize":
339
+ img = img.resize((
340
+ params.get("width", img.width // 2),
341
+ params.get("height", img.height // 2),
342
+ ))
 
 
343
  elif operation == "rotate":
344
  img = img.rotate(params.get("angle", 90), expand=True)
345
  elif operation == "crop":
346
+ img = img.crop((
347
+ params.get("left", 0),
348
+ params.get("top", 0),
349
+ params.get("right", img.width),
350
+ params.get("bottom", img.height),
351
+ ))
 
 
352
  elif operation == "flip":
353
  if params.get("direction", "horizontal") == "horizontal":
354
  img = img.transpose(Image.FLIP_LEFT_RIGHT)
 
374
  except Exception as e:
375
  return {"error": str(e)}
376
 
 
377
  @tool
378
  def draw_on_image(
379
  image_base64: str, drawing_type: str, params: Dict[str, Any]
380
  ) -> Dict[str, Any]:
381
+ """Draw shapes (rectangle, circle, line) or text onto an image."""
 
 
 
 
 
 
 
 
382
  try:
383
  img = decode_image(image_base64)
384
  draw = ImageDraw.Draw(img)
 
398
  width=params.get("width", 2),
399
  )
400
  elif drawing_type == "line":
401
+ draw.line((
402
+ params["start_x"],
403
+ params["start_y"],
404
+ params["end_x"],
405
+ params["end_y"],
406
+ ), fill=color, width=params.get("width", 2))
 
 
 
 
407
  elif drawing_type == "text":
408
  font_size = params.get("font_size", 20)
409
  try:
 
426
  except Exception as e:
427
  return {"error": str(e)}
428
 
 
429
  @tool
430
  def generate_simple_image(
431
  image_type: str,
 
433
  height: int = 500,
434
  params: Optional[Dict[str, Any]] = None,
435
  ) -> Dict[str, Any]:
436
+ """Generate a simple image (gradient, noise, pattern, chart)."""
 
 
 
 
 
 
 
 
437
  try:
438
  params = params or {}
439
 
 
447
 
448
  if direction == "horizontal":
449
  for x in range(width):
450
+ r = int(start_color[0] + (end_color[0] - start_color[0]) * x / width)
451
+ g = int(start_color[1] + (end_color[1] - start_color[1]) * x / width)
452
+ b = int(start_color[2] + (end_color[2] - start_color[2]) * x / width)
 
 
 
 
 
 
453
  draw.line([(x, 0), (x, height)], fill=(r, g, b))
454
  else:
455
  for y in range(height):
456
+ r = int(start_color[0] + (end_color[0] - start_color[0]) * y / height)
457
+ g = int(start_color[1] + (end_color[1] - start_color[1]) * y / height)
458
+ b = int(start_color[2] + (end_color[2] - start_color[2]) * y / height)
 
 
 
 
 
 
459
  draw.line([(0, y), (width, y)], fill=(r, g, b))
460
 
461
  elif image_type == "noise":
462
  noise_array = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8)
463
  img = Image.fromarray(noise_array, "RGB")
 
464
  else:
465
  return {"error": f"Unsupported image_type {image_type}"}
466
 
 
471
  except Exception as e:
472
  return {"error": str(e)}
473
 
 
474
  @tool
475
  def combine_images(
476
  images_base64: List[str], operation: str, params: Optional[Dict[str, Any]] = None
477
  ) -> Dict[str, Any]:
478
+ """Combine multiple images (collage, stack, blend)."""
 
 
 
 
 
 
 
 
479
  try:
480
  images = [decode_image(b64) for b64 in images_base64]
481
  params = params or {}
 
508
  except Exception as e:
509
  return {"error": str(e)}
510
 
511
+ # ============== SPECIALIZED AGENT CLASSES ============== #
512
+
513
+ class SpecializedAgent:
514
+ """Base class for specialized agents"""
515
+ def __init__(self, name: str, system_prompt: str, tools: List):
516
+ self.name = name
517
+ self.system_prompt = system_prompt
518
+ self.tools = tools
519
+ self.llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", api_key=os.getenv("GOOGLE_API_KEY"))
520
+ self.llm_with_tools = self.llm.bind_tools(tools)
521
+ self.graph = self._build_graph()
 
522
 
523
+ def _build_graph(self):
524
+ def agent_node(state: MessagesState) -> MessagesState:
525
+ messages = state["messages"]
526
+ if not messages or not isinstance(messages[0], SystemMessage):
527
+ messages = [SystemMessage(content=self.system_prompt)] + messages
528
+ return {"messages": [self.llm_with_tools.invoke(messages)]}
529
 
530
+ builder = StateGraph(MessagesState)
531
+ builder.add_node("agent", agent_node)
532
+ builder.add_node("tools", ToolNode(self.tools))
533
 
534
+ builder.add_edge(START, "agent")
535
+ builder.add_conditional_edges("agent", tools_condition)
536
+ builder.add_edge("tools", "agent")
 
537
 
538
+ return builder.compile()
539
+
540
+ def __call__(self, question: str) -> str:
541
+ try:
542
+ messages = [HumanMessage(content=question)]
543
+ result = self.graph.invoke({"messages": messages})
544
+ return result["messages"][-1].content
545
+ except Exception as e:
546
+ return f"Error in {self.name}: {str(e)}"
547
 
548
+ # Agent tool groupings
549
+ RESEARCH_TOOLS = [wikidata_search, tavily_search_tool, youtube_search_tool]
550
+ MATH_TOOLS = [multiply, add, subtract, divide]
551
+ DATA_ANALYSIS_TOOLS = [analyze_csv_file, analyze_excel_file, extract_text_from_image]
552
+ IMAGE_PROCESSING_TOOLS = [analyze_image, transform_image, draw_on_image, generate_simple_image, combine_images]
553
+ FILE_MANAGEMENT_TOOLS = [save_and_read_file, download_file_from_url, download_task_file]
554
 
555
+ # ============== MULTI-AGENT SYSTEM ============== #
 
 
556
 
557
+ class MultiAgentSystem:
558
+ def __init__(self):
559
+ # Initialize specialized agents
560
+ self.research_agent = SpecializedAgent("Research Agent", RESEARCH_AGENT_PROMPT, RESEARCH_TOOLS)
561
+ self.math_agent = SpecializedAgent("Math Agent", MATH_AGENT_PROMPT, MATH_TOOLS)
562
+ self.data_agent = SpecializedAgent("Data Analysis Agent", DATA_ANALYSIS_AGENT_PROMPT, DATA_ANALYSIS_TOOLS)
563
+ self.image_agent = SpecializedAgent("Image Processing Agent", IMAGE_PROCESSING_AGENT_PROMPT, IMAGE_PROCESSING_TOOLS)
564
+ self.file_agent = SpecializedAgent("File Management Agent", FILE_MANAGEMENT_AGENT_PROMPT, FILE_MANAGEMENT_TOOLS)
565
 
566
+ # Coordinator LLM
567
+ self.coordinator_llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", api_key=os.getenv("GOOGLE_API_KEY"))
568
+
569
+ print("Multi-Agent System initialized with 5 specialized agents.")
 
 
 
 
 
 
 
570
 
571
+ def _classify_task(self, question: str) -> Dict[str, Any]:
572
+ """Use the coordinator to classify the task and determine which agents to use"""
573
+ classification_prompt = f"""
574
+ As a task classifier, analyze this question and determine which specialized agents are needed:
575
+
576
+ Question: {question}
577
+
578
+ Available agents:
579
+ - research: For Wikipedia, web search, YouTube search
580
+ - math: For mathematical calculations
581
+ - data_analysis: For CSV/Excel analysis, OCR
582
+ - image_processing: For image analysis, manipulation, generation
583
+ - file_management: For file operations, downloads
584
+
585
+ Respond with a JSON object containing:
586
+ {{
587
+ "primary_agent": "agent_name",
588
+ "supporting_agents": ["agent1", "agent2"],
589
+ "task_breakdown": "explanation of how to approach this task",
590
+ "requires_coordination": true/false
591
+ }}
592
+ """
593
+
594
+ response = self.coordinator_llm.invoke([HumanMessage(content=classification_prompt)])
595
+
596
+ # Simple classification logic as fallback
597
+ question_lower = question.lower()
598
+
599
+ classification = {
600
+ "primary_agent": "research",
601
+ "supporting_agents": [],
602
+ "task_breakdown": "Research-based question",
603
+ "requires_coordination": False
604
+ }
605
+
606
+ # Determine primary agent based on keywords
607
+ if any(word in question_lower for word in ['calculate', 'multiply', 'add', 'subtract', 'divide', 'math']):
608
+ classification["primary_agent"] = "math"
609
+ elif any(word in question_lower for word in ['csv', 'excel', 'data', 'analyze data', 'spreadsheet']):
610
+ classification["primary_agent"] = "data_analysis"
611
+ elif any(word in question_lower for word in ['image', 'photo', 'picture', 'draw', 'generate image']):
612
+ classification["primary_agent"] = "image_processing"
613
+ elif any(word in question_lower for word in ['download', 'file', 'save']):
614
+ classification["primary_agent"] = "file_management"
615
+
616
+ return classification
617
 
618
+ def __call__(self, question: str) -> str:
619
+ """Route the question to appropriate agents and coordinate the response"""
620
+ try:
621
+ # Classify the task
622
+ classification = self._classify_task(question)
623
+ primary_agent = classification["primary_agent"]
624
+
625
+ # Route to primary agent
626
+ if primary_agent == "research":
627
+ response = self.research_agent(question)
628
+ elif primary_agent == "math":
629
+ response = self.math_agent(question)
630
+ elif primary_agent == "data_analysis":
631
+ response = self.data_agent(question)
632
+ elif primary_agent == "image_processing":
633
+ response = self.image_agent(question)
634
+ elif primary_agent == "file_management":
635
+ response = self.file_agent(question)
636
+ else:
637
+ response = self.research_agent(question) # Default fallback
638
+
639
+ # For now, return the primary agent's response
640
+ # In a more sophisticated system, we would coordinate between multiple agents
641
+ return response
642
+
643
+ except Exception as e:
644
+ return f"Error in Multi-Agent System: {str(e)}"
645
 
646
+ # ============== MAIN AGENT CLASS (for backward compatibility) ============== #
647
 
648
  class LangGraphAgent:
649
  def __init__(self):
650
+ self.multi_agent_system = MultiAgentSystem()
651
+ print("LangGraphAgent initialized with Multi-Agent System.")
652
 
653
  def __call__(self, question: str) -> str:
654
+ """Run the multi-agent system on a question and return the answer"""
655
+ return self.multi_agent_system(question)
 
 
 
 
 
 
 
656
 
657
  if __name__ == "__main__":
658
  agent = LangGraphAgent()
659
  question = "How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia."
660
  answer = agent(question)
661
+ print(f"\nFinal Answer: {answer}")
662
 
663
 
664