Luong Huu Thanh commited on
Commit
6b3dac4
Β·
1 Parent(s): a6b2731

Create agent.py

Browse files
Files changed (1) hide show
  1. agent.py +806 -0
agent.py ADDED
@@ -0,0 +1,806 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from typing import List, Dict, Any, Optional
4
+ import tempfile
5
+ import re
6
+ import json
7
+ import requests
8
+ from urllib.parse import urlparse
9
+ import pytesseract
10
+ from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter
11
+ import cmath
12
+ import pandas as pd
13
+ import uuid
14
+ import numpy as np
15
+ from code_interpreter import CodeInterpreter
16
+
17
+ interpreter_instance = CodeInterpreter()
18
+
19
+ from image_processing import *
20
+
21
+ """Langraph"""
22
+ from langgraph.graph import START, StateGraph, MessagesState
23
+ from langchain_community.tools.tavily_search import TavilySearchResults
24
+ from langchain_community.document_loaders import WikipediaLoader
25
+ from langchain_community.document_loaders import ArxivLoader
26
+ from langgraph.prebuilt import ToolNode, tools_condition
27
+ from langchain_google_genai import ChatGoogleGenerativeAI
28
+ from langchain_groq import ChatGroq
29
+ from langchain_huggingface import (
30
+ ChatHuggingFace,
31
+ HuggingFaceEndpoint,
32
+ HuggingFaceEmbeddings,
33
+ )
34
+ from langchain_community.vectorstores import SupabaseVectorStore
35
+ from langchain_core.messages import SystemMessage, HumanMessage
36
+ from langchain_core.tools import tool
37
+ from langchain.tools.retriever import create_retriever_tool
38
+ from supabase.client import Client, create_client
39
+
40
+ load_dotenv()
41
+
42
+ ### =============== BROWSER TOOLS =============== ###
43
+
44
+
45
+ @tool
46
+ def wiki_search(query: str) -> Dict:
47
+ """
48
+ Seach Wikipedia for a query and return maximum 2 results.
49
+
50
+ Args:
51
+ query (str): the search query
52
+ """
53
+ search_docs = WikipediaLoader(query=query, max_results=2).load()
54
+ formatted_search_docs = "\n\n---\n\n".join(
55
+ [
56
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
57
+ for doc in search_docs
58
+ ]
59
+ )
60
+ return {"wiki_results": formatted_search_docs}
61
+
62
+
63
+ @tool
64
+ def web_search(query: str) -> Dict:
65
+ """
66
+ Search the web for a query and return maximum 2 results.
67
+
68
+ Args:
69
+ query (str): the search query
70
+ """
71
+ search_docs = TavilySearchResults(query=query, max_results=2).load()
72
+ formatted_search_docs = "\n\n---\n\n".join(
73
+ [
74
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
75
+ for doc in search_docs
76
+ ]
77
+ )
78
+ return {"web_results": formatted_search_docs}
79
+
80
+
81
+ @tool
82
+ def arxiv_search(query: str) -> Dict:
83
+ """
84
+ Search arXiv for a query and return maximum 2 results.
85
+
86
+ Args:
87
+ query (str): the search query
88
+ """
89
+ search_docs = ArxivLoader(query=query, max_results=2).load()
90
+ formatted_search_docs = "\n\n---\n\n".join(
91
+ [
92
+ f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'
93
+ for doc in search_docs
94
+ ]
95
+ )
96
+ return {"arxiv_results": formatted_search_docs}
97
+
98
+
99
+ ### =============== CODE INTERPRETER TOOLS =============== ###
100
+
101
+
102
+ @tool
103
+ def execute_code_multilang(code: str, language: str = "python") -> str:
104
+ """Execute code in multiple languages (Python, Bash, SQL, C, Java) and return results.
105
+
106
+ Args:
107
+ code (str): The source code to execute.
108
+ language (str): The language of the code. Supported: "python", "bash", "sql", "c", "java".
109
+
110
+ Returns:
111
+ A string summarizing the execution results (stdout, stderr, errors, plots, dataframes if any).
112
+ """
113
+ supported_languages = ["python", "bash", "sql", "c", "java"]
114
+ language = language.lower()
115
+
116
+ if language not in supported_languages:
117
+ return f"❌ Unsupported language: {language}. Supported languages are: {', '.join(supported_languages)}"
118
+
119
+ result = interpreter_instance.execute_code(code, language=language)
120
+
121
+ response = []
122
+
123
+ if result["status"] == "success":
124
+ response.append(f"βœ… Code executed successfully in **{language.upper()}**")
125
+
126
+ if result.get("stdout"):
127
+ response.append(
128
+ "\n**Standard Output:**\n```\n" + result["stdout"].strip() + "\n```"
129
+ )
130
+
131
+ if result.get("stderr"):
132
+ response.append(
133
+ "\n**Standard Error (if any):**\n```\n"
134
+ + result["stderr"].strip()
135
+ + "\n```"
136
+ )
137
+
138
+ if result.get("result") is not None:
139
+ response.append(
140
+ "\n**Execution Result:**\n```\n"
141
+ + str(result["result"]).strip()
142
+ + "\n```"
143
+ )
144
+
145
+ if result.get("dataframes"):
146
+ for df_info in result["dataframes"]:
147
+ response.append(
148
+ f"\n**DataFrame `{df_info['name']}` (Shape: {df_info['shape']})**"
149
+ )
150
+ df_preview = pd.DataFrame(df_info["head"])
151
+ response.append("First 5 rows:\n```\n" + str(df_preview) + "\n```")
152
+
153
+ if result.get("plots"):
154
+ response.append(
155
+ f"\n**Generated {len(result['plots'])} plot(s)** (Image data returned separately)"
156
+ )
157
+
158
+ else:
159
+ response.append(f"❌ Code execution failed in **{language.upper()}**")
160
+ if result.get("stderr"):
161
+ response.append(
162
+ "\n**Error Log:**\n```\n" + result["stderr"].strip() + "\n```"
163
+ )
164
+
165
+ return "\n".join(response)
166
+
167
+
168
+ ### =============== MATHEMATICAL TOOLS =============== ###
169
+
170
+
171
+ @tool
172
+ def multiply(a: float, b: float) -> float:
173
+ """
174
+ Multiplies two numbers.
175
+
176
+ Args:
177
+ a (float): the first number
178
+ b (float): the second number
179
+ """
180
+ return a * b
181
+
182
+
183
+ @tool
184
+ def add(a: float, b: float) -> float:
185
+ """
186
+ Adds two numbers.
187
+
188
+ Args:
189
+ a (float): the first number
190
+ b (float): the second number
191
+ """
192
+ return a + b
193
+
194
+
195
+ @tool
196
+ def subtract(a: float, b: float) -> int:
197
+ """
198
+ Subtracts two numbers.
199
+
200
+ Args:
201
+ a (float): the first number
202
+ b (float): the second number
203
+ """
204
+ return a - b
205
+
206
+
207
+ @tool
208
+ def divide(a: float, b: float) -> float:
209
+ """
210
+ Divides two numbers.
211
+
212
+ Args:
213
+ a (float): the first float number
214
+ b (float): the second float number
215
+ """
216
+ if b == 0:
217
+ raise ValueError("Cannot divided by zero.")
218
+ return a / b
219
+
220
+
221
+ @tool
222
+ def modulus(a: int, b: int) -> int:
223
+ """
224
+ Get the modulus of two numbers.
225
+
226
+ Args:
227
+ a (int): the first number
228
+ b (int): the second number
229
+ """
230
+ return a % b
231
+
232
+
233
+ @tool
234
+ def power(a: float, b: float) -> float:
235
+ """
236
+ Get the power of two numbers.
237
+
238
+ Args:
239
+ a (float): the first number
240
+ b (float): the second number
241
+ """
242
+ return a**b
243
+
244
+
245
+ @tool
246
+ def square_root(a: float) -> float | complex:
247
+ """
248
+ Get the square root of a number.
249
+
250
+ Args:
251
+ a (float): the number to get the square root of
252
+ """
253
+ if a >= 0:
254
+ return a**0.5
255
+ return cmath.sqrt(a)
256
+
257
+
258
+ ### =============== DOCUMENT PROCESSING TOOLS =============== ###
259
+
260
+
261
+ @tool
262
+ def save_and_read_file(content: str, filename: Optional[str] = None) -> str:
263
+ """
264
+ Save content to a file and return the path.
265
+
266
+ Args:
267
+ content (str): the content to save to the file
268
+ filename (str, optional): the name of the file. If not provided, a random name file will be created.
269
+ """
270
+ temp_dir = tempfile.gettempdir()
271
+ if filename is None:
272
+ temp_file = tempfile.NamedTemporaryFile(delete=False, dir=temp_dir)
273
+ filepath = temp_file.name
274
+ else:
275
+ filepath = os.path.join(temp_dir, filename)
276
+
277
+ with open(filepath, "w") as f:
278
+ f.write(content)
279
+
280
+ return f"File saved to {filepath}. You can read this file to process its contents."
281
+
282
+
283
+ @tool
284
+ def download_file_from_url(url: str, filename: Optional[str] = None) -> str:
285
+ """
286
+ Download a file from a URL and save it to a temporary location.
287
+
288
+ Args:
289
+ url (str): the URL of the file to download.
290
+ filename (str, optional): the name of the file. If not provided, a random name file will be created.
291
+ """
292
+ try:
293
+ # Parse URL to get filename if not provided
294
+ if not filename:
295
+ path = urlparse(url).path
296
+ filename = os.path.basename(path)
297
+ if not filename:
298
+ filename = f"downloaded_{uuid.uuid4().hex[:8]}"
299
+
300
+ # Create temporary file
301
+ temp_dir = tempfile.gettempdir()
302
+ filepath = os.path.join(temp_dir, filename)
303
+
304
+ # Download the file
305
+ response = requests.get(url, stream=True)
306
+ response.raise_for_status()
307
+
308
+ # Save the file
309
+ with open(filepath, "wb") as f:
310
+ for chunk in response.iter_content(chunk_size=8192):
311
+ f.write(chunk)
312
+
313
+ return f"File downloaded to {filepath}. You can read this file to process its contents."
314
+ except Exception as e:
315
+ return f"Error downloading file: {str(e)}"
316
+
317
+
318
+ @tool
319
+ def extract_text_from_image(image_path: str) -> str:
320
+ """
321
+ Extract text from an image using OCR library pytesseract (if available).
322
+
323
+ Args:
324
+ image_path (str): the path to the image file.
325
+ """
326
+ try:
327
+ # Open the image
328
+ image = Image.open(image_path)
329
+
330
+ # Extract text from the image
331
+ text = pytesseract.image_to_string(image)
332
+
333
+ return f"Extracted text from image:\n\n{text}"
334
+ except Exception as e:
335
+ return f"Error extracting text from image: {str(e)}"
336
+
337
+
338
+ @tool
339
+ def analyze_csv_file(file_path: str, query: str) -> str:
340
+ """
341
+ Analyze a CSV file using pandas and answer a question about it.
342
+
343
+ Args:
344
+ file_path (str): the path to the CSV file.
345
+ query (str): Question about the data
346
+ """
347
+ try:
348
+ # Read the CSV file
349
+ df = pd.read_csv(file_path)
350
+
351
+ # Run various analyses based on the query
352
+ result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
353
+ result += f"Columns: {', '.join(df.columns)}\n\n"
354
+
355
+ # Add summary statistics
356
+ result += "Summary statistics:\n"
357
+ result += str(df.describe())
358
+
359
+ return result
360
+
361
+ except Exception as e:
362
+ return f"Error analyzing CSV file: {str(e)}"
363
+
364
+
365
+ @tool
366
+ def analyze_excel_file(file_path: str, query: str) -> str:
367
+ """
368
+ Analyze an Excel file using pandas and answer a question about it.
369
+
370
+ Args:
371
+ file_path (str): the path to the Excel file.
372
+ query (str): Question about the data
373
+ """
374
+ try:
375
+ # Read the Excel file
376
+ df = pd.read_excel(file_path)
377
+
378
+ # Run various analyses based on the query
379
+ result = (
380
+ f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"
381
+ )
382
+ result += f"Columns: {', '.join(df.columns)}\n\n"
383
+
384
+ # Add summary statistics
385
+ result += "Summary statistics:\n"
386
+ result += str(df.describe())
387
+
388
+ return result
389
+
390
+ except Exception as e:
391
+ return f"Error analyzing Excel file: {str(e)}"
392
+
393
+
394
+ ### ============== IMAGE PROCESSING AND GENERATION TOOLS =============== ###
395
+
396
+
397
+ @tool
398
+ def analyze_image(image_base64: str) -> Dict[str, Any]:
399
+ """
400
+ Analyze basic properties of an image (size, mode, color analysis, thumbnail preview).
401
+
402
+ Args:
403
+ image_base64 (str): Base64 encoded image string
404
+
405
+ Returns:
406
+ Dictionary with analysis result
407
+ """
408
+ try:
409
+ img = decode_image(image_base64)
410
+ width, height = img.size
411
+ mode = img.mode
412
+
413
+ if mode in ("RGB", "RGBA"):
414
+ arr = np.array(img)
415
+ avg_colors = arr.mean(axis=(0, 1))
416
+ dominant = ["Red", "Green", "Blue"][np.argmax(avg_colors[:3])]
417
+ brightness = avg_colors.mean()
418
+ color_analysis = {
419
+ "average_rgb": avg_colors.tolist(),
420
+ "brightness": brightness,
421
+ "dominant_color": dominant,
422
+ }
423
+ else:
424
+ color_analysis = {"note": f"No color analysis for mode {mode}"}
425
+
426
+ thumbnail = img.copy()
427
+ thumbnail.thumbnail((100, 100))
428
+ thumb_path = save_image(thumbnail, "thumbnails")
429
+ thumbnail_base64 = encode_image(thumb_path)
430
+
431
+ return {
432
+ "dimensions": (width, height),
433
+ "mode": mode,
434
+ "color_analysis": color_analysis,
435
+ "thumbnail": thumbnail_base64,
436
+ }
437
+ except Exception as e:
438
+ return {"error": str(e)}
439
+
440
+
441
+ @tool
442
+ def transform_image(
443
+ image_base64: str, operation: str, params: Optional[Dict[str, Any]] = None
444
+ ) -> Dict[str, Any]:
445
+ """
446
+ Apply transformations: resize, rotate, crop, flip, brightness, contrast, blur, sharpen, grayscale.
447
+
448
+ Args:
449
+ image_base64 (str): Base64 encoded input image
450
+ operation (str): Transformation operation
451
+ params (Dict[str, Any], optional): Parameters for the operation
452
+
453
+ Returns:
454
+ Dictionary with transformed image (base64)
455
+ """
456
+ try:
457
+ img = decode_image(image_base64)
458
+ params = params or {}
459
+
460
+ if operation == "resize":
461
+ img = img.resize(
462
+ (
463
+ params.get("width", img.width // 2),
464
+ params.get("height", img.height // 2),
465
+ )
466
+ )
467
+ elif operation == "rotate":
468
+ img = img.rotate(params.get("angle", 90), expand=True)
469
+ elif operation == "crop":
470
+ img = img.crop(
471
+ (
472
+ params.get("left", 0),
473
+ params.get("top", 0),
474
+ params.get("right", img.width),
475
+ params.get("bottom", img.height),
476
+ )
477
+ )
478
+ elif operation == "flip":
479
+ if params.get("direction", "horizontal") == "horizontal":
480
+ img = img.transpose(Image.FLIP_LEFT_RIGHT)
481
+ else:
482
+ img = img.transpose(Image.FLIP_TOP_BOTTOM)
483
+ elif operation == "adjust_brightness":
484
+ img = ImageEnhance.Brightness(img).enhance(params.get("factor", 1.5))
485
+ elif operation == "adjust_contrast":
486
+ img = ImageEnhance.Contrast(img).enhance(params.get("factor", 1.5))
487
+ elif operation == "blur":
488
+ img = img.filter(ImageFilter.GaussianBlur(params.get("radius", 2)))
489
+ elif operation == "sharpen":
490
+ img = img.filter(ImageFilter.SHARPEN)
491
+ elif operation == "grayscale":
492
+ img = img.convert("L")
493
+ else:
494
+ return {"error": f"Unknown operation: {operation}"}
495
+
496
+ result_path = save_image(img)
497
+ result_base64 = encode_image(result_path)
498
+ return {"transformed_image": result_base64}
499
+
500
+ except Exception as e:
501
+ return {"error": str(e)}
502
+
503
+
504
+ @tool
505
+ def draw_on_image(
506
+ image_base64: str, drawing_type: str, params: Dict[str, Any]
507
+ ) -> Dict[str, Any]:
508
+ """
509
+ Draw shapes (rectangle, circle, line) or text onto an image.
510
+
511
+ Args:
512
+ image_base64 (str): Base64 encoded input image
513
+ drawing_type (str): Drawing type
514
+ params (Dict[str, Any]): Drawing parameters
515
+
516
+ Returns:
517
+ Dictionary with result image (base64)
518
+ """
519
+ try:
520
+ img = decode_image(image_base64)
521
+ draw = ImageDraw.Draw(img)
522
+ color = params.get("color", "red")
523
+
524
+ if drawing_type == "rectangle":
525
+ draw.rectangle(
526
+ [params["left"], params["top"], params["right"], params["bottom"]],
527
+ outline=color,
528
+ width=params.get("width", 2),
529
+ )
530
+ elif drawing_type == "circle":
531
+ x, y, r = params["x"], params["y"], params["radius"]
532
+ draw.ellipse(
533
+ (x - r, y - r, x + r, y + r),
534
+ outline=color,
535
+ width=params.get("width", 2),
536
+ )
537
+ elif drawing_type == "line":
538
+ draw.line(
539
+ (
540
+ params["start_x"],
541
+ params["start_y"],
542
+ params["end_x"],
543
+ params["end_y"],
544
+ ),
545
+ fill=color,
546
+ width=params.get("width", 2),
547
+ )
548
+ elif drawing_type == "text":
549
+ font_size = params.get("font_size", 20)
550
+ try:
551
+ font = ImageFont.truetype("arial.ttf", font_size)
552
+ except IOError:
553
+ font = ImageFont.load_default()
554
+ draw.text(
555
+ (params["x"], params["y"]),
556
+ params.get("text", "Text"),
557
+ fill=color,
558
+ font=font,
559
+ )
560
+ else:
561
+ return {"error": f"Unknown drawing type: {drawing_type}"}
562
+
563
+ result_path = save_image(img)
564
+ result_base64 = encode_image(result_path)
565
+ return {"result_image": result_base64}
566
+
567
+ except Exception as e:
568
+ return {"error": str(e)}
569
+
570
+
571
+ @tool
572
+ def generate_simple_image(
573
+ image_type: str,
574
+ width: int = 500,
575
+ height: int = 500,
576
+ params: Optional[Dict[str, Any]] = None,
577
+ ) -> Dict[str, Any]:
578
+ """
579
+ Generate a simple image (gradient, noise, pattern, chart).
580
+
581
+ Args:
582
+ image_type (str): Type of image
583
+ width (int), height (int)
584
+ params (Dict[str, Any], optional): Specific parameters
585
+
586
+ Returns:
587
+ Dictionary with generated image (base64)
588
+ """
589
+ try:
590
+ params = params or {}
591
+
592
+ if image_type == "gradient":
593
+ direction = params.get("direction", "horizontal")
594
+ start_color = params.get("start_color", (255, 0, 0))
595
+ end_color = params.get("end_color", (0, 0, 255))
596
+
597
+ img = Image.new("RGB", (width, height))
598
+ draw = ImageDraw.Draw(img)
599
+
600
+ if direction == "horizontal":
601
+ for x in range(width):
602
+ r = int(
603
+ start_color[0] + (end_color[0] - start_color[0]) * x / width
604
+ )
605
+ g = int(
606
+ start_color[1] + (end_color[1] - start_color[1]) * x / width
607
+ )
608
+ b = int(
609
+ start_color[2] + (end_color[2] - start_color[2]) * x / width
610
+ )
611
+ draw.line([(x, 0), (x, height)], fill=(r, g, b))
612
+ else:
613
+ for y in range(height):
614
+ r = int(
615
+ start_color[0] + (end_color[0] - start_color[0]) * y / height
616
+ )
617
+ g = int(
618
+ start_color[1] + (end_color[1] - start_color[1]) * y / height
619
+ )
620
+ b = int(
621
+ start_color[2] + (end_color[2] - start_color[2]) * y / height
622
+ )
623
+ draw.line([(0, y), (width, y)], fill=(r, g, b))
624
+
625
+ elif image_type == "noise":
626
+ noise_array = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8)
627
+ img = Image.fromarray(noise_array, "RGB")
628
+
629
+ else:
630
+ return {"error": f"Unsupported image_type {image_type}"}
631
+
632
+ result_path = save_image(img)
633
+ result_base64 = encode_image(result_path)
634
+ return {"generated_image": result_base64}
635
+
636
+ except Exception as e:
637
+ return {"error": str(e)}
638
+
639
+
640
+ @tool
641
+ def combine_images(
642
+ images_base64: List[str], operation: str, params: Optional[Dict[str, Any]] = None
643
+ ) -> Dict[str, Any]:
644
+ """
645
+ Combine multiple images (collage, stack, blend).
646
+
647
+ Args:
648
+ images_base64 (List[str]): List of base64 images
649
+ operation (str): Combination type
650
+ params (Dict[str, Any], optional)
651
+
652
+ Returns:
653
+ Dictionary with combined image (base64)
654
+ """
655
+ try:
656
+ images = [decode_image(b64) for b64 in images_base64]
657
+ params = params or {}
658
+
659
+ if operation == "stack":
660
+ direction = params.get("direction", "horizontal")
661
+ if direction == "horizontal":
662
+ total_width = sum(img.width for img in images)
663
+ max_height = max(img.height for img in images)
664
+ new_img = Image.new("RGB", (total_width, max_height))
665
+ x = 0
666
+ for img in images:
667
+ new_img.paste(img, (x, 0))
668
+ x += img.width
669
+ else:
670
+ max_width = max(img.width for img in images)
671
+ total_height = sum(img.height for img in images)
672
+ new_img = Image.new("RGB", (max_width, total_height))
673
+ y = 0
674
+ for img in images:
675
+ new_img.paste(img, (0, y))
676
+ y += img.height
677
+ else:
678
+ return {"error": f"Unsupported combination operation {operation}"}
679
+
680
+ result_path = save_image(new_img)
681
+ result_base64 = encode_image(result_path)
682
+ return {"combined_image": result_base64}
683
+
684
+ except Exception as e:
685
+ return {"error": str(e)}
686
+
687
+
688
+ # load the system prompt from the file
689
+ with open("system_prompt.txt", "r", encoding="utf-8") as f:
690
+ system_prompt = f.read()
691
+ print(system_prompt)
692
+
693
+ # System message
694
+ sys_msg = SystemMessage(content=system_prompt)
695
+
696
+ # build a retriever
697
+ embeddings = HuggingFaceEmbeddings(
698
+ model_name="sentence-transformers/all-mpnet-base-v2"
699
+ ) # dim=768
700
+ supabase: Client = create_client(
701
+ os.environ.get("SUPABASE_URL"), os.environ.get("SUPABASE_SERVICE_KEY")
702
+ )
703
+ vector_store = SupabaseVectorStore(
704
+ client=supabase,
705
+ embedding=embeddings,
706
+ table_name="documents2",
707
+ query_name="match_documents_2",
708
+ )
709
+ create_retriever_tool = create_retriever_tool(
710
+ retriever=vector_store.as_retriever(),
711
+ name="Question Search",
712
+ description="A tool to retrieve similar questions from a vector store.",
713
+ )
714
+
715
+
716
+ tools = [
717
+ web_search,
718
+ wiki_search,
719
+ arxiv_search,
720
+ multiply,
721
+ add,
722
+ subtract,
723
+ divide,
724
+ modulus,
725
+ power,
726
+ square_root,
727
+ save_and_read_file,
728
+ download_file_from_url,
729
+ extract_text_from_image,
730
+ analyze_csv_file,
731
+ analyze_excel_file,
732
+ execute_code_multilang,
733
+ analyze_image,
734
+ transform_image,
735
+ draw_on_image,
736
+ generate_simple_image,
737
+ combine_images,
738
+ ]
739
+
740
+ # Build graph function
741
+ def build_graph(provider: str = "groq"):
742
+ """Build the graph"""
743
+ # Load environment variables from .env file
744
+ if provider == "groq":
745
+ # Groq https://console.groq.com/docs/models
746
+ llm = ChatGroq(model="meta-llama/llama-4-scout-17b-16e-instruct", temperature=0)
747
+ elif provider == "huggingface":
748
+ # TODO: Add huggingface endpoint
749
+ llm = ChatHuggingFace(
750
+ llm=HuggingFaceEndpoint(
751
+ repo_id="meta-llama/Llama-2-7b-chat-hf",
752
+ task="text-generation", # for chat‐style use β€œtext-generation”
753
+ max_new_tokens=1024,
754
+ do_sample=False,
755
+ repetition_penalty=1.03,
756
+ temperature=0,
757
+ ),
758
+ verbose=True,
759
+ )
760
+ else:
761
+ raise ValueError("Invalid provider. Choose 'google', 'groq' or 'huggingface'.")
762
+ # Bind tools to LLM
763
+ llm_with_tools = llm.bind_tools(tools)
764
+
765
+ # Node
766
+ def assistant(state: MessagesState):
767
+ """Assistant node"""
768
+ return {"messages": [llm_with_tools.invoke(state["messages"])]}
769
+
770
+ def retriever(state: MessagesState):
771
+ """Retriever node"""
772
+ similar_question = vector_store.similarity_search(state["messages"][0].content)
773
+
774
+ if similar_question: # Check if the list is not empty
775
+ example_msg = HumanMessage(
776
+ content=f"Here I provide a similar question and answer for reference: \n\n{similar_question[0].page_content}",
777
+ )
778
+ return {"messages": [sys_msg] + state["messages"] + [example_msg]}
779
+ else:
780
+ # Handle the case when no similar questions are found
781
+ return {"messages": [sys_msg] + state["messages"]}
782
+
783
+ builder = StateGraph(MessagesState)
784
+ builder.add_node("retriever", retriever)
785
+ builder.add_node("assistant", assistant)
786
+ builder.add_node("tools", ToolNode(tools))
787
+ builder.add_edge(START, "retriever")
788
+ builder.add_edge("retriever", "assistant")
789
+ builder.add_conditional_edges(
790
+ "assistant",
791
+ tools_condition,
792
+ )
793
+ builder.add_edge("tools", "assistant")
794
+
795
+ # Compile graph
796
+ return builder.compile()
797
+
798
+
799
+ # test
800
+ if __name__ == "__main__":
801
+ question = "Generate gradient image with red and blue colors"
802
+ graph = build_graph(provider="groq")
803
+ messages = [HumanMessage(content=question)]
804
+ messages = graph.invoke({"messages": messages})
805
+ for m in messages["messages"]:
806
+ m.pretty_print()