techy-ai commited on
Commit
500e8e6
ยท
1 Parent(s): 5c8dd16

tool error fix

Browse files
Files changed (1) hide show
  1. agent.py +483 -127
agent.py CHANGED
@@ -19,6 +19,9 @@ interpreter_instance = CodeInterpreter()
19
 
20
  from image_processing import *
21
 
 
 
 
22
  """Langraph"""
23
  from langgraph.graph import START, StateGraph, MessagesState
24
  from langchain_community.tools.tavily_search import TavilySearchResults
@@ -54,8 +57,13 @@ def tool_response(success: bool, data=None, error=None):
54
  from typing import Any
55
 
56
  @tool
57
- def multiply(a: Any, b: Any):
58
- """Multiply two numbers and return the product."""
 
 
 
 
 
59
  logger.info("multiply called with a=%s, b=%s", a, b)
60
  try:
61
  a = float(a)
@@ -67,8 +75,13 @@ def multiply(a: Any, b: Any):
67
  return tool_response(False, error=f"Invalid input: {e}")
68
 
69
  @tool
70
- def add(a: Any, b: Any):
71
- """Add two numbers and return the sum."""
 
 
 
 
 
72
  logger.info("add called with a=%s, b=%s", a, b)
73
  try:
74
  a = float(a)
@@ -79,8 +92,13 @@ def add(a: Any, b: Any):
79
  return tool_response(False, error=f"Invalid input: {e}")
80
 
81
  @tool
82
- def subtract(a: Any, b: Any):
83
- """Subtract b from a and return the result."""
 
 
 
 
 
84
  logger.info("subtract called with a=%s, b=%s", a, b)
85
  try:
86
  a = float(a)
@@ -91,8 +109,13 @@ def subtract(a: Any, b: Any):
91
  return tool_response(False, error=f"Invalid input: {e}")
92
 
93
  @tool
94
- def divide(a: Any, b: Any):
95
- """Divide a by b and return the quotient."""
 
 
 
 
 
96
  logger.info("divide called with a=%s, b=%s", a, b)
97
  try:
98
  a = float(a)
@@ -105,20 +128,30 @@ def divide(a: Any, b: Any):
105
  return tool_response(False, error=f"Invalid input: {e}")
106
 
107
  @tool
108
- def modulus(a: Any, b: Any):
109
- """Return the remainder of a divided by b."""
 
 
 
 
 
110
  logger.info("modulus called with a=%s, b=%s", a, b)
111
  try:
112
- a = float(a)
113
- b = float(b)
114
  return tool_response(True, a % b)
115
  except Exception as e:
116
  logger.error("modulus failed: %s", str(e))
117
  return tool_response(False, error=f"Invalid input: {e}")
118
 
119
  @tool
120
- def power(a: Any, b: Any):
121
- """Raise a to the power of b."""
 
 
 
 
 
122
  logger.info("power called with a=%s, b=%s", a, b)
123
  try:
124
  a = float(a)
@@ -129,8 +162,12 @@ def power(a: Any, b: Any):
129
  return tool_response(False, error=f"Invalid input: {e}")
130
 
131
  @tool
132
- def square_root(a: Any):
133
- """Return the square root of a number."""
 
 
 
 
134
  logger.info("square_root called with a=%s", a)
135
  try:
136
  a = float(a)
@@ -141,6 +178,30 @@ def square_root(a: Any):
141
  except Exception as e:
142
  logger.error("square_root failed: %s", str(e))
143
  return tool_response(False, error=f"Invalid input: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  # =========================
146
  # ๐Ÿ“‚ File Tools
@@ -148,7 +209,12 @@ def square_root(a: Any):
148
 
149
  @tool
150
  def save_and_read_file(filename: str, content: str):
151
- """Save content to a file and return the content back."""
 
 
 
 
 
152
  logger.info("save_and_read_file called with filename=%s", filename)
153
  try:
154
  with open(filename, "w", encoding="utf-8") as f:
@@ -163,7 +229,12 @@ def save_and_read_file(filename: str, content: str):
163
 
164
  @tool
165
  def download_file_from_url(url: str):
166
- """Download a file from a URL and return its local path."""
 
 
 
 
 
167
  logger.info("download_file_from_url called with url=%s", url)
168
  try:
169
  if url.startswith("file://"):
@@ -184,101 +255,303 @@ def download_file_from_url(url: str):
184
  # =========================
185
 
186
  @tool
187
- def extract_text_from_image(image_path: str):
188
- """Extract text from an image using OCR."""
189
- logger.info("extract_text_from_image called with image_path=%s", image_path)
 
 
 
190
  try:
191
- text = pytesseract.image_to_string(Image.open(image_path))
192
- return tool_response(True, text.strip())
 
 
 
 
 
193
  except Exception as e:
194
- logger.error("extract_text_from_image failed: %s", str(e))
195
- return tool_response(False, error=f"OCR error: {e}")
196
 
197
 
198
  @tool
199
- def analyze_image(image_path: str):
200
- """Return basic analysis (size, mode) of an image."""
201
- logger.info("analyze_image called with image_path=%s", image_path)
 
 
 
 
 
202
  try:
203
- with Image.open(image_path) as img:
204
- data = {"format": img.format, "mode": img.mode, "size": img.size}
205
- return tool_response(True, data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
  except Exception as e:
207
- logger.error("analyze_image failed: %s", str(e))
208
- return tool_response(False, error=f"Image analysis error: {e}")
209
 
210
 
211
  @tool
212
- def transform_image(image_path: str, operation: str):
213
- """Apply a simple transform (grayscale, blur, sharpen)."""
214
- logger.info("transform_image called with image_path=%s operation=%s", image_path, operation)
 
 
 
 
 
 
 
 
 
215
  try:
216
- img = Image.open(image_path)
217
- if operation == "grayscale":
218
- img = img.convert("L")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  elif operation == "blur":
220
- img = img.filter(ImageFilter.BLUR)
221
  elif operation == "sharpen":
222
  img = img.filter(ImageFilter.SHARPEN)
 
 
223
  else:
224
- raise ValueError(f"Unsupported operation: {operation}")
225
- output_path = f"transformed_{uuid.uuid4()}.png"
226
- img.save(output_path)
227
- return tool_response(True, output_path)
 
 
228
  except Exception as e:
229
- logger.error("transform_image failed: %s", str(e))
230
- return tool_response(False, error=f"Transform error: {e}")
231
 
232
 
233
  @tool
234
- def draw_on_image(image_path: str, text: str):
235
- """Draw text on an image."""
236
- logger.info("draw_on_image called with image_path=%s text=%s", image_path, text)
 
 
 
 
 
 
 
 
 
237
  try:
238
- img = Image.open(image_path)
239
  draw = ImageDraw.Draw(img)
240
- draw.text((10, 10), text, fill="black")
241
- output_path = f"drawn_{uuid.uuid4()}.png"
242
- img.save(output_path)
243
- return tool_response(True, output_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  except Exception as e:
245
- logger.error("draw_on_image failed: %s", str(e))
246
- return tool_response(False, error=f"Draw error: {e}")
247
 
248
 
249
  @tool
250
- def generate_simple_image(text: str):
251
- """Generate a simple image with text."""
252
- logger.info("generate_simple_image called with text=%s", text)
 
 
 
 
 
 
 
 
 
 
 
 
253
  try:
254
- img = Image.new("RGB", (200, 100), color="white")
255
- draw = ImageDraw.Draw(img)
256
- draw.text((10, 40), text, fill="black")
257
- output_path = f"generated_{uuid.uuid4()}.png"
258
- img.save(output_path)
259
- return tool_response(True, output_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  except Exception as e:
261
- logger.error("generate_simple_image failed: %s", str(e))
262
- return tool_response(False, error=f"Image generation error: {e}")
263
 
264
 
265
  @tool
266
- def combine_images(image1_path: str, image2_path: str):
267
- """Combine two images side by side."""
268
- logger.info("combine_images called with %s and %s", image1_path, image2_path)
 
 
 
 
 
 
 
 
 
269
  try:
270
- img1 = Image.open(image1_path)
271
- img2 = Image.open(image2_path)
272
- combined = Image.new("RGB", (img1.width + img2.width, max(img1.height, img2.height)))
273
- combined.paste(img1, (0, 0))
274
- combined.paste(img2, (img1.width, 0))
275
- output_path = f"combined_{uuid.uuid4()}.png"
276
- combined.save(output_path)
277
- return tool_response(True, output_path)
278
- except Exception as e:
279
- logger.error("combine_images failed: %s", str(e))
280
- return tool_response(False, error=f"Combine error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
281
 
 
 
 
 
 
 
282
 
283
  # =========================
284
  # ๐Ÿ“Š Data Tools
@@ -286,7 +559,12 @@ def combine_images(image1_path: str, image2_path: str):
286
 
287
  @tool
288
  def analyze_csv_file(file_path: str):
289
- """Analyze a CSV file and return basic info."""
 
 
 
 
 
290
  logger.info("analyze_csv_file called with file_path=%s", file_path)
291
  try:
292
  df = pd.read_csv(file_path)
@@ -299,7 +577,12 @@ def analyze_csv_file(file_path: str):
299
 
300
  @tool
301
  def analyze_excel_file(file_path: str):
302
- """Analyze an Excel file and return basic info."""
 
 
 
 
 
303
  logger.info("analyze_excel_file called with file_path=%s", file_path)
304
  try:
305
  df = pd.read_excel(file_path)
@@ -315,62 +598,119 @@ def analyze_excel_file(file_path: str):
315
  # =========================
316
 
317
  @tool
318
- def execute_code_multilang(code: str, language: str = "python"):
319
- """Execute code in multiple languages using CodeInterpreter."""
320
- logger.info("execute_code_multilang called with language=%s", language)
321
- try:
322
- result = interpreter_instance.execute_code(code, language)
323
- return tool_response(True, result)
324
- except Exception as e:
325
- logger.error("execute_code_multilang failed: %s", str(e))
326
- return tool_response(False, error=f"Code execution error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
 
328
  # =========================
329
  # ๐ŸŒ Search Tools
330
  # =========================
331
 
332
  @tool
333
- def web_search(query: str, max_results: int = 3):
334
- """Perform a web search using TavilySearchResults."""
335
- logger.info("web_search called with query=%s", query)
336
- try:
337
- tavily = TavilySearchResults(max_results=max_results)
338
- results = tavily.invoke(query)
339
- return tool_response(True, results)
340
- except Exception as e:
341
- logger.error("web_search failed: %s", str(e))
342
- return tool_response(False, error=f"Web search error: {e}")
 
 
343
 
344
 
345
  @tool
346
- def wiki_search(query: str):
347
- """Search Wikipedia and return documents."""
348
- logger.info("wiki_search called with query=%s", query)
349
- try:
350
- loader = WikipediaLoader(query=query, load_max_docs=3)
351
- docs = loader.load()
352
- results = [doc.page_content for doc in docs]
353
- return tool_response(True, results)
354
- except Exception as e:
355
- logger.error("wiki_search failed: %s", str(e))
356
- return tool_response(False, error=f"Wikipedia error: {e}")
 
357
 
358
 
359
  @tool
360
- def arxiv_search(query: str):
361
- """Search Arxiv and return documents."""
362
- logger.info("arxiv_search called with query=%s", query)
363
- try:
364
- loader = ArxivLoader(query=query, load_max_docs=3)
365
- docs = loader.load()
366
- results = [doc.page_content for doc in docs]
367
- return tool_response(True, results)
368
- except Exception as e:
369
- logger.error("arxiv_search failed: %s", str(e))
370
- return tool_response(False, error=f"Arxiv error: {e}")
 
 
 
 
371
 
372
- if __name__ == "__main__":
373
- logger.info("=== Running Tool Tests ===")
374
 
375
 
376
  # =========================
@@ -445,6 +785,7 @@ tools = [
445
  draw_on_image,
446
  generate_simple_image,
447
  combine_images,
 
448
  ]
449
 
450
 
@@ -452,9 +793,24 @@ tools = [
452
  def build_graph(provider: str = "groq"):
453
  """Build the graph"""
454
  # Load environment variables from .env file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  if provider == "groq":
456
  # Groq https://console.groq.com/docs/models
457
- llm = ChatGroq(model="qwen/qwen3-32b", temperature=0)
458
  elif provider == "huggingface":
459
  # TODO: Add huggingface endpoint
460
  llm = ChatHuggingFace(
 
19
 
20
  from image_processing import *
21
 
22
+ from langchain_core.tools import tool
23
+ import speech_recognition as sr
24
+
25
  """Langraph"""
26
  from langgraph.graph import START, StateGraph, MessagesState
27
  from langchain_community.tools.tavily_search import TavilySearchResults
 
57
  from typing import Any
58
 
59
  @tool
60
+ def multiply(a: float, b: float) -> float:
61
+ """
62
+ Multiplies two numbers.
63
+ Args:
64
+ a (float): the first number
65
+ b (float): the second number
66
+ """
67
  logger.info("multiply called with a=%s, b=%s", a, b)
68
  try:
69
  a = float(a)
 
75
  return tool_response(False, error=f"Invalid input: {e}")
76
 
77
  @tool
78
+ def add(a: float, b: float) -> float:
79
+ """
80
+ Adds two numbers.
81
+ Args:
82
+ a (float): the first number
83
+ b (float): the second number
84
+ """
85
  logger.info("add called with a=%s, b=%s", a, b)
86
  try:
87
  a = float(a)
 
92
  return tool_response(False, error=f"Invalid input: {e}")
93
 
94
  @tool
95
+ def subtract(a: float, b: float) -> int:
96
+ """
97
+ Subtracts two numbers.
98
+ Args:
99
+ a (float): the first number
100
+ b (float): the second number
101
+ """
102
  logger.info("subtract called with a=%s, b=%s", a, b)
103
  try:
104
  a = float(a)
 
109
  return tool_response(False, error=f"Invalid input: {e}")
110
 
111
  @tool
112
+ def divide(a: float, b: float) -> float:
113
+ """
114
+ Divides two numbers.
115
+ Args:
116
+ a (float): the first float number
117
+ b (float): the second float number
118
+ """
119
  logger.info("divide called with a=%s, b=%s", a, b)
120
  try:
121
  a = float(a)
 
128
  return tool_response(False, error=f"Invalid input: {e}")
129
 
130
  @tool
131
+ def modulus(a: int, b: int) -> int:
132
+ """
133
+ Get the modulus of two numbers.
134
+ Args:
135
+ a (int): the first number
136
+ b (int): the second number
137
+ """
138
  logger.info("modulus called with a=%s, b=%s", a, b)
139
  try:
140
+ a = int(a)
141
+ b = int(b)
142
  return tool_response(True, a % b)
143
  except Exception as e:
144
  logger.error("modulus failed: %s", str(e))
145
  return tool_response(False, error=f"Invalid input: {e}")
146
 
147
  @tool
148
+ def power(a: float, b: float) -> float:
149
+ """
150
+ Get the power of two numbers.
151
+ Args:
152
+ a (float): the first number
153
+ b (float): the second number
154
+ """
155
  logger.info("power called with a=%s, b=%s", a, b)
156
  try:
157
  a = float(a)
 
162
  return tool_response(False, error=f"Invalid input: {e}")
163
 
164
  @tool
165
+ def square_root(a: float) -> float|complex:
166
+ """
167
+ Get the square root of a number.
168
+ Args:
169
+ a (float): the number to get the square root of
170
+ """
171
  logger.info("square_root called with a=%s", a)
172
  try:
173
  a = float(a)
 
178
  except Exception as e:
179
  logger.error("square_root failed: %s", str(e))
180
  return tool_response(False, error=f"Invalid input: {e}")
181
+
182
+
183
+ ### =============== Audio TOOLS =============== ###
184
+
185
+ @tool("extract_text_from_audio", return_direct=True)
186
+ def extract_text_from_audio(audio_file_path: str) -> str:
187
+ """
188
+ Extract text from an audio file using speech recognition.
189
+ Args:
190
+ audio_file_path (str): Path to the audio file (wav, mp3, etc.)
191
+ Returns:
192
+ str: Transcribed text from the audio.
193
+ """
194
+ recognizer = sr.Recognizer()
195
+ with sr.AudioFile(audio_file_path) as source:
196
+ audio = recognizer.record(source)
197
+ try:
198
+ # Use Whisper (local) recognizer as recommended by SpeechRecognition[whisper-local]
199
+ text = recognizer.recognize_whisper(audio)
200
+ except sr.UnknownValueError:
201
+ text = "Could not understand audio."
202
+ except sr.RequestError as e:
203
+ text = f"Recognition error: {e}"
204
+ return text
205
 
206
  # =========================
207
  # ๐Ÿ“‚ File Tools
 
209
 
210
  @tool
211
  def save_and_read_file(filename: str, content: str):
212
+ """
213
+ Save content to a file and return the path.
214
+ Args:
215
+ content (str): the content to save to the file
216
+ filename (str, optional): the name of the file. If not provided, a random name file will be created.
217
+ """
218
  logger.info("save_and_read_file called with filename=%s", filename)
219
  try:
220
  with open(filename, "w", encoding="utf-8") as f:
 
229
 
230
  @tool
231
  def download_file_from_url(url: str):
232
+ """
233
+ Download a file from a URL and save it to a temporary location.
234
+ Args:
235
+ url (str): the URL of the file to download.
236
+ filename (str, optional): the name of the file. If not provided, a random name file will be created.
237
+ """
238
  logger.info("download_file_from_url called with url=%s", url)
239
  try:
240
  if url.startswith("file://"):
 
255
  # =========================
256
 
257
  @tool
258
+ def extract_text_from_image(image_path: str) -> str:
259
+ """
260
+ Extract text from an image using OCR library pytesseract (if available).
261
+ Args:
262
+ image_path (str): the path to the image file.
263
+ """
264
  try:
265
+ # Open the image
266
+ image = Image.open(image_path)
267
+
268
+ # Extract text from the image
269
+ text = pytesseract.image_to_string(image)
270
+
271
+ return f"Extracted text from image:\n\n{text}"
272
  except Exception as e:
273
+ return f"Error extracting text from image: {str(e)}"
 
274
 
275
 
276
  @tool
277
+ def analyze_image(image_base64: str) -> Dict[str, Any]:
278
+ """
279
+ Analyze basic properties of an image (size, mode, color analysis, thumbnail preview).
280
+ Args:
281
+ image_base64 (str): Base64 encoded image string
282
+ Returns:
283
+ Dictionary with analysis result
284
+ """
285
  try:
286
+ img = decode_image(image_base64)
287
+ width, height = img.size
288
+ mode = img.mode
289
+
290
+ if mode in ("RGB", "RGBA"):
291
+ arr = np.array(img)
292
+ avg_colors = arr.mean(axis=(0, 1))
293
+ dominant = ["Red", "Green", "Blue"][np.argmax(avg_colors[:3])]
294
+ brightness = avg_colors.mean()
295
+ color_analysis = {
296
+ "average_rgb": avg_colors.tolist(),
297
+ "brightness": brightness,
298
+ "dominant_color": dominant,
299
+ }
300
+ else:
301
+ color_analysis = {"note": f"No color analysis for mode {mode}"}
302
+
303
+ thumbnail = img.copy()
304
+ thumbnail.thumbnail((100, 100))
305
+ thumb_path = save_image(thumbnail, "thumbnails")
306
+ thumbnail_base64 = encode_image(thumb_path)
307
+
308
+ return {
309
+ "dimensions": (width, height),
310
+ "mode": mode,
311
+ "color_analysis": color_analysis,
312
+ "thumbnail": thumbnail_base64,
313
+ }
314
  except Exception as e:
315
+ return {"error": str(e)}
 
316
 
317
 
318
  @tool
319
+ def transform_image(
320
+ image_base64: str, operation: str, params: Optional[Dict[str, Any]] = None
321
+ ) -> Dict[str, Any]:
322
+ """
323
+ Apply transformations: resize, rotate, crop, flip, brightness, contrast, blur, sharpen, grayscale.
324
+ Args:
325
+ image_base64 (str): Base64 encoded input image
326
+ operation (str): Transformation operation
327
+ params (Dict[str, Any], optional): Parameters for the operation
328
+ Returns:
329
+ Dictionary with transformed image (base64)
330
+ """
331
  try:
332
+ img = decode_image(image_base64)
333
+ params = params or {}
334
+
335
+ if operation == "resize":
336
+ img = img.resize(
337
+ (
338
+ params.get("width", img.width // 2),
339
+ params.get("height", img.height // 2),
340
+ )
341
+ )
342
+ elif operation == "rotate":
343
+ img = img.rotate(params.get("angle", 90), expand=True)
344
+ elif operation == "crop":
345
+ img = img.crop(
346
+ (
347
+ params.get("left", 0),
348
+ params.get("top", 0),
349
+ params.get("right", img.width),
350
+ params.get("bottom", img.height),
351
+ )
352
+ )
353
+ elif operation == "flip":
354
+ if params.get("direction", "horizontal") == "horizontal":
355
+ img = img.transpose(Image.FLIP_LEFT_RIGHT)
356
+ else:
357
+ img = img.transpose(Image.FLIP_TOP_BOTTOM)
358
+ elif operation == "adjust_brightness":
359
+ img = ImageEnhance.Brightness(img).enhance(params.get("factor", 1.5))
360
+ elif operation == "adjust_contrast":
361
+ img = ImageEnhance.Contrast(img).enhance(params.get("factor", 1.5))
362
  elif operation == "blur":
363
+ img = img.filter(ImageFilter.GaussianBlur(params.get("radius", 2)))
364
  elif operation == "sharpen":
365
  img = img.filter(ImageFilter.SHARPEN)
366
+ elif operation == "grayscale":
367
+ img = img.convert("L")
368
  else:
369
+ return {"error": f"Unknown operation: {operation}"}
370
+
371
+ result_path = save_image(img)
372
+ result_base64 = encode_image(result_path)
373
+ return {"transformed_image": result_base64}
374
+
375
  except Exception as e:
376
+ return {"error": str(e)}
 
377
 
378
 
379
  @tool
380
+ def draw_on_image(
381
+ image_base64: str, drawing_type: str, params: Dict[str, Any]
382
+ ) -> Dict[str, Any]:
383
+ """
384
+ Draw shapes (rectangle, circle, line) or text onto an image.
385
+ Args:
386
+ image_base64 (str): Base64 encoded input image
387
+ drawing_type (str): Drawing type
388
+ params (Dict[str, Any]): Drawing parameters
389
+ Returns:
390
+ Dictionary with result image (base64)
391
+ """
392
  try:
393
+ img = decode_image(image_base64)
394
  draw = ImageDraw.Draw(img)
395
+ color = params.get("color", "red")
396
+
397
+ if drawing_type == "rectangle":
398
+ draw.rectangle(
399
+ [params["left"], params["top"], params["right"], params["bottom"]],
400
+ outline=color,
401
+ width=params.get("width", 2),
402
+ )
403
+ elif drawing_type == "circle":
404
+ x, y, r = params["x"], params["y"], params["radius"]
405
+ draw.ellipse(
406
+ (x - r, y - r, x + r, y + r),
407
+ outline=color,
408
+ width=params.get("width", 2),
409
+ )
410
+ elif drawing_type == "line":
411
+ draw.line(
412
+ (
413
+ params["start_x"],
414
+ params["start_y"],
415
+ params["end_x"],
416
+ params["end_y"],
417
+ ),
418
+ fill=color,
419
+ width=params.get("width", 2),
420
+ )
421
+ elif drawing_type == "text":
422
+ font_size = params.get("font_size", 20)
423
+ try:
424
+ font = ImageFont.truetype("arial.ttf", font_size)
425
+ except IOError:
426
+ font = ImageFont.load_default()
427
+ draw.text(
428
+ (params["x"], params["y"]),
429
+ params.get("text", "Text"),
430
+ fill=color,
431
+ font=font,
432
+ )
433
+ else:
434
+ return {"error": f"Unknown drawing type: {drawing_type}"}
435
+
436
+ result_path = save_image(img)
437
+ result_base64 = encode_image(result_path)
438
+ return {"result_image": result_base64}
439
+
440
  except Exception as e:
441
+ return {"error": str(e)}
 
442
 
443
 
444
  @tool
445
+ def generate_simple_image(
446
+ image_type: str,
447
+ width: int = 500,
448
+ height: int = 500,
449
+ params: Optional[Dict[str, Any]] = None,
450
+ ) -> Dict[str, Any]:
451
+ """
452
+ Generate a simple image (gradient, noise, pattern, chart).
453
+ Args:
454
+ image_type (str): Type of image
455
+ width (int), height (int)
456
+ params (Dict[str, Any], optional): Specific parameters
457
+ Returns:
458
+ Dictionary with generated image (base64)
459
+ """
460
  try:
461
+ params = params or {}
462
+
463
+ if image_type == "gradient":
464
+ direction = params.get("direction", "horizontal")
465
+ start_color = params.get("start_color", (255, 0, 0))
466
+ end_color = params.get("end_color", (0, 0, 255))
467
+
468
+ img = Image.new("RGB", (width, height))
469
+ draw = ImageDraw.Draw(img)
470
+
471
+ if direction == "horizontal":
472
+ for x in range(width):
473
+ r = int(
474
+ start_color[0] + (end_color[0] - start_color[0]) * x / width
475
+ )
476
+ g = int(
477
+ start_color[1] + (end_color[1] - start_color[1]) * x / width
478
+ )
479
+ b = int(
480
+ start_color[2] + (end_color[2] - start_color[2]) * x / width
481
+ )
482
+ draw.line([(x, 0), (x, height)], fill=(r, g, b))
483
+ else:
484
+ for y in range(height):
485
+ r = int(
486
+ start_color[0] + (end_color[0] - start_color[0]) * y / height
487
+ )
488
+ g = int(
489
+ start_color[1] + (end_color[1] - start_color[1]) * y / height
490
+ )
491
+ b = int(
492
+ start_color[2] + (end_color[2] - start_color[2]) * y / height
493
+ )
494
+ draw.line([(0, y), (width, y)], fill=(r, g, b))
495
+
496
+ elif image_type == "noise":
497
+ noise_array = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8)
498
+ img = Image.fromarray(noise_array, "RGB")
499
+
500
+ else:
501
+ return {"error": f"Unsupported image_type {image_type}"}
502
+
503
+ result_path = save_image(img)
504
+ result_base64 = encode_image(result_path)
505
+ return {"generated_image": result_base64}
506
+
507
  except Exception as e:
508
+ return {"error": str(e)}
 
509
 
510
 
511
  @tool
512
+ def combine_images(
513
+ images_base64: List[str], operation: str, params: Optional[Dict[str, Any]] = None
514
+ ) -> Dict[str, Any]:
515
+ """
516
+ Combine multiple images (collage, stack, blend).
517
+ Args:
518
+ images_base64 (List[str]): List of base64 images
519
+ operation (str): Combination type
520
+ params (Dict[str, Any], optional)
521
+ Returns:
522
+ Dictionary with combined image (base64)
523
+ """
524
  try:
525
+ images = [decode_image(b64) for b64 in images_base64]
526
+ params = params or {}
527
+
528
+ if operation == "stack":
529
+ direction = params.get("direction", "horizontal")
530
+ if direction == "horizontal":
531
+ total_width = sum(img.width for img in images)
532
+ max_height = max(img.height for img in images)
533
+ new_img = Image.new("RGB", (total_width, max_height))
534
+ x = 0
535
+ for img in images:
536
+ new_img.paste(img, (x, 0))
537
+ x += img.width
538
+ else:
539
+ max_width = max(img.width for img in images)
540
+ total_height = sum(img.height for img in images)
541
+ new_img = Image.new("RGB", (max_width, total_height))
542
+ y = 0
543
+ for img in images:
544
+ new_img.paste(img, (0, y))
545
+ y += img.height
546
+ else:
547
+ return {"error": f"Unsupported combination operation {operation}"}
548
 
549
+ result_path = save_image(new_img)
550
+ result_base64 = encode_image(result_path)
551
+ return {"combined_image": result_base64}
552
+
553
+ except Exception as e:
554
+ return {"error": str(e)}
555
 
556
  # =========================
557
  # ๐Ÿ“Š Data Tools
 
559
 
560
  @tool
561
  def analyze_csv_file(file_path: str):
562
+ """
563
+ Analyze a CSV file using pandas and answer a question about it.
564
+ Args:
565
+ file_path (str): the path to the CSV file.
566
+ query (str): Question about the data
567
+ """
568
  logger.info("analyze_csv_file called with file_path=%s", file_path)
569
  try:
570
  df = pd.read_csv(file_path)
 
577
 
578
  @tool
579
  def analyze_excel_file(file_path: str):
580
+ """
581
+ Analyze an Excel file using pandas and answer a question about it.
582
+ Args:
583
+ file_path (str): the path to the Excel file.
584
+ query (str): Question about the data
585
+ """
586
  logger.info("analyze_excel_file called with file_path=%s", file_path)
587
  try:
588
  df = pd.read_excel(file_path)
 
598
  # =========================
599
 
600
  @tool
601
+ def execute_code_multilang(code: str, language: str = "python") -> str:
602
+ """Execute code in multiple languages (Python, Bash, SQL, C, Java) and return results.
603
+ Args:
604
+ code (str): The source code to execute.
605
+ language (str): The language of the code. Supported: "python", "bash", "sql", "c", "java".
606
+ Returns:
607
+ A string summarizing the execution results (stdout, stderr, errors, plots, dataframes if any).
608
+ """
609
+ supported_languages = ["python", "bash", "sql", "c", "java"]
610
+ language = language.lower()
611
+
612
+ if language not in supported_languages:
613
+ return f"โŒ Unsupported language: {language}. Supported languages are: {', '.join(supported_languages)}"
614
+
615
+ result = interpreter_instance.execute_code(code, language=language)
616
+
617
+ response = []
618
+
619
+ if result["status"] == "success":
620
+ response.append(f"โœ… Code executed successfully in **{language.upper()}**")
621
+
622
+ if result.get("stdout"):
623
+ response.append(
624
+ "\n**Standard Output:**\n```\n" + result["stdout"].strip() + "\n```"
625
+ )
626
+
627
+ if result.get("stderr"):
628
+ response.append(
629
+ "\n**Standard Error (if any):**\n```\n"
630
+ + result["stderr"].strip()
631
+ + "\n```"
632
+ )
633
+
634
+ if result.get("result") is not None:
635
+ response.append(
636
+ "\n**Execution Result:**\n```\n"
637
+ + str(result["result"]).strip()
638
+ + "\n```"
639
+ )
640
+
641
+ if result.get("dataframes"):
642
+ for df_info in result["dataframes"]:
643
+ response.append(
644
+ f"\n**DataFrame `{df_info['name']}` (Shape: {df_info['shape']})**"
645
+ )
646
+ df_preview = pd.DataFrame(df_info["head"])
647
+ response.append("First 5 rows:\n```\n" + str(df_preview) + "\n```")
648
+
649
+ if result.get("plots"):
650
+ response.append(
651
+ f"\n**Generated {len(result['plots'])} plot(s)** (Image data returned separately)"
652
+ )
653
+
654
+ else:
655
+ response.append(f"โŒ Code execution failed in **{language.upper()}**")
656
+ if result.get("stderr"):
657
+ response.append(
658
+ "\n**Error Log:**\n```\n" + result["stderr"].strip() + "\n```"
659
+ )
660
+
661
+ return "\n".join(response)
662
 
663
  # =========================
664
  # ๐ŸŒ Search Tools
665
  # =========================
666
 
667
  @tool
668
+ def web_search(query: str) -> str:
669
+ """Search Tavily for a query and return maximum 3 results.
670
+ Args:
671
+ query: The search query."""
672
+ search_docs = TavilySearchResults(max_results=3).invoke(query)
673
+ formatted_search_docs = "\n\n---\n\n".join(
674
+ [
675
+ f'<Document source="{doc.get("url", "")}" title="{doc.get("title", "")}"/>\n{doc.get("content", "")}\n</Document>'
676
+ for doc in search_docs
677
+ ]
678
+ )
679
+ return {"web_results": formatted_search_docs}
680
 
681
 
682
  @tool
683
+ def wiki_search(query: str) -> str:
684
+ """Search Wikipedia for a query and return maximum 2 results.
685
+ Args:
686
+ query: The search query."""
687
+ search_docs = WikipediaLoader(query=query, load_max_docs=2).load()
688
+ formatted_search_docs = "\n\n---\n\n".join(
689
+ [
690
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
691
+ for doc in search_docs
692
+ ]
693
+ )
694
+ return {"wiki_results": formatted_search_docs}
695
 
696
 
697
  @tool
698
+ def arxiv_search(query: str) -> str:
699
+ """Search Arxiv for a query and return maximum 3 result.
700
+ Args:
701
+ query: The search query."""
702
+ search_docs = ArxivLoader(query=query, load_max_docs=3).load()
703
+ formatted_search_docs = "\n\n---\n\n".join(
704
+ [
705
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'
706
+ for doc in search_docs
707
+ ]
708
+ )
709
+ return {"arxiv_results": formatted_search_docs}
710
+
711
+
712
+
713
 
 
 
714
 
715
 
716
  # =========================
 
785
  draw_on_image,
786
  generate_simple_image,
787
  combine_images,
788
+ extract_text_from_audio,
789
  ]
790
 
791
 
 
793
  def build_graph(provider: str = "groq"):
794
  """Build the graph"""
795
  # Load environment variables from .env file
796
+ import time
797
+ import httpx
798
+ class ChatGroqWithRetry(ChatGroq):
799
+ def invoke(self, *args, **kwargs):
800
+ max_retries = 5
801
+ for attempt in range(max_retries):
802
+ try:
803
+ return super().invoke(*args, **kwargs)
804
+ except httpx.HTTPStatusError as e:
805
+ if e.response.status_code == 429:
806
+ wait = min(2 ** attempt, 30)
807
+ time.sleep(wait)
808
+ continue
809
+ raise
810
+ raise Exception("Groq API: Too Many Requests after retries.")
811
  if provider == "groq":
812
  # Groq https://console.groq.com/docs/models
813
+ llm = ChatGroqWithRetry(model="qwen/qwen3-32b", temperature=0)
814
  elif provider == "huggingface":
815
  # TODO: Add huggingface endpoint
816
  llm = ChatHuggingFace(