CoolFace
Apppublic

ShawnIL/GAIA_Agent_Evaluation

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
agent.py1000 linesDownload Raw Back to root
1import os2from dotenv import load_dotenv3from typing import List, Dict, Any, Optional4import tempfile5import re6import json7import requests8from urllib.parse import urlparse9import pytesseract10from PIL import Image, ImageDraw, ImageFont, ImageEnhance, ImageFilter11import cmath12import pandas as pd13import uuid14import numpy as np15from code_interpreter import CodeInterpreter16 17interpreter_instance = CodeInterpreter()18 19from image_processing import *20 21"""Langraph"""22from langgraph.graph import START, StateGraph, MessagesState23from langchain_community.tools.tavily_search import TavilySearchResults24from langchain_openai import ChatOpenAI25from langchain_community.document_loaders import WikipediaLoader26from langchain_community.document_loaders import ArxivLoader27from langgraph.prebuilt import ToolNode, tools_condition28from langchain_google_genai import ChatGoogleGenerativeAI29from langchain_groq import ChatGroq30from langchain_huggingface import (31    ChatHuggingFace,32    HuggingFaceEndpoint,33    HuggingFaceEmbeddings,34)35from langchain_community.vectorstores import SupabaseVectorStore36from langchain_core.messages import SystemMessage, HumanMessage37from langchain_core.tools import tool, create_retriever_tool38from supabase.client import Client, create_client39 40"""Langfuse (optional)"""41try:42    from langfuse import Langfuse43    from langfuse.callback import CallbackHandler as LangfuseCallbackHandler44except Exception:45    Langfuse = None46    LangfuseCallbackHandler = None47 48load_dotenv()49 50 51# Optional Langfuse callback handler52def get_langfuse_handler():53    """Return a Langfuse callback handler if env vars are available; else None.54    Expected envs:55      - LANGFUSE_HOST56      - LANGFUSE_PUBLIC_KEY57      - LANGFUSE_SECRET_KEY58    """59    if Langfuse is None or LangfuseCallbackHandler is None:60        return None61    host = os.getenv("LANGFUSE_HOST")62    public = os.getenv("LANGFUSE_PUBLIC_KEY")63    secret = os.getenv("LANGFUSE_SECRET_KEY")64    try:65        # Langfuse SDK can read envs; pass explicitly for clarity66        lf = Langfuse(host=host, public_key=public, secret_key=secret)67        return LangfuseCallbackHandler(lf)68    except Exception:69        return None70 71 72### =============== BROWSER TOOLS =============== ###73 74 75@tool76def wiki_search(query: str) -> str:77    """Search Wikipedia for a query and return maximum 2 results.78    Args:79        query: The search query."""80    search_docs = WikipediaLoader(query=query, load_max_docs=2).load()81    formatted_search_docs = "\n\n---\n\n".join(82        [83            f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content}\n</Document>'84            for doc in search_docs85        ]86    )87    return {"wiki_results": formatted_search_docs}88 89 90@tool91def web_search(query: str) -> str:92    """Search Tavily for a query and return maximum 3 results.93    Args:94        query: The search query."""95    search_docs = TavilySearchResults(max_results=3).invoke(query)96    formatted_search_docs = "\n\n---\n\n".join(97        [98            f'<Document source="{doc.get("url", "")}" title="{doc.get("title", "")}"/>\n{doc.get("content", "")}\n</Document>'99            for doc in search_docs100        ]101    )102    return {"web_results": formatted_search_docs}103 104 105@tool106def arxiv_search(query: str) -> str:107    """Search Arxiv for a query and return maximum 3 result.108    Args:109        query: The search query."""110    search_docs = ArxivLoader(query=query, load_max_docs=3).load()111    formatted_search_docs = "\n\n---\n\n".join(112        [113            f'<Document source="{doc.metadata["source"]}" page="{doc.metadata.get("page", "")}"/>\n{doc.page_content[:1000]}\n</Document>'114            for doc in search_docs115        ]116    )117    return {"arxiv_results": formatted_search_docs}118 119 120### =============== CODE INTERPRETER TOOLS =============== ###121 122 123@tool124def execute_code_multilang(code: str, language: str = "python") -> str:125    """Execute code in multiple languages (Python, Bash, SQL, C, Java) and return results.126    Args:127        code (str): The source code to execute.128        language (str): The language of the code. Supported: "python", "bash", "sql", "c", "java".129    Returns:130        A string summarizing the execution results (stdout, stderr, errors, plots, dataframes if any).131    """132    supported_languages = ["python", "bash", "sql", "c", "java"]133    language = language.lower()134 135    if language not in supported_languages:136        return f"❌ Unsupported language: {language}. Supported languages are: {', '.join(supported_languages)}"137 138    result = interpreter_instance.execute_code(code, language=language)139 140    response = []141 142    if result["status"] == "success":143        response.append(f"✅ Code executed successfully in **{language.upper()}**")144 145        if result.get("stdout"):146            response.append(147                "\n**Standard Output:**\n```\n" + result["stdout"].strip() + "\n```"148            )149 150        if result.get("stderr"):151            response.append(152                "\n**Standard Error (if any):**\n```\n"153                + result["stderr"].strip()154                + "\n```"155            )156 157        if result.get("result") is not None:158            response.append(159                "\n**Execution Result:**\n```\n"160                + str(result["result"]).strip()161                + "\n```"162            )163 164        if result.get("dataframes"):165            for df_info in result["dataframes"]:166                response.append(167                    f"\n**DataFrame `{df_info['name']}` (Shape: {df_info['shape']})**"168                )169                df_preview = pd.DataFrame(df_info["head"])170                response.append("First 5 rows:\n```\n" + str(df_preview) + "\n```")171 172        if result.get("plots"):173            response.append(174                f"\n**Generated {len(result['plots'])} plot(s)** (Image data returned separately)"175            )176 177    else:178        response.append(f"❌ Code execution failed in **{language.upper()}**")179        if result.get("stderr"):180            response.append(181                "\n**Error Log:**\n```\n" + result["stderr"].strip() + "\n```"182            )183 184    return "\n".join(response)185 186 187### =============== MATHEMATICAL TOOLS =============== ###188 189 190@tool191def multiply(a: float, b: float) -> float:192    """193    Multiplies two numbers.194    Args:195        a (float): the first number196        b (float): the second number197    """198    return a * b199 200 201@tool202def add(a: float, b: float) -> float:203    """204    Adds two numbers.205    Args:206        a (float): the first number207        b (float): the second number208    """209    return a + b210 211 212@tool213def subtract(a: float, b: float) -> int:214    """215    Subtracts two numbers.216    Args:217        a (float): the first number218        b (float): the second number219    """220    return a - b221 222 223@tool224def divide(a: float, b: float) -> float:225    """226    Divides two numbers.227    Args:228        a (float): the first float number229        b (float): the second float number230    """231    if b == 0:232        raise ValueError("Cannot divided by zero.")233    return a / b234 235 236@tool237def modulus(a: int, b: int) -> int:238    """239    Get the modulus of two numbers.240    Args:241        a (int): the first number242        b (int): the second number243    """244    return a % b245 246 247@tool248def power(a: float, b: float) -> float:249    """250    Get the power of two numbers.251    Args:252        a (float): the first number253        b (float): the second number254    """255    return a**b256 257 258@tool259def square_root(a: float) -> float | complex:260    """261    Get the square root of a number.262    Args:263        a (float): the number to get the square root of264    """265    if a >= 0:266        return a**0.5267    return cmath.sqrt(a)268 269 270### =============== DOCUMENT PROCESSING TOOLS =============== ###271 272 273@tool274def save_and_read_file(content: str, filename: Optional[str] = None) -> str:275    """276    Save content to a file and return the path.277    Args:278        content (str): the content to save to the file279        filename (str, optional): the name of the file. If not provided, a random name file will be created.280    """281    temp_dir = tempfile.gettempdir()282    if filename is None:283        temp_file = tempfile.NamedTemporaryFile(delete=False, dir=temp_dir)284        filepath = temp_file.name285    else:286        filepath = os.path.join(temp_dir, filename)287 288    with open(filepath, "w") as f:289        f.write(content)290 291    return f"File saved to {filepath}. You can read this file to process its contents."292 293 294@tool295def download_file_from_url(url: str, filename: Optional[str] = None) -> str:296    """297    Download a file from a URL and save it to a temporary location.298    Args:299        url (str): the URL of the file to download.300        filename (str, optional): the name of the file. If not provided, a random name file will be created.301    """302    try:303        # Parse URL to get filename if not provided304        if not filename:305            path = urlparse(url).path306            filename = os.path.basename(path)307            if not filename:308                filename = f"downloaded_{uuid.uuid4().hex[:8]}"309 310        # Create temporary file311        temp_dir = tempfile.gettempdir()312        filepath = os.path.join(temp_dir, filename)313 314        # Download the file315        response = requests.get(url, stream=True)316        response.raise_for_status()317 318        # Save the file319        with open(filepath, "wb") as f:320            for chunk in response.iter_content(chunk_size=8192):321                f.write(chunk)322 323        return f"File downloaded to {filepath}. You can read this file to process its contents."324    except Exception as e:325        return f"Error downloading file: {str(e)}"326 327 328@tool329def extract_text_from_image(image_path: str) -> str:330    """331    Extract text from an image using OCR library pytesseract (if available).332    Args:333        image_path (str): the path to the image file.334    """335    try:336        # Open the image337        image = Image.open(image_path)338 339        # Extract text from the image340        text = pytesseract.image_to_string(image)341 342        return f"Extracted text from image:\n\n{text}"343    except Exception as e:344        return f"Error extracting text from image: {str(e)}"345 346 347@tool348def analyze_csv_file(file_path: str, query: str) -> str:349    """350    Analyze a CSV file using pandas and answer a question about it.351    Args:352        file_path (str): the path to the CSV file.353        query (str): Question about the data354    """355    try:356        # Read the CSV file357        df = pd.read_csv(file_path)358 359        # Run various analyses based on the query360        result = f"CSV file loaded with {len(df)} rows and {len(df.columns)} columns.\n"361        result += f"Columns: {', '.join(df.columns)}\n\n"362 363        # Add summary statistics364        result += "Summary statistics:\n"365        result += str(df.describe())366 367        return result368 369    except Exception as e:370        return f"Error analyzing CSV file: {str(e)}"371 372 373@tool374def analyze_excel_file(file_path: str, query: str) -> str:375    """376    Analyze an Excel file using pandas and answer a question about it.377    Args:378        file_path (str): the path to the Excel file.379        query (str): Question about the data380    """381    try:382        # Read the Excel file383        df = pd.read_excel(file_path)384 385        # Run various analyses based on the query386        result = (387            f"Excel file loaded with {len(df)} rows and {len(df.columns)} columns.\n"388        )389        result += f"Columns: {', '.join(df.columns)}\n\n"390 391        # Add summary statistics392        result += "Summary statistics:\n"393        result += str(df.describe())394 395        return result396 397    except Exception as e:398        return f"Error analyzing Excel file: {str(e)}"399 400 401### ============== IMAGE PROCESSING AND GENERATION TOOLS =============== ###402 403 404@tool405def analyze_image(image_base64: str) -> Dict[str, Any]:406    """407    Analyze basic properties of an image (size, mode, color analysis, thumbnail preview).408    Args:409        image_base64 (str): Base64 encoded image string410    Returns:411        Dictionary with analysis result412    """413    try:414        img = decode_image(image_base64)415        width, height = img.size416        mode = img.mode417 418        if mode in ("RGB", "RGBA"):419            arr = np.array(img)420            avg_colors = arr.mean(axis=(0, 1))421            dominant = ["Red", "Green", "Blue"][np.argmax(avg_colors[:3])]422            brightness = avg_colors.mean()423            color_analysis = {424                "average_rgb": avg_colors.tolist(),425                "brightness": brightness,426                "dominant_color": dominant,427            }428        else:429            color_analysis = {"note": f"No color analysis for mode {mode}"}430 431        thumbnail = img.copy()432        thumbnail.thumbnail((100, 100))433        thumb_path = save_image(thumbnail, "thumbnails")434        thumbnail_base64 = encode_image(thumb_path)435 436        return {437            "dimensions": (width, height),438            "mode": mode,439            "color_analysis": color_analysis,440            "thumbnail": thumbnail_base64,441        }442    except Exception as e:443        return {"error": str(e)}444 445 446@tool447def transform_image(448    image_base64: str, operation: str, params: Optional[Dict[str, Any]] = None449) -> Dict[str, Any]:450    """451    Apply transformations: resize, rotate, crop, flip, brightness, contrast, blur, sharpen, grayscale.452    Args:453        image_base64 (str): Base64 encoded input image454        operation (str): Transformation operation455        params (Dict[str, Any], optional): Parameters for the operation456    Returns:457        Dictionary with transformed image (base64)458    """459    try:460        img = decode_image(image_base64)461        params = params or {}462 463        if operation == "resize":464            img = img.resize(465                (466                    params.get("width", img.width // 2),467                    params.get("height", img.height // 2),468                )469            )470        elif operation == "rotate":471            img = img.rotate(params.get("angle", 90), expand=True)472        elif operation == "crop":473            img = img.crop(474                (475                    params.get("left", 0),476                    params.get("top", 0),477                    params.get("right", img.width),478                    params.get("bottom", img.height),479                )480            )481        elif operation == "flip":482            if params.get("direction", "horizontal") == "horizontal":483                img = img.transpose(Image.FLIP_LEFT_RIGHT)484            else:485                img = img.transpose(Image.FLIP_TOP_BOTTOM)486        elif operation == "adjust_brightness":487            img = ImageEnhance.Brightness(img).enhance(params.get("factor", 1.5))488        elif operation == "adjust_contrast":489            img = ImageEnhance.Contrast(img).enhance(params.get("factor", 1.5))490        elif operation == "blur":491            img = img.filter(ImageFilter.GaussianBlur(params.get("radius", 2)))492        elif operation == "sharpen":493            img = img.filter(ImageFilter.SHARPEN)494        elif operation == "grayscale":495            img = img.convert("L")496        else:497            return {"error": f"Unknown operation: {operation}"}498 499        result_path = save_image(img)500        result_base64 = encode_image(result_path)501        return {"transformed_image": result_base64}502 503    except Exception as e:504        return {"error": str(e)}505 506 507@tool508def draw_on_image(509    image_base64: str, drawing_type: str, params: Dict[str, Any]510) -> Dict[str, Any]:511    """512    Draw shapes (rectangle, circle, line) or text onto an image.513    Args:514        image_base64 (str): Base64 encoded input image515        drawing_type (str): Drawing type516        params (Dict[str, Any]): Drawing parameters517    Returns:518        Dictionary with result image (base64)519    """520    try:521        img = decode_image(image_base64)522        draw = ImageDraw.Draw(img)523        color = params.get("color", "red")524 525        if drawing_type == "rectangle":526            draw.rectangle(527                [params["left"], params["top"], params["right"], params["bottom"]],528                outline=color,529                width=params.get("width", 2),530            )531        elif drawing_type == "circle":532            x, y, r = params["x"], params["y"], params["radius"]533            draw.ellipse(534                (x - r, y - r, x + r, y + r),535                outline=color,536                width=params.get("width", 2),537            )538        elif drawing_type == "line":539            draw.line(540                (541                    params["start_x"],542                    params["start_y"],543                    params["end_x"],544                    params["end_y"],545                ),546                fill=color,547                width=params.get("width", 2),548            )549        elif drawing_type == "text":550            font_size = params.get("font_size", 20)551            try:552                font = ImageFont.truetype("arial.ttf", font_size)553            except IOError:554                font = ImageFont.load_default()555            draw.text(556                (params["x"], params["y"]),557                params.get("text", "Text"),558                fill=color,559                font=font,560            )561        else:562            return {"error": f"Unknown drawing type: {drawing_type}"}563 564        result_path = save_image(img)565        result_base64 = encode_image(result_path)566        return {"result_image": result_base64}567 568    except Exception as e:569        return {"error": str(e)}570 571 572@tool573def generate_simple_image(574    image_type: str,575    width: int = 500,576    height: int = 500,577    params: Optional[Dict[str, Any]] = None,578) -> Dict[str, Any]:579    """580    Generate a simple image (gradient, noise, pattern, chart).581    Args:582        image_type (str): Type of image583        width (int), height (int)584        params (Dict[str, Any], optional): Specific parameters585    Returns:586        Dictionary with generated image (base64)587    """588    try:589        params = params or {}590 591        if image_type == "gradient":592            direction = params.get("direction", "horizontal")593            start_color = params.get("start_color", (255, 0, 0))594            end_color = params.get("end_color", (0, 0, 255))595 596            img = Image.new("RGB", (width, height))597            draw = ImageDraw.Draw(img)598 599            if direction == "horizontal":600                for x in range(width):601                    r = int(602                        start_color[0] + (end_color[0] - start_color[0]) * x / width603                    )604                    g = int(605                        start_color[1] + (end_color[1] - start_color[1]) * x / width606                    )607                    b = int(608                        start_color[2] + (end_color[2] - start_color[2]) * x / width609                    )610                    draw.line([(x, 0), (x, height)], fill=(r, g, b))611            else:612                for y in range(height):613                    r = int(614                        start_color[0] + (end_color[0] - start_color[0]) * y / height615                    )616                    g = int(617                        start_color[1] + (end_color[1] - start_color[1]) * y / height618                    )619                    b = int(620                        start_color[2] + (end_color[2] - start_color[2]) * y / height621                    )622                    draw.line([(0, y), (width, y)], fill=(r, g, b))623 624        elif image_type == "noise":625            noise_array = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8)626            img = Image.fromarray(noise_array, "RGB")627 628        else:629            return {"error": f"Unsupported image_type {image_type}"}630 631        result_path = save_image(img)632        result_base64 = encode_image(result_path)633        return {"generated_image": result_base64}634 635    except Exception as e:636        return {"error": str(e)}637 638 639@tool640def combine_images(641    images_base64: List[str], operation: str, params: Optional[Dict[str, Any]] = None642) -> Dict[str, Any]:643    """644    Combine multiple images (collage, stack, blend).645    Args:646        images_base64 (List[str]): List of base64 images647        operation (str): Combination type648        params (Dict[str, Any], optional)649    Returns:650        Dictionary with combined image (base64)651    """652    try:653        images = [decode_image(b64) for b64 in images_base64]654        params = params or {}655 656        if operation == "stack":657            direction = params.get("direction", "horizontal")658            if direction == "horizontal":659                total_width = sum(img.width for img in images)660                max_height = max(img.height for img in images)661                new_img = Image.new("RGB", (total_width, max_height))662                x = 0663                for img in images:664                    new_img.paste(img, (x, 0))665                    x += img.width666            else:667                max_width = max(img.width for img in images)668                total_height = sum(img.height for img in images)669                new_img = Image.new("RGB", (max_width, total_height))670                y = 0671                for img in images:672                    new_img.paste(img, (0, y))673                    y += img.height674        else:675            return {"error": f"Unsupported combination operation {operation}"}676 677        result_path = save_image(new_img)678        result_base64 = encode_image(result_path)679        return {"combined_image": result_base64}680 681    except Exception as e:682        return {"error": str(e)}683 684 685# load the system prompt from the file686with open("system_prompt.txt", "r", encoding="utf-8") as f:687    system_prompt = f.read()688print(system_prompt)689 690# System message691sys_msg = SystemMessage(content=system_prompt)692 693# build a retriever694embeddings = HuggingFaceEmbeddings(695    model_name="sentence-transformers/all-mpnet-base-v2"696)  #  dim=768697supabase: Client = create_client(698    os.environ.get("SUPABASE_URL"), os.environ.get("SUPABASE_SERVICE_ROLE_KEY")699)700vector_store = SupabaseVectorStore(701    client=supabase,702    embedding=embeddings,703    table_name="documents2",704    query_name="match_documents_2",705)706create_retriever_tool = create_retriever_tool(707    retriever=vector_store.as_retriever(),708    name="Question Search",709    description="A tool to retrieve similar questions from a vector store.",710)711 712 713tools = [714    web_search,715    wiki_search,716    arxiv_search,717    multiply,718    add,719    subtract,720    divide,721    modulus,722    power,723    square_root,724    save_and_read_file,725    download_file_from_url,726    extract_text_from_image,727    analyze_csv_file,728    analyze_excel_file,729    execute_code_multilang,730    analyze_image,731    transform_image,732    draw_on_image,733    generate_simple_image,734    combine_images,735]736 737 738# Supabase RPC-based similarity search (compatible with newer supabase-py)739def supabase_similarity_search(query: str, k: int = 3) -> List[Dict[str, Any]]:740    try:741        query_embedding = embeddings.embed_query(query)742        # Call the SQL function via RPC to retrieve similar documents743        resp = supabase.rpc(744            "match_documents_2",745            {"query_embedding": query_embedding, "match_count": k},746        ).execute()747        data = getattr(resp, "data", None)748        if not data:749            return []750        # Ensure list of dicts with at least 'content'751        return data752    except Exception:753        return []754 755 756# Build graph function757def build_graph(provider: str = "groq"):758    """Build the graph"""759    # Optional observability via Langfuse760    lf_handler = get_langfuse_handler()761    if provider == "groq":762        # Groq https://console.groq.com/docs/models763        llm = ChatGroq(764            model="qwen/qwen3-32b",765            temperature=0,766            callbacks=[lf_handler] if lf_handler else None,767        )768    elif provider == "openai":769        # OpenAI GPT models770        llm = ChatOpenAI(771            model="gpt-3.5-turbo",772            temperature=0,773            callbacks=[lf_handler] if lf_handler else None,774        )775    else:776        raise ValueError("Invalid provider. Choose 'groq' or 'openai'.")777    # Bind tools to LLM778    llm_with_tools = llm.bind_tools(tools)779 780    # Node781    def assistant(state: MessagesState):782        """Assistant node"""783        return {"messages": [llm_with_tools.invoke(state["messages"])]}784 785    def retriever(state: MessagesState):786        """Retriever node"""787        # Use Supabase RPC to avoid client/version incompatibilities788        results = supabase_similarity_search(state["messages"][0].content, k=3)789 790        if results:  # Check if the list is not empty791            top = results[0]792            content = top.get("content") or top.get("page_content") or ""793            example_msg = HumanMessage(794                content=f"Here I provide a similar question and answer for reference: \n\n{content}",795            )796            return {"messages": [sys_msg] + state["messages"] + [example_msg]}797        else:798            # Handle the case when no similar questions are found799            return {"messages": [sys_msg] + state["messages"]}800 801    builder = StateGraph(MessagesState)802    builder.add_node("retriever", retriever)803    builder.add_node("assistant", assistant)804    builder.add_node("tools", ToolNode(tools))805    builder.add_edge(START, "retriever")806    builder.add_edge("retriever", "assistant")807    builder.add_conditional_edges(808        "assistant",809        tools_condition,810    )811    builder.add_edge("tools", "assistant")812 813    # Compile graph814    return builder.compile()815 816 817### =============== GRAPH VISUALIZATION =============== ###818 819 820def visualize_graph(821    graph, output_format: str = "mermaid", save_path: Optional[str] = None822) -> str:823    """824    Visualize the LangGraph agent graph.825 826    Args:827        graph: The compiled LangGraph graph828        output_format: "mermaid" for text diagram, "png" for image, "ascii" for terminal829        save_path: Optional path to save the visualization830 831    Returns:832        The visualization content (Mermaid code or file path)833    """834    try:835        if output_format == "mermaid":836            # Generate Mermaid diagram code837            mermaid_code = graph.get_graph().draw_mermaid()838 839            if save_path:840                with open(save_path, "w", encoding="utf-8") as f:841                    f.write("```mermaid\n")842                    f.write(mermaid_code)843                    f.write("\n```")844                print(f"✅ Mermaid diagram saved to: {save_path}")845 846            return mermaid_code847 848        elif output_format == "png":849            # Generate PNG image850            try:851                png_data = graph.get_graph().draw_mermaid_png()852 853                if save_path is None:854                    save_path = "langgraph_diagram.png"855 856                with open(save_path, "wb") as f:857                    f.write(png_data)858                print(f"✅ PNG diagram saved to: {save_path}")859                return save_path860 861            except Exception as e:862                print(f"⚠️  PNG generation failed: {e}")863                print("Falling back to Mermaid text format...")864                return visualize_graph(865                    graph,866                    "mermaid",867                    save_path.replace(".png", ".md") if save_path else None,868                )869 870        elif output_format == "ascii":871            # Print ASCII diagram to console872            graph.get_graph().print_ascii()873            return "ASCII diagram printed to console"874 875        else:876            raise ValueError(877                f"Unknown format: {output_format}. Use 'mermaid', 'png', or 'ascii'"878            )879 880    except Exception as e:881        print(f"❌ Visualization error: {e}")882        return f"Error: {e}"883 884 885def get_graph_info(graph) -> dict:886    """887    Get detailed information about the graph structure.888 889    Args:890        graph: The compiled LangGraph graph891 892    Returns:893        Dictionary with graph information894    """895    graph_obj = graph.get_graph()896 897    info = {898        "nodes": list(graph_obj.nodes.keys()),899        "edges": [],900        "entry_point": None,901        "tool_count": len(tools),902        "tools": [t.name for t in tools],903    }904 905    # Get edges906    for edge in graph_obj.edges:907        info["edges"].append({"from": edge.source, "to": edge.target})908 909    return info910 911 912def print_graph_summary():913    """Print a summary of the agent graph structure."""914    print("\n" + "=" * 60)915    print("🤖 GAIA Agent - LangGraph Structure Summary")916    print("=" * 60)917 918    print("\n📊 NODES:")919    print("  ├── retriever   : RAG node - searches similar Q&A from Supabase")920    print("  ├── assistant   : LLM node - processes messages with bound tools")921    print("  └── tools       : ToolNode - executes tool calls")922 923    print("\n🔗 EDGES:")924    print("  START → retriever → assistant")925    print("  assistant → [tools_condition] → tools OR END")926    print("  tools → assistant")927 928    print("\n🛠️  AVAILABLE TOOLS (21):")929    print("  ├── 🔍 Search: web_search, wiki_search, arxiv_search")930    print("  ├── 🔢 Math: add, subtract, multiply, divide, modulus, power, square_root")931    print(932        "  ├── 📁 File: save_and_read_file, download_file_from_url, extract_text_from_image"933    )934    print("  ├── 📊 Data: analyze_csv_file, analyze_excel_file")935    print("  ├── 💻 Code: execute_code_multilang (Python, Bash, SQL, C, Java)")936    print(937        "  └── 🖼️  Image: analyze_image, transform_image, draw_on_image, generate_simple_image, combine_images"938    )939 940    print("\n🔑 ENVIRONMENT VARIABLES:")941    print("  ├── GROQ_API_KEY        : For Groq LLM (qwen3-32b)")942    print("  ├── TAVILY_API_KEY      : For web search tool")943    print("  ├── SUPABASE_URL        : For vector database")944    print("  └── SUPABASE_SERVICE_ROLE_KEY : For Supabase authentication")945 946    print("\n" + "=" * 60 + "\n")947 948 949# test950if __name__ == "__main__":951    print("\n" + "=" * 60)952    print("🚀 GAIA Agent - Graph Visualization & Test")953    print("=" * 60 + "\n")954 955    # Build the graph956    print("🔧 Building agent graph...")957    graph = build_graph(provider="groq")958    print("✅ Graph built successfully!\n")959 960    # Print graph summary961    print_graph_summary()962 963    # Generate Mermaid diagram964    print("📊 Generating Mermaid diagram...")965    mermaid_code = visualize_graph(graph, "mermaid", "graph_diagram.md")966    print("\n--- Mermaid Diagram Code ---")967    print(mermaid_code)968    print("--- End of Mermaid Code ---\n")969 970    # Try to generate PNG (may fail without required dependencies)971    print("🖼️  Attempting to generate PNG diagram...")972    visualize_graph(graph, "png", "langgraph_diagram.png")973 974    # Get graph info975    print("\n📋 Graph Information:")976    info = get_graph_info(graph)977    print(f"  Nodes: {info['nodes']}")978    print(f"  Edges: {info['edges']}")979    print(f"  Tool count: {info['tool_count']}")980    print(f"  Tools: {info['tools'][:5]}... (and {len(info['tools'])-5} more)")981 982    # Test with a sample question983    print("\n" + "=" * 60)984    print("🧪 Testing with sample question...")985    print("=" * 60 + "\n")986 987    question = "When was a picture of St. Thomas Aquinas first added to the Wikipedia page on the Principle of double effect?"988    print(f"❓ Question: {question}\n")989 990    messages = [HumanMessage(content=question)]991    result = graph.invoke({"messages": messages})992 993    print("📨 Response messages:")994    for m in result["messages"]:995        m.pretty_print()996 997    print("\n" + "=" * 60)998    print("✅ Test completed!")999    print("=" * 60 + "\n")1000