File size: 34,022 Bytes
d116ff2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c67efc
4e34cc5
54cf5b7
c745533
4e34cc5
d116ff2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570dc2b
 
db76bdc
d116ff2
3817cc8
d116ff2
 
 
 
 
 
 
 
 
 
 
3817cc8
 
 
 
 
 
 
 
 
db76bdc
3817cc8
 
 
d116ff2
 
 
 
 
 
 
 
 
 
 
 
 
c9454da
d116ff2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3817cc8
d116ff2
3817cc8
d116ff2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3817cc8
d116ff2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3817cc8
 
d116ff2
 
1217190
be6c585
1217190
 
 
 
 
 
 
a4de75d
1217190
 
 
 
 
 
 
 
 
 
 
 
a4de75d
1217190
 
 
4c4dd9a
a288928
4c4dd9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a288928
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d116ff2
 
 
 
 
 
 
 
3817cc8
d116ff2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45af5d9
d116ff2
 
 
3817cc8
d116ff2
 
 
3817cc8
 
1217190
a288928
 
d116ff2
 
 
45af5d9
d116ff2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
import requests
import os
import gradio as gr
import inspect
import pandas as pd
import time
import re
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_community.tools import TavilySearchResults
from langchain import hub # Used to pull predefined prompts from LangChain Hub
from langchain.agents import AgentExecutor, create_react_agent
from langchain.memory import ConversationSummaryMemory
from typing import Any, List, Optional
from langchain.agents import AgentExecutor, Agent
from langchain.tools.base import BaseTool
from langchain.memory import ConversationSummaryBufferMemory
from google.api_core import retry
from google import genai
from langchain.prompts import PromptTemplate

# for openAI model
from langchain_openai import ChatOpenAI
from openai import OpenAI

# tools imported from helper.py
from helper import repl_tool, get_travily_api_search_tool,audio_transcriber_tool,file_saver_tool,gemini_multimodal_tool
from helper import wikipedia_search_tool2
# (Keep Constants as is)
# --- Constants ---
DEFAULT_API_URL = "https://agents-course-unit4-scoring.hf.space"

# --- Basic Agent Definition ---
# ----- THIS IS WERE YOU CAN BUILD WHAT YOU WANT ------
class BasicAgent:
    def __init__(
        self,
        agent: Agent,
        tools: List[BaseTool],
        verbose: bool = False,
        handle_parsing_errors: bool = True,
        max_iterations: int = 9,
        memory: Optional[ConversationSummaryMemory] = None
    ) -> None:
        """
        Initialize with parameters required for AgentExecutor.
        """
        self.agent: Agent = agent
        self.tools: List[BaseTool] = tools
        self.verbose: bool = verbose
        self.handle_parsing_errors: bool = handle_parsing_errors
        self.max_iterations: int = max_iterations
        self.memory: Optional[ConversationSummaryMemory] = memory
        self.agent_obj = AgentExecutor(
            agent=self.agent,
            tools=self.tools,
            verbose=self.verbose,
            handle_parsing_errors=self.handle_parsing_errors,
            max_iterations=self.max_iterations,
            memory=self.memory
        )

    def is_retriable(self, e: Exception) -> bool:
        # Adjust this check if your error type is different
        return isinstance(e, genai.errors.APIError) and getattr(e, "code", None) in {429, 503}

    def invoke_with_retry(self,question: str, max_retries: int = 5, initial_delay: float = 10.0) -> str:
        current_delay = initial_delay
        for attempt in range(max_retries):
            try:
                result = self.agent_obj.invoke(
                    {"input": question},
                    config={"configurable": {"session_id": "test-session"}},
                )
                return result['output']
            except Exception as e:
                if self.is_retriable(e):
                    # Check if the error object provides a specific retry_delay
                    if hasattr(e, 'retry_delay') and hasattr(e.retry_delay, 'seconds'):
                        # Use the specific retry_delay provided by the API
                        current_delay = float(e.retry_delay.seconds)
                        print(f"Quota error (attempt {attempt+1}/{max_retries}). API suggested retry after {current_delay} seconds.", flush=True)
                    else:
                        # Fallback to exponential backoff if no specific delay is provided
                        print(f"Quota error (attempt {attempt+1}/{max_retries}). Retrying in {current_delay} seconds with exponential backoff.", flush=True)
                        current_delay *= 2  # Exponential backoff
                    
                    time.sleep(current_delay)
                else:
                    # If it's not a retriable error, re-raise it
                    raise
        # If all retries fail, raise a RuntimeError
        raise RuntimeError(f"Max retries ({max_retries}) exceeded due to persistent quota errors or other retriable issues.")

    def __call__(self, question: str) -> str:
        """
        Allows the instance to be called directly to get an AgentExecutor.
        """
        return self.invoke_with_retry(question)

def run_and_submit_all( profile: gr.OAuthProfile | None):
    """
    Fetches all questions, runs the BasicAgent on them, submits all answers,
    and displays the results.
    """
    # --- Determine HF Space Runtime URL and Repo URL ---
    space_id = os.getenv("SPACE_ID") # Get the SPACE_ID for sending link to the code

    if profile:
        username= f"{profile.username}"
        print(f"User logged in: {username}")
    else:
        print("User not logged in.")
        return "Please Login to Hugging Face with the button.", None

    api_url = DEFAULT_API_URL
    questions_url = f"{api_url}/questions"
    submit_url = f"{api_url}/submit"

    google_api_key = os.getenv("GOOGLE_API_KEY")
    if not google_api_key:
        print("Google API key not found in environment variables.")
        return "Google API key not found. Please set GOOGLE_API_KEY environment variable.", None
    print(f"Using Google API key: {google_api_key[:4]}... (truncated for security)")

    openai_api_key = os.getenv("OPENAI_API_KEY")
    if not openai_api_key:
        print("OpenAI API key not found in environment variables.")
        return "OpenAI API key not found. Please set OPENAI_API_KEY environment variable.", None
    print(f"Using OpenAI API key: {openai_api_key[:4]}... (truncated for security)")

    #gemini_model ="gemini-2.0-flash"
    #gemini_model ="gemini-1.5-flash"
    #gemini_model ="models/gemini-2.5-flash-preview-05-20"
    gemini_model = "models/gemini-2.5-flash-lite-preview-06-17"


    #NMODEL
    
    llm_client = ChatGoogleGenerativeAI(
        model=gemini_model,          # or another Gemini model name
        google_api_key=google_api_key, # your Gemini API key
        temperature=0,
    )
    

    #llm_client = ChatOpenAI(model='gpt-4o',temperature=0,api_key=openai_api_key)

    #llm_client = ChatOpenAI(model='gpt-4o',temperature=0,api_key=openai_api_key,top_p=1,presence_penalty=0,frequency_penalty=0,seed=12345)


    serp_api_key = os.getenv("SERP_API_KEY")
    if not serp_api_key:
        print("SerpAPI key not found in environment variables.")
        return "SerpAPI key not found. Please set SERP_API_KEY environment variable.", None
    print(f"Using SerpAPI key: {serp_api_key[:4]}... (truncated for security)")

    tavily_api_key = os.getenv("TAVILY_API_KEY")
    if not tavily_api_key:
        print("Tavily API key not found in environment variables.")
        return "Tavily API key not found. Please set TAVILY_API_KEY environment variable.", None
    print(f"Using Tavily API key: {tavily_api_key[:4]}... (truncated for security)")

    travily_api_search_tool = get_travily_api_search_tool(tavily_api_key)
    tools = [ repl_tool, file_saver_tool,audio_transcriber_tool,travily_api_search_tool, gemini_multimodal_tool, wikipedia_search_tool2]
    #tools = [ repl_tool, file_saver_tool,audio_transcriber_tool,travily_api_search_tool, gemini_multimodal_tool]



    EX5_OBSERVATION_STRING = (
    "[{{'title': '1977 New York Yankees Hitting Stats - Baseball-Reference.com', "
    "'url': 'https://www.baseball-reference.com/teams/NYY/1977.shtml', "
    "'content': '| Rk | Player | Age | Pos | WAR | W | L | W-L% | ERA | G | GS | GF | CG | SHO | SV | IP | H | R | ER | HR | BB | IBB | SO | HBP | BK | WP | BF | ERA+ | FIP | WHIP | H9 | HR9 | BB9 | SO9 | SO/BB | Awards | All logos are the trademark & property of their owners and not Sports Reference LLC. Copyright © 2000-2025 Sports Reference LLC. Sports Info Solutions logo Sports Info Solutions logo Sports Info Solutions logo'}}]"
    )

    prompt = PromptTemplate(
    input_variables=["input", "agent_scratchpad", "chat_history", "tool_names"],
    template="""
    You are a smart and helpful AI Agent/Assistant that excels at fact-based reasoning. You are allowed and encouraged to use one or more tools as needed to answer complex questions and perform tasks.
    It is CRUCIAL that you ALWAYS follow the exact format below. Do not deviate.

    **IMPORTANT - CONCISE FINAL ANSWER FORMATTING RULES (MANDATORY):**
    Your FINAL ANSWER must be presented in one of these formats, and ONLY the answer itself (no introductory phrases):
    - A number (e.g., '26', '1977', '519')
    - As few words as possible (e.g., 'Paris', 'down', 'LUX')
    - A comma-separated list of numbers and/or strings (e.g., '10,20,30', 'apple,banana,orange')

    **STRICT RULES FOR FINAL ANSWER CONTENT:**
    1.  **Numbers:** Do NOT use commas (e.g., '1000000' instead of '1,000,000'). Do NOT use units ($ or % or degrees Celsius/Fahrenheit) unless explicitly requested in the question.
    2.  **Strings (Words):** Do NOT use articles (a, an, the). Do NOT use abbreviations (e.g., 'New York' instead of 'NYC', e.g., 'Saint' instead of 'St'). Write digits in plain text unless specified otherwise (e.g., 'two' instead of '2' if it's part of a string answer and not a numerical answer).
    3.  **Comma-Separated Lists:** Apply the above rules (numbers or strings) to each element in the list.

    **Your response MUST always start with 'Thought:'. Your FINAL ANSWER must be preceded by 'Final Answer: '.**

    You have access to the following tools:
    {tools}

    To use a tool, you MUST follow this precise format:

    Thought: I need to use a tool to find the answer.
    Action: [tool_name] # This will be one of [{tool_names}]
    Action Input: [input_for_the_tool]
    Observation: [result_from_the_tool]

    IMPORTANT NOTE ON TOOL USAGE:
    - If an 'Observation' from a tool, especially `tavily_search` or `serpapi_Google Search_tool`, contains a list, table, or structured text that might hold the answer, your next step should be to use `python_repl` to parse and extract the required information from that observation's content. Do NOT search again unless the content is genuinely insufficient or irrelevant.
    - If an 'Observation' from a tool does NOT directly contain the specific answer to your question, you MUST refine your query or switch to a different, more suitable tool (e.g., 'tavily_search' for broader or more current information if 'wikipedia_search_tool2' was insufficient). Do NOT get stuck repeatedly using the same tool if it's not yielding the direct answer.
    - If the input contains the exact phrase "Attachment '{{file_name}}' available at: {{attachment_url}}" (where '{{file_name}}' and '{{attachment_url}}' are placeholders for actual values), consider the file type:
      - If the file type is binary/text (e.g., .xlsx, .docx, .mp3, .jpg, .pdf,.png), you MUST use the 'file_saver' tool to download and save it.
        For 'file_saver', the Action Input must be a JSON string like: '{{"url": "the_attachment_url", "local_filename": "the_file_name_from_attachment"}}'
        example: for input, Attachment '1f975693-876d-457b-a649-393859e79bf3.mp3' available at EXACT URL: https://agents-course-unit4-scoring.hf.space/files/1f975693-876d-457b-a649-393859e79bf3, Action Input for file_saver would be '{{"url": "https://agents-course-unit4-scoring.hf.space/files/1f975693-876d-457b-a649-393859e79bf3", "local_filename": "1f975693-876d-457b-a649-393859e79bf3.mp3"}}'

    IMPORTANT: When processing audio files (like .mp3) that have been saved using 'file_saver', the 'audio_transcriber_tool' MUST be used with the 'local_filename' of the saved audio file as its Action Input. Do NOT pass URLs or remote paths directly to 'audio_transcriber_tool'.

    **For image files (like .jpg, .png) that have been saved using 'file_saver', the 'gemini_multimodal_tool' MUST be used to analyze their content and answer questions based on the image. The Action Input for 'gemini_multimodal_tool' must be a JSON string like: '{{"image_path": "the_local_filename", "question": "the_user_question"}}'**

    Example: given a chess board image and asked to predict the next best move, if Multi-modal LLM is available, you can use it to answer the question.

    If you have sufficient information and can provide a CONCISE response, or if no tool is needed, you MUST use this precise format:

    Thought: I have enough information, or no tool is needed.
    Final Answer: [your concise/short response here]

    **VERY IMPORTANT: Your response MUST always start with 'Thought:'. Your FINAL ANSWER must be preceded by 'Final Answer: '.**

    Here are some examples of how you should respond, strictly adhering to the formatting rules:

    Example 1:
    Question: What is the capital of France?
    Thought: I need to use a tool to find the capital of France.
    Action: tavily_search
    Action Input: capital of France
    Observation: The capital of France is Paris.
    Thought: I have found the answer.
    Final Answer: Paris

    Example 2:
    Question: What is 2 + 2?
    Thought: This is a simple arithmetic question, no tool is needed.
    Final Answer: 4

    Example 3:
    Question: How many studio albums were published by Mercedes Sosa between 2000 and 2009 (included)? You can use the latest 2022 version of english wikipedia.
    Thought: The user is asking for specific information about discography, which might be found with a search tool. The `serpapi_Google Search_tool` can fetch detailed sections. After getting the content, I will need to parse it using `python_repl` to count the albums within the specified years.
    Action: serpapi_Google Search
    Action Input: Mercedes Sosa discography
    Observation: [Discography text content from search result]
    Thought: I have retrieved discography text. Now I need to parse this text to identify and count studio albums released between 2000 and 2009. I will use the `python_repl` tool for this.
    Action: python_repl
    Action Input:
    ```python
    import re
    text = "[Discography text content from previous observation]" # Replace with actual text
    albums_2000_2009 = []
    pattern = r"\((\d{{4}})\)\s*(.*?)(?:\[|\n|$)" # Ensures year is captured. Double braces {{}} to escape regex literal braces
    for match in re.finditer(pattern, text):
        year = int(match.group(1))
        if 2000 <= year <= 2009:
            albums_2000_2009.append(match.group(2).strip())
    print(len(albums_2000_2009))
    ```
    Observation: 3
    Thought: I have parsed the discography and counted the albums. I have found the answer.
    Final Answer: 3

    **Example 4: (Crucial new example for image processing)**
    Question: What is the next best move in this chess position? Attachment 'chess_board.png' available at EXACT URL: https://agents-course-unit4-scoring.hf.space/files/cca530fc-4052-43b2-b130-b30968d8aa44
    Thought: The user is asking a question about a chess position and has provided an image. I need to first save the image locally using the 'file_saver' tool, and then use the 'gemini_multimodal_tool' to analyze the image and answer the question.
    Action: file_saver
    Action Input: {{"url": "https://agents-course-unit4-scoring.hf.space/files/cca530fc-4052-43b2-b130-b30968d8aa44", "local_filename": "cca530fc-4052-43b2-b130-b30968d8aa44.png"}}
    Observation: File downloaded successfully to cca530fc-4052-43b2-b130-b30968d8aa44.png
    Thought: The image has been successfully downloaded. Now I need to analyze its content to determine the next best chess move using the 'gemini_multimodal_tool'.
    Action: gemini_multimodal_tool
    Action Input: {{"image_path": "cca530fc-4052-43b2-b130-b30968d8aa44.png", "question": "What is the next best move in this chess position?"}}
    Observation: The next best move is e4.
    Thought: I have used the 'gemini_multimodal_tool' to get the best move based on the image.
    Final Answer: e4

    Example 5:
    Question: What is the opposite of up?
    Thought: The question asks for the opposite of up. This is a direct knowledge question.
    Final Answer: down

    Example 6: (New example for parsing baseball stats)
    Question: How many at bats did the Yankee with the most walks in the 1977 regular season have that same season?
    Thought: I need to find the Yankee player with the most walks in 1977 and then find their at bats. This will require searching for Yankees 1977 stats and then parsing the results to extract the relevant player and their at-bats. I will use 'tavily_search' first to find the stats. After getting the search results, I will examine their content for a list or table of players and their stats. If found, I will use 'python_repl' to parse it.
    Action: tavily_search
    Action Input: New York Yankees 1977 batting stats
    Observation: {{EX5_OBSERVATION_STRING}}
    Thought: I have received an observation from `tavily_search`. I need to examine its `content` to determine if it contains the necessary data (e.g., a list or table of players/stats/winners). If so, my next step is to use `python_repl` to parse this content to extract the specific information needed to answer the question. I should only consider another `tavily_search` if the current observation's content is clearly insufficient.
    Action: python_repl
    Action Input:
    ```python
    # Example: Parse the text content from the tavily_search observation.
    # This is a placeholder for the actual parsing logic you would write.
    # For the Malko question, you would parse the list of winners and their nationalities.
    # For instance, if the observation content contains:
    # "1983 | Claus Peter Flor | East Germany"
    # You would extract this and apply your filtering logic.
    ```
    Observation: 519
    Thought: I have parsed the data and identified Roy White as the Yankee with the most walks (75) in 1977, and his at-bats were 519. I have found the answer.
    Final Answer: 519

    Example 7: (Parsing a table for minimum value)
    Question: What country had the least number of athletes at the 1928 Summer Olympics? If there's a tie for a number of athletes, return the first in alphabetical order. Give the IOC country code as your answer.
    Thought: I need to find a table of athlete counts by country for the 1928 Olympics. I will use 'tavily_search' to find the data. After getting the search results, I will examine their content for a list or table of countries and their athlete counts. If found, I will use 'python_repl' to parse it.
    Action: tavily_search
    Action Input: 1928 Summer Olympics athlete count by country
    Observation: [Table or HTML/text with country and athlete counts]
    Thought: I have found the table in the `tavily_search` observation. Now I need to parse it to find the country with the least athletes, and if there is a tie, pick the first alphabetically. I will use 'python_repl' for this.
    Action: python_repl
    Action Input:
    ```python
    # Example: python code to parse the text table and find the IOC country code with the least athletes.
    # (Replace this with actual code as needed)
    ```
    Observation: LUX
    Thought: I have found the country with the least athletes.
    Final Answer: LUX

    Example 8: (Identifying non-commutative elements from a table)
    Question: Given this table defining * on the set S = {{p, q, r}}, provide the subset of S involved in any possible counter-examples that prove * is not commutative. Provide your answer as a comma separated list of the elements in the set in alphabetical order.

    |*|p|q|r|
    |---|---|---|---|
    |p|p|r|q|
    |q|r|q|p|
    |r|p|q|r|

    Thought: The user wants to find elements involved in counter-examples to prove non-commutativity. This means I need to check if $x * y \neq y * x$ for any pair of distinct elements $x, y$ from the set S = {{p, q, r}}. I will go through each unique pair and check their commutativity directly from the provided table.

    1.  **Check (p, q):**
        * $p * q = r$ (from row 'p', column 'q')
        * $q * p = r$ (from row 'q', column 'p')
        * Result: Commutative (since $r = r$).

    2.  **Check (p, r):**
        * $p * r = q$ (from row 'p', column 'r')
        * $r * p = p$ (from row 'r', column 'p')
        * Result: Not commutative (since $q \neq p$). This is a counter-example.
        * The elements involved are 'p' and 'r'.

    Since I found a counter-example (p, r), the subset of elements involved is {{p, r}}. I should list them alphabetically.

    Final Answer: p,r

    
    Example 9:
    Question: From the list "red apple, green banana, blue berry, yellow zucchini", identify the botanical fruits and list them alphabetically.
    Thought: The user wants me to identify botanical fruits and then sort them alphabetically.
    - red apple: botanical fruit
    - green banana: botanical fruit
    - blue berry: botanical fruit
    - yellow zucchini: botanical fruit

    All items are botanical fruits. Now I will alphabetize them, treating each phrase as a single unit for sorting, and maintaining their original phrasing and spaces.
    1. blue berry
    2. green banana
    3. red apple
    4. yellow zucchini
    Final Answer: blue berry,green banana,red apple,yellow zucchini

    
    Example 10:
    Question: How many Nobel Prizes were awarded to individuals with the surname Curie between 1900 and 1960 (included)?
    Thought: The user is asking for a count of Nobel Prizes for a specific family within a date range. I should use `wikipedia_search_tool2` to find information about the Curie family's Nobel Prizes. It is CRUCIAL that I then identify and strictly parse relevant sections (e.g., "Nobel Prizes" or "Awards") for individuals named Curie, and only count those within the 1900 to 1960 range (inclusive). I will use `python_repl` for robust parsing and filtering.
    Action: wikipedia_search_tool2
    Action Input: Curie family Nobel Prizes
    Observation: {{OBSERVATION_FROM_WIKIPEDIA_CURIE_FAMILY}} # Assume this contains text including details about Marie Curie, Pierre Curie, Irène Joliot-Curie, Frédéric Joliot-Curie, etc., and their awards.
    Thought: I have retrieved information about the Curie family's Nobel Prizes. Now, I must carefully parse this content to find individuals with the surname Curie (including hyphenated names like Joliot-Curie) and extract only those Nobel Prizes awarded between 1900 and 1960 (inclusive). I will use `python_repl` to perform this precise extraction and counting.
    Action: python_repl
    Action Input:
    ```python
    import re

    # This text should be replaced by the actual 'Observation' content from the previous step.
    # For demonstration, let's use a simplified representation of relevant text.
    curie_content = \"""
    == Nobel Prizes ==
    Marie Curie:
    - Physics (1903) with Pierre Curie and Henri Becquerel
    - Chemistry (1911)

    Pierre Curie:
    - Physics (1903) with Marie Curie and Henri Becquerel

    Irène Joliot-Curie:
    - Chemistry (1935) with Frédéric Joliot-Curie

    Frédéric Joliot-Curie:
    - Chemistry (1935) with Irène Joliot-Curie

    Other family members received awards outside this scope or were not direct Nobel laureates with the surname Curie.
    \"""

    unique_prizes = set()
    
    # Looking for a pattern like "Category (YYYY)" that is associated with a Curie
    # This regex attempts to find Nobel Prize categories followed by a year in parentheses.
    # We then ensure the line also mentions a "Curie" related name.
    
    lines = curie_content.split('\n')
    
    for line in lines:
        # Regex to find (Category) (YYYY) pattern and ensure 'Curie' or 'Joliot-Curie' is in the line
        # The (?:...) is for non-capturing group.
        # The \\b ensures whole word match for 'Curie'
        prize_match = re.search(r'(Physics|Chemistry|Physiology or Medicine|Peace|Literature|Economic Sciences)\s*\(((\d{{4}}))\).*?(?:Curie|Joliot-Curie)', line)
        if prize_match:
            category = prize_match.group(1)
            year = int(prize_match.group(3))
            
            if 1900 <= year <= 1960:
                # Add a unique identifier for each prize instance (category-year)
                # This correctly counts a shared prize (e.g., 1903 Physics) as one distinct prize.
                unique_prizes.add(f"{{{{category}}}}-{{{{year}}}}")

    print(len(unique_prizes))
    ```
    Observation: 3
    Thought: I have successfully parsed the information about the Curie family's Nobel Prizes, filtered for the correct surname and date range (1900-1960), and counted the unique awards. I have found the answer.
    Final Answer: 3

    ---
    Previous conversation history:
    {chat_history}

    New input: {input}
    ---
    {agent_scratchpad}
    """
)

    summary_memory = ConversationSummaryMemory(llm=llm_client, memory_key="chat_history")


    # Initialize gemini model with streaming enabled
    
    summary_llm = ChatGoogleGenerativeAI(
        model=gemini_model,
        google_api_key=google_api_key,
        temperature=0,
        streaming=True
    )
    

    #summary_llm = ChatOpenAI(model='gpt-4o', temperature=0, streaming=False,api_key=openai_api_key)
    #summary_llm = ChatOpenAI(model='gpt-4o', temperature=0, streaming=False,api_key=openai_api_key,top_p=1,presence_penalty=0,frequency_penalty=0)


    # Create a ReAct agent
    summary_react_agent = create_react_agent(
        llm=summary_llm,
        tools=tools,
        prompt=prompt
    )

    # 1. Instantiate Agent ( modify this part to create your agent)
    try:
        agent = BasicAgent(summary_react_agent, tools, True, True, 30, summary_memory)
    except Exception as e:
        print(f"Error instantiating agent: {e}")
        return f"Error initializing agent: {e}", None
    agent_code = f"https://huggingface.co/spaces/{space_id}/tree/main"
    print(agent_code)

    # 2. Fetch Questions
    print(f"Fetching questions from: {questions_url}")
    try:
        response = requests.get(questions_url, timeout=15)
        response.raise_for_status()
        questions_data = response.json()
        if not questions_data:
             print("Fetched questions list is empty.")
             return "Fetched questions list is empty or invalid format.", None
        print(f"Fetched {len(questions_data)} questions.")
    except requests.exceptions.RequestException as e:
        print(f"Error fetching questions: {e}")
        return f"Error fetching questions: {e}", None
    except requests.exceptions.JSONDecodeError as e:
         print(f"Error decoding JSON response from questions endpoint: {e}")
         print(f"Response text: {response.text[:500]}")
         return f"Error decoding server response for questions: {e}", None
    except Exception as e:
        print(f"An unexpected error occurred fetching questions: {e}")
        return f"An unexpected error occurred fetching questions: {e}", None

    # 3. Run your Agent
    results_log = []
    answers_payload = []
    print(f"Running agent on {len(questions_data)} questions...")
    for item in questions_data:
        task_id = item.get("task_id")
        question_text = item.get("question")
        file_name = item.get("file_name") # Get the file_name if it exists

        # Construct the question string that your LLM will see,
        # including the attachment URL if present.
        full_question_for_agent = question_text
        if file_name:
            attachment_url = f"https://agents-course-unit4-scoring.hf.space/files/{task_id}"
            full_question_for_agent += f"\n\nAttachment '{file_name}' available at EXACT URL: {attachment_url}"
            print(f"Running agent on task {task_id}: {full_question_for_agent}",flush=True)
        
        '''
        allowed_ids = {
            #"cca530fc-4052-43b2-b130-b30968d8aa44",
            #"a1e91b78-d3d8-4675-bb8d-62741b4b68a6",
            #"3f57289b-8c60-48be-bd80-01f8099ca449",
            #"2d83110e-a098-4ebb-9987-066c06fa42d0",
            #"cf106601-ab4f-4af9-b045-5295fe67b37d",
            #"7bd855d8-463d-4ed5-93ca-5fe35145f733",
            #"5a0c1adf-205e-4841-a666-7c3ef95def9d",
            #"f918266a-b3e0-4914-865d-4faa564f1aef",
            #"9d191bce-651d-4746-be2d-7ef8ecadb9c2",
            #"6f37996b-2ac7-44b0-8e68-6d28256631b4",
            "8e867cd7-cff9-4e6c-867a-ff5ddc2550be",
        }
        if task_id not in allowed_ids:
            continue
        '''
        try:
            submitted_answer = agent(full_question_for_agent)
            answers_payload.append({"task_id": task_id, "submitted_answer": submitted_answer})
            results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": submitted_answer})
            time.sleep(61)  # Add a 1 min delay before running the agent
        except Exception as e:
             print(f"Error running agent on task {task_id}: {e}")
             results_log.append({"Task ID": task_id, "Question": question_text, "Submitted Answer": f"AGENT ERROR: {e}"})

    if not answers_payload:
        print("Agent did not produce any answers to submit.")
        return "Agent did not produce any answers to submit.", pd.DataFrame(results_log)

    # 4. Prepare Submission 
    submission_data = {"username": username.strip(), "agent_code": agent_code, "answers": answers_payload}
    status_update = f"Agent finished. Submitting {len(answers_payload)} answers for user '{username}'..."
    print(status_update)

    # 5. Submit
    print(f"Submitting {len(answers_payload)} answers to: {submit_url}")
    try:
        response = requests.post(submit_url, json=submission_data, timeout=60)
        response.raise_for_status()
        result_data = response.json()
        final_status = (
            f"Submission Successful!\n"
            f"User: {result_data.get('username')}\n"
            f"Overall Score: {result_data.get('score', 'N/A')}% "
            f"({result_data.get('correct_count', '?')}/{result_data.get('total_attempted', '?')} correct)\n"
            f"Message: {result_data.get('message', 'No message received.')}"
        )
        print("Submission successful.")
        cleaned_final_status = re.sub(r'[^\x20-\x7E\n\r\t]+', '', final_status)
        cleaned_final_status = cleaned_final_status.strip()
        results_df = pd.DataFrame(results_log)
        return cleaned_final_status, results_df
    except requests.exceptions.HTTPError as e:
        error_detail = f"Server responded with status {e.response.status_code}."
        try:
            error_json = e.response.json()
            error_detail += f" Detail: {error_json.get('detail', e.response.text)}"
        except requests.exceptions.JSONDecodeError:
            error_detail += f" Response: {e.response.text[:500]}"
        status_message = f"Submission Failed: {error_detail}"
        print(status_message)
        results_df = pd.DataFrame(results_log)
        return status_message, results_df
    except requests.exceptions.Timeout:
        status_message = "Submission Failed: The request timed out."
        print(status_message)
        results_df = pd.DataFrame(results_log)
        return status_message, results_df
    except requests.exceptions.RequestException as e:
        status_message = f"Submission Failed: Network error - {e}"
        print(status_message)
        results_df = pd.DataFrame(results_log)
        return status_message, results_df
    except Exception as e:
        status_message = f"An unexpected error occurred during submission: {e}"
        print(status_message)
        results_df = pd.DataFrame(results_log)
        return status_message, results_df

# --- Build Gradio Interface using Blocks ---
with gr.Blocks() as demo:
    gr.Markdown("# Basic Agent Evaluation Runner")
    gr.Markdown(
        """
        **Instructions:**

        1.  Please clone this space, then modify the code to define your agent's logic, the tools, the necessary packages, etc ...
        2.  Log in to your Hugging Face account using the button below. This uses your HF username for submission.
        3.  Click 'Run Evaluation & Submit All Answers' to fetch questions, run your agent, submit answers, and see the score.

        ---
        **Disclaimers:**
        Once clicking on the "submit button, it can take quite some time ( this is the time for the agent to go through all the questions).
        This space provides a basic setup and is intentionally sub-optimal to encourage you to develop your own, more robust solution. For instance for the delay process of the submit button, a solution could be to cache the answers and submit in a seperate action or even to answer the questions in async.
        """
    )

    gr.LoginButton()

    run_button = gr.Button("Run Evaluation & Submit All Answers")

    status_output = gr.Textbox(label="Run Status / Submission Result", lines=5, interactive=False)
    results_table = gr.DataFrame(label="Questions and Agent Answers", wrap=True)

    run_button.click(
        fn=run_and_submit_all,
        outputs=[status_output, results_table]
    )

if __name__ == "__main__":
    print("\n" + "-"*30 + " App Starting " + "-"*30)
    space_host_startup = os.getenv("SPACE_HOST")
    space_id_startup = os.getenv("SPACE_ID") # Get SPACE_ID at startup

    if space_host_startup:
        print(f"✅ SPACE_HOST found: {space_host_startup}")
        print(f"   Runtime URL should be: https://{space_host_startup}.hf.space")
    else:
        print("ℹ️  SPACE_HOST environment variable not found (running locally?).")

    if space_id_startup: # Print repo URLs if SPACE_ID is found
        print(f"✅ SPACE_ID found: {space_id_startup}")
        print(f"   Repo URL: https://huggingface.co/spaces/{space_id_startup}")
        print(f"   Repo Tree URL: https://huggingface.co/spaces/{space_id_startup}/tree/main")
    else:
        print("ℹ️  SPACE_ID environment variable not found (running locally?). Repo URL cannot be determined.")

    print("-"*(60 + len(" App Starting ")) + "\n")

    print("Launching Gradio Interface for Basic Agent Evaluation...")
    demo.launch(debug=True, share=False)