File size: 16,078 Bytes
a94fa9b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
import logging
import os
import base64
import pandas as pd
import io
import contextlib

from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI
from langchain_community.tools import DuckDuckGoSearchRun
from langchain_community.document_loaders import AssemblyAIAudioTranscriptLoader
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
from langchain_core.tools import tool
from langchain_google_genai import ChatGoogleGenerativeAI

logger = logging.getLogger("eval_logger")


try:
    tools_llm = ChatOpenAI(model="gpt-4o", temperature=0)
except Exception as e:
    logger.error(f"Failed to initialize tools_llm (OpenAI gpt-4o) in tools.py: {e}. Ensure OPENAI_API_KEY is set.", exc_info=True)
    tools_llm = None


GEMINI_SHARED_MODEL_NAME = "gemini-2.5-pro-preview-05-06"
try:
    gemini_llm = ChatGoogleGenerativeAI(
        model=GEMINI_SHARED_MODEL_NAME, 
        temperature=0,
        timeout=360 # 6-minute timeout
    )
    logger.info(f"Successfully initialized shared Gemini model: {GEMINI_SHARED_MODEL_NAME} with a 360s timeout.")
except Exception as e:
    logger.error(f"Failed to initialize shared_gemini_llm in tools.py (model: {GEMINI_SHARED_MODEL_NAME}): {e}. Ensure GOOGLE_API_KEY is set and valid, and the model name is correct/available.", exc_info=True)
    gemini_llm = None


try:
    tools_llm = ChatOpenAI(model="gpt-4o", temperature=0)
except Exception as e:
    logger.error(f"Failed to initialize tools_llm in tools.py (ChatOpenAI with gpt-4o): {e}. Ensure OPENAI_API_KEY is set and .env is loaded.", exc_info=True)
    tools_llm = None # Set to None so the app can still load, but tool will fail

@tool
def analyse_image(img_path: str, question: str) -> str:
    """
    Analyses a **locally stored** image file to answer a specific question using a multimodal model.
    IMPORTANT: This tool expects a local file path for 'img_path' and cannot process web URLs directly.
    Args:
        img_path: Local path to the image file (e.g., /path/to/your/image.png).
        question: The question the user is trying to answer by analysing this image.
    Returns:
        A string containing the relevant information extracted from the image to answer the question, 
        or an error message if analysis fails.
    """
    if not tools_llm:
        return "Error: Vision LLM (gpt-4o) not initialized in tools.py. Cannot analyse image."
    if not os.path.exists(img_path):
        # This check is more critical now that we emphasize local paths.
        return f"Error: Image file not found at local path: {img_path}. This tool requires a local file path."
    
    logger.info(f"Attempting to analyse image: {img_path} for question: '{question}'")
    try:
        with open(img_path, "rb") as image_file:
            image_bytes = image_file.read()
        image_base64 = base64.b64encode(image_bytes).decode("utf-8")

        image_type = os.path.splitext(img_path)[1].lower()
        if image_type == '.jpg':
            image_type = '.jpeg'
        if image_type not in ['.png', '.jpeg', '.gif', '.webp']:
             return f"Error: Unsupported image type '{image_type}' for gpt-4o vision. Supported: PNG, JPEG, GIF, WEBP."

        prompt_text = f"Analyse this image to answer the following question: '{question}'. Focus on extracting only the information directly relevant to this question. Return only the extracted information, with no additional explanations or commentary."
        message = HumanMessage(
            content=[
                {"type": "text", "text": prompt_text},
                {"type": "image_url", "image_url": {"url": f"data:image/{image_type[1:]};base64,{image_base64}"}},
            ]
        )
        response = tools_llm.invoke([message])
        extracted_text = response.content
        logger.info(f"Successfully analysed {img_path} for question '{question}'. Response length: {len(extracted_text)}")
        return extracted_text.strip()
    except Exception as e:
        logger.error(f"Error analysing image {img_path} for question '{question}': {e}", exc_info=True)
        return f"Error during image analysis for question '{question}': {str(e)}"

@tool
def analyse_audio(audio_path: str, question: str) -> str:
    """
    Transcribes a **locally stored** audio file using AssemblyAI and then analyses the transcript 
    with a multimodal model (gpt-4o) to answer a specific question.
    IMPORTANT: This tool expects a local file path for 'audio_path' (e.g., /path/to/your/audio.mp3) 
    and **cannot process web URLs (like YouTube links) directly.**
    Args:
        audio_path: Local path to the audio file (e.g., /path/to/your/audio.mp3).
        question: The question the user is trying to answer by analysing this audio.
    Returns:
        A string containing the relevant information extracted from the audio to answer the question, 
        or an error message if analysis fails.
    """
    logger.info(f"Attempting to analyse audio from local path: {audio_path} for question: '{question}'")
    if not tools_llm: # vision_llm (gpt-4o) is used for the Q&A part
        return "Error: LLM (gpt-4o) for Q&A not initialized in tools.py. Cannot analyse audio transcript."
    if not audio_path: 
        return "Error: Audio file path not provided."
    if not os.path.exists(audio_path):
        return f"Error: Audio file not found at local path: {audio_path}. This tool requires a local file path."

    try:
        logger.info(f"Loading/transcribing audio from local file: {audio_path} using AssemblyAI.")
        loader = AssemblyAIAudioTranscriptLoader(file_path=audio_path) # AssemblyAI loader primarily works with local paths for reliability.
        docs = loader.load()
        
        if not docs or not docs[0].page_content:
            logger.error(f"AssemblyAI transcription failed or returned empty for {audio_path}.")
            return f"Error: Transcription failed or returned empty content for {audio_path}."
            
        transcript = docs[0].page_content
        logger.info(f"Successfully transcribed audio from {audio_path}. Transcript length: {len(transcript)}")
        
        qa_prompt_text = (
            f"The following is a transcript of an audio file: \n\nTranscript:\n{transcript}\n\n---\n\n"
            f"Based SOLELY on the information in the transcript above, answer the following question: '{question}'. "
            f"Provide only the direct answer as extracted or inferred from the transcript, with no additional commentary."
        )
        
        message = HumanMessage(content=qa_prompt_text)
        response = tools_llm.invoke([message])
        answer = response.content
        logger.info(f"Successfully analysed transcript from {audio_path} for question '{question}'. Answer length: {len(answer)}")
        return answer.strip()

    except Exception as e:
        logger.error(f"Error analysing audio {audio_path} for question '{question}': {e}", exc_info=True)
        if "api key" in str(e).lower() or "authenticate" in str(e).lower():
            return f"Error during audio analysis: AssemblyAI authentication failed. Please check your ASSEMBLYAI_API_KEY. Original error: {str(e)}"
        return f"Error during audio analysis for question '{question}': {str(e)}"

@tool
def execute_python_code_from_file(file_path: str, question: str) -> str:
    """
    Reads the content of a **locally stored** Python file and uses a powerful LLM (gpt-4o) 
    to answer a specific question about the Python code (e.g., its output, functionality, or errors).
    IMPORTANT: This tool expects a local file path for 'file_path' and cannot process web URLs directly.
    It does NOT actually execute the code, but rather analyses it textually.
    Args:
        file_path: Local path to the Python file (e.g., /path/to/your/script.py).
        question: The question the user is trying to answer about this Python code.
    Returns:
        A string containing the LLM's analysis or answer about the Python code, or an error message.
    """
    logger.info(f"Attempting to analyse Python file: {file_path} for question: '{question}'")
    if not tools_llm: # vision_llm (gpt-4o) is used for the analysis
        return "Error: LLM (gpt-4o) for code analysis not initialized in tools.py."
    if not file_path:
        return "Error: Python file path not provided."
    if not os.path.exists(file_path):
        return f"Error: Python file not found at local path: {file_path}. This tool requires a local file path."
    if not file_path.lower().endswith('.py'):
        return f"Error: File at {file_path} is not a Python (.py) file."

    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            python_code_content = f.read()
        
        logger.info(f"Successfully read Python file {file_path}. Content length: {len(python_code_content)}")

        analysis_prompt_text = (
            f"The following is the content of a Python file: \n\nPython Code:\n```python\n{python_code_content}\n```\n\n---\n\n"
            f"Based SOLELY on the Python code provided above, answer the following question: '{question}'. "
            f"If the question asks for the output, predict the output. If it asks about functionality, describe it. "
            f"Provide only the direct answer or analysis, with no additional commentary or explanations unless the question asks for it."
        )
        
        message = HumanMessage(content=analysis_prompt_text)
        response = tools_llm.invoke([message]) # Using vision_llm (gpt-4o) for this analysis
        answer = response.content
        logger.info(f"Successfully analysed Python code from {file_path} for question '{question}'. Answer length: {len(answer)}")
        return answer.strip()

    except Exception as e:
        logger.error(f"Error analysing Python file {file_path} for question '{question}': {e}", exc_info=True)
        return f"Error during Python file analysis for question '{question}': {str(e)}"

@tool
def execute_pandas_script_for_excel(excel_file_path: str, python_code: str) -> str:
    """
    Executes a given Python script (which should use pandas) to perform analysis on an Excel file.
    The script MUST load the Excel file using the provided 'excel_file_path' variable.
    The script MUST print its final answer to standard output. The print output will be returned as the result.
    This tool is for calculations, data manipulation, and specific lookups within the Excel file.

    Args:
        excel_file_path: The path to the Excel file that the script will process.
        python_code: A string containing the Python script to execute. 
                     Example: 
                     '''
                     import pandas as pd
                     df = pd.read_excel(excel_file_path, sheet_name=0)
                     # Perform analysis ...
                     final_answer = df["SomeColumn"].sum() # Example operation
                     print(final_answer)
                     '''
    Returns:
        The standard output from the executed script (which should be the answer), or an error message if execution fails.
    """
    logger.info(f"Attempting to execute pandas script for Excel file: {excel_file_path}")
    logger.debug(f"Python code to execute:\n{python_code}")

    if not os.path.exists(excel_file_path):
        return f"Error: Excel file not found at {excel_file_path}"

    # Prepare the local namespace for exec, including pandas and the file path
    local_namespace = {
        "pd": pd, 
        "excel_file_path": excel_file_path,
        "__builtins__": __builtins__ # Ensure basic builtins are available
    }
    
    # Capture stdout
    stdout_capture = io.StringIO()
    try:
        with contextlib.redirect_stdout(stdout_capture):
            exec(python_code, {"__builtins__": __builtins__}, local_namespace) # Provide pandas in globals, path in locals
        output = stdout_capture.getvalue().strip()
        logger.info(f"Successfully executed pandas script. Output: '{output}'")
        if not output: # If the script printed nothing, it might indicate an issue or missing print().
            return "Script executed successfully but produced no output. Ensure the script prints the final answer."
        return output
    except Exception as e:
        logger.error(f"Error executing pandas script: {e}", exc_info=True)
        # Provide a more detailed error message back to the LLM
        import traceback
        tb_str = traceback.format_exc()
        return f"Error during script execution: {str(e)}\nTraceback:\n{tb_str}"

@tool
def analyse_youtube(youtube_url: str, question: str) -> str:
    """
    Analyzes a YouTube video to answer a specific question.
    This tool is intended for questions that require understanding the visual content of the video.
    It sends the YouTube URL directly to the shared Gemini model.

    Args:
        youtube_url: The full URL of the YouTube video (e.g., https://www.youtube.com/watch?v=...). 
        question: The question to answer based on the video's content.
    Returns:
        A string containing the answer from the shared Gemini model, or an error message if analysis fails.
    """
    logger.info(f"Attempting to analyse YouTube video: {youtube_url} with shared Gemini model ({GEMINI_SHARED_MODEL_NAME}) for question: '{question}'")
    if not gemini_llm:
        return f"Error: Shared Gemini LLM ({GEMINI_SHARED_MODEL_NAME}) not initialized in tools.py. Cannot analyse YouTube video."

    try:
        
        prompt = f"Video URL: {youtube_url}\n\nQuestion: {question}\n\nBased on the video at the URL, please provide the answer."
        message = HumanMessage(content=prompt)

        response = gemini_llm.invoke([message])
        answer = response.content
        logger.info(f"Successfully analysed YouTube video {youtube_url} with shared Gemini. Answer: {answer[:200]}...")
        return answer.strip()
    except Exception as e:
        logger.error(f"Error analysing YouTube video {youtube_url} with shared Gemini ({GEMINI_SHARED_MODEL_NAME}): {e}", exc_info=True)
        return f"Error during YouTube video analysis with shared Gemini: {str(e)}"

@tool
def deep_analysis_with_gemini(question: str) -> str:
    """
    Performs a deep analysis of a complex question using a powerful shared Gemini model.
    Use this tool for questions that are multifaceted, require deep reasoning, 
    or for historical queries where standard search tools might be insufficient after initial attempts.
    This tool directly passes the question to a shared Gemini model for a comprehensive answer.

    Args:
        question: The complex question to be analyzed.
    Returns:
        A string containing the detailed answer from the shared Gemini model, or an error message.
    """
    logger.info(f"Attempting deep analysis with shared Gemini model ({GEMINI_SHARED_MODEL_NAME}) for question: '{question}'")
    if not gemini_llm:
        return f"Error: Shared Gemini LLM ({GEMINI_SHARED_MODEL_NAME}) not initialized in tools.py."

    try:
        message = HumanMessage(content=question)
        response = gemini_llm.invoke([message])
        answer = response.content
        logger.info(f"Successfully performed deep analysis with shared Gemini. Answer length: {len(answer)}")
        return answer.strip()
    except Exception as e:
        logger.error(f"Error during deep analysis with shared Gemini ({GEMINI_SHARED_MODEL_NAME}): {e}", exc_info=True)
        return f"Error during deep analysis with shared Gemini: {str(e)}"

# Initialize other tools
search_tool = DuckDuckGoSearchRun()

wikipedia_tool = WikipediaQueryRun(api_wrapper=WikipediaAPIWrapper())

TOOLS = [
    analyse_image,
    search_tool,
    analyse_audio,
    execute_python_code_from_file,
    wikipedia_tool,
    execute_pandas_script_for_excel,
    analyse_youtube,
    deep_analysis_with_gemini,
]

logger.info(f"Tools initialized in tools.py: {[tool.name if hasattr(tool, 'name') else tool.__name__ for tool in TOOLS]}")