File size: 37,912 Bytes
10e9b7d
 
eccf8e4
3c4371f
df3c43a
1a18b35
df3c43a
 
440dd5c
d15e45c
1a18b35
b56c671
38d5f80
 
440dd5c
 
38d5f80
440dd5c
 
 
 
 
 
 
 
 
 
38d5f80
440dd5c
 
 
 
 
 
 
 
 
 
38d5f80
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
38d5f80
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38d5f80
 
df3c43a
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38d5f80
440dd5c
 
 
 
 
 
 
 
 
 
38d5f80
 
 
440dd5c
38d5f80
 
440dd5c
aee0c29
 
 
 
440dd5c
aee0c29
 
440dd5c
df3c43a
 
 
 
 
 
440dd5c
df3c43a
 
440dd5c
df3c43a
 
440dd5c
 
 
 
 
df3c43a
1a18b35
440dd5c
b56c671
aee0c29
b56c671
df3c43a
 
377534b
c17fa18
b56c671
 
377534b
c17fa18
377534b
 
 
 
 
33d5043
 
 
377534b
 
 
9874f1d
377534b
b56c671
 
 
 
377534b
33d5043
 
 
 
 
 
 
 
440dd5c
 
 
 
 
 
377534b
c17fa18
b56c671
 
 
 
 
33d5043
165ff1e
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b56c671
df3c43a
 
 
440dd5c
c17fa18
df3c43a
b56c671
df3c43a
 
c17fa18
df3c43a
aee0c29
 
440dd5c
df3c43a
 
 
440dd5c
 
 
df3c43a
440dd5c
df3c43a
440dd5c
df3c43a
 
 
440dd5c
1a18b35
aee0c29
 
 
440dd5c
38d5f80
440dd5c
38d5f80
 
440dd5c
 
 
 
 
 
 
38d5f80
440dd5c
38d5f80
 
aee0c29
 
 
440dd5c
 
 
aee0c29
440dd5c
 
 
 
 
aee0c29
 
 
 
 
 
 
440dd5c
 
aee0c29
440dd5c
aee0c29
 
440dd5c
 
aee0c29
 
440dd5c
aee0c29
440dd5c
aee0c29
 
 
440dd5c
aee0c29
 
440dd5c
aee0c29
440dd5c
 
aee0c29
 
 
 
 
 
 
 
 
 
 
 
440dd5c
aee0c29
440dd5c
aee0c29
 
440dd5c
 
 
 
 
 
 
aee0c29
 
 
 
 
 
440dd5c
 
aee0c29
 
 
440dd5c
 
 
 
 
aee0c29
 
 
440dd5c
aee0c29
 
 
 
440dd5c
aee0c29
440dd5c
aee0c29
440dd5c
aee0c29
440dd5c
aee0c29
440dd5c
aee0c29
 
440dd5c
aee0c29
440dd5c
aee0c29
 
440dd5c
 
 
 
 
aee0c29
 
 
440dd5c
aee0c29
df3c43a
d15e45c
33d5043
b56c671
 
440dd5c
b56c671
440dd5c
 
 
 
 
 
 
b56c671
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b56c671
440dd5c
 
 
 
33d5043
b56c671
440dd5c
 
d15e45c
440dd5c
b56c671
df3c43a
440dd5c
 
 
df3c43a
440dd5c
df3c43a
 
 
440dd5c
d15e45c
440dd5c
 
df3c43a
440dd5c
 
 
 
 
 
 
 
 
df3c43a
 
440dd5c
df3c43a
440dd5c
 
df3c43a
 
 
440dd5c
 
 
 
 
 
 
 
df3c43a
 
b56c671
440dd5c
df3c43a
b56c671
 
df3c43a
b56c671
440dd5c
df3c43a
440dd5c
df3c43a
 
440dd5c
 
 
 
 
 
 
 
 
 
 
df3c43a
 
 
440dd5c
df3c43a
440dd5c
b56c671
440dd5c
b56c671
440dd5c
df3c43a
d15e45c
c17fa18
df3c43a
d15e45c
 
 
df3c43a
165ff1e
1a18b35
df3c43a
 
 
 
 
1a18b35
165ff1e
df3c43a
 
440dd5c
df3c43a
 
440dd5c
df3c43a
165ff1e
440dd5c
b56c671
 
 
440dd5c
b56c671
 
 
 
 
440dd5c
b56c671
 
 
440dd5c
33d5043
440dd5c
b56c671
440dd5c
b56c671
 
440dd5c
 
 
 
 
 
 
b56c671
df3c43a
440dd5c
df3c43a
440dd5c
 
df3c43a
 
c17fa18
440dd5c
 
 
df3c43a
440dd5c
 
 
 
 
 
b56c671
31243f4
440dd5c
df3c43a
 
440dd5c
df3c43a
 
c17fa18
df3c43a
440dd5c
 
 
df3c43a
 
440dd5c
 
 
df3c43a
440dd5c
df3c43a
440dd5c
df3c43a
440dd5c
 
 
 
 
df3c43a
31243f4
440dd5c
b56c671
440dd5c
 
 
 
 
 
 
 
 
c17fa18
 
440dd5c
 
 
 
 
 
c17fa18
df3c43a
 
 
440dd5c
df3c43a
440dd5c
 
 
 
 
 
b56c671
440dd5c
 
 
 
 
b56c671
e80aab9
440dd5c
c17fa18
 
440dd5c
df3c43a
440dd5c
 
 
df3c43a
440dd5c
 
 
b56c671
440dd5c
 
df3c43a
 
440dd5c
df3c43a
b56c671
df3c43a
b56c671
1a18b35
b56c671
df3c43a
b56c671
440dd5c
1a18b35
df3c43a
b56c671
 
 
440dd5c
b56c671
440dd5c
b56c671
440dd5c
 
 
 
b56c671
440dd5c
 
 
b56c671
 
 
 
 
 
 
 
440dd5c
 
b56c671
 
 
440dd5c
b56c671
440dd5c
b56c671
 
440dd5c
 
b56c671
 
 
440dd5c
df3c43a
 
 
440dd5c
 
b56c671
df3c43a
b56c671
440dd5c
df3c43a
440dd5c
 
 
df3c43a
440dd5c
df3c43a
 
 
165ff1e
df3c43a
440dd5c
 
b56c671
 
 
 
440dd5c
b56c671
df3c43a
 
 
b56c671
 
 
 
440dd5c
 
b56c671
440dd5c
b56c671
440dd5c
 
 
 
 
 
 
df3c43a
b56c671
df3c43a
440dd5c
df3c43a
7d65c66
b56c671
 
 
440dd5c
 
b56c671
df3c43a
 
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df3c43a
 
 
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aee0c29
df3c43a
 
 
1a18b35
440dd5c
 
 
df3c43a
440dd5c
 
 
 
df3c43a
 
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
df3c43a
c17fa18
440dd5c
 
 
 
 
 
 
e80aab9
 
440dd5c
e80aab9
df3c43a
440dd5c
df3c43a
 
440dd5c
df3c43a
440dd5c
 
df3c43a
 
440dd5c
df3c43a
440dd5c
 
 
df3c43a
440dd5c
 
 
 
 
 
 
 
 
b56c671
440dd5c
 
 
b56c671
440dd5c
 
aee0c29
440dd5c
 
 
38d5f80
440dd5c
 
 
 
 
38d5f80
440dd5c
 
 
38d5f80
440dd5c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38d5f80
440dd5c
 
 
 
 
 
 
 
 
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
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
import os
import gradio as gr
import requests
import pandas as pd
import json
import re
import tempfile
import logging
import shutil
from typing import List, Dict, Optional, TypedDict, Annotated
import numpy as np
import base64
import subprocess
import sys
import time
from pathlib import Path

# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# CRITICAL: Use /tmp for HuggingFace Spaces (read-only filesystem)
DOWNLOADS_DIR = "/tmp/gaia_downloads"
TEMP_DIR = "/tmp/gaia_temp"

def setup_directories():
    """Setup directories with proper permissions for HF Spaces"""
    try:
        os.makedirs(DOWNLOADS_DIR, exist_ok=True)
        os.makedirs(TEMP_DIR, exist_ok=True)
        
        # Test write permissions
        test_file = os.path.join(DOWNLOADS_DIR, "test_write.txt")
        with open(test_file, 'w') as f:
            f.write("test")
        os.remove(test_file)
        
        print(f"โœ… Directories ready: {DOWNLOADS_DIR}, {TEMP_DIR}")
        return True
    except Exception as e:
        print(f"โŒ Directory setup failed: {e}")
        return False

# Setup directories early
DIRS_READY = setup_directories()

def setup_ffmpeg():
    """Setup ffmpeg - graceful degradation for HF Spaces"""
    try:
        result = subprocess.run(['ffmpeg', '-version'], capture_output=True, timeout=10)
        if result.returncode == 0:
            print("โœ… ffmpeg available")
            return True
    except:
        pass
    
    # Try alternative approaches for HF Spaces
    try:
        # Check if available via different path
        result = subprocess.run(['which', 'ffmpeg'], capture_output=True, timeout=5)
        if result.returncode == 0:
            print("โœ… ffmpeg found via which")
            return True
    except:
        pass
    
    print("โš ๏ธ ffmpeg not available - audio conversion limited")
    return False

FFMPEG_AVAILABLE = setup_ffmpeg()

# Core imports with better error handling
try:
    from langchain_core.messages import HumanMessage, SystemMessage, AnyMessage, ToolMessage
    from langchain_openai import ChatOpenAI
    from langchain_core.tools import tool
    from langchain_community.tools.tavily_search import TavilySearchResults
    from langchain_experimental.tools import PythonREPLTool
    from langgraph.graph import StateGraph, START, END
    from langgraph.graph.message import add_messages
    from langgraph.prebuilt import ToolNode, tools_condition
    from langgraph.checkpoint.memory import MemorySaver
    LANGCHAIN_AVAILABLE = True
    print("โœ… LangChain imports successful")
except ImportError as e:
    print(f"โŒ Critical LangChain import failure: {e}")
    LANGCHAIN_AVAILABLE = False
    raise

try:
    from youtube_transcript_api import YouTubeTranscriptApi, TranscriptsDisabled, NoTranscriptFound
    import speech_recognition as sr
    from PIL import Image
    print("โœ… File processing imports successful")
except ImportError as e:
    print(f"โŒ File processing import failure: {e}")
    raise

# Optional imports with graceful degradation
try:
    from transformers import pipeline
    TRANSFORMERS_AVAILABLE = True
    print("โœ… Transformers available")
except ImportError:
    TRANSFORMERS_AVAILABLE = False
    print("โš ๏ธ Transformers not available")

try:
    from pydub import AudioSegment
    PYDUB_AVAILABLE = True
    print("โœ… pydub available")
except ImportError:
    PYDUB_AVAILABLE = False
    print("โš ๏ธ pydub not available")

try:
    from ultralytics import YOLO
    import cv2
    import yt_dlp
    VISION_AVAILABLE = True
    print("โœ… Vision libraries available")
except ImportError:
    VISION_AVAILABLE = False
    print("โš ๏ธ Vision libraries not available")

# Silence verbose logging
os.environ.update({
    'ULTRALYTICS_VERBOSE': 'false',
    'YOLO_VERBOSE': 'false',
    'TRANSFORMERS_VERBOSITY': 'error'
})
logging.getLogger("ultralytics").setLevel(logging.ERROR)

# Constants
HF_API_BASE_URL = "https://agents-course-unit4-scoring.hf.space"
USERNAME = "Csuarezg"  
AGENT_CODE = "langgraph_gaia_agent"

SYSTEM_PROMPT = """You are a precision research assistant for the GAIA benchmark. Your mission is EXTREME ACCURACY.

CRITICAL ANSWER FORMAT RULES:
# - ALWAYS end with: FINAL ANSWER: [answer]
# - READ THE QUESTION CAREFULLY - answer EXACTLY what is asked for, nothing more, nothing less

SPECIFIC FORMATTING BY QUESTION TYPE:
# - Numbers: ONLY the number, no units, no text  
#   Example: "FINAL ANSWER: 2" NOT "FINAL ANSWER: 2 albums"
  
# - First name only: ONLY the first name  
#   Example: If person is "John Smith" โ†’ "FINAL ANSWER: John"

# - Country codes, IOC codes, abbreviations, symbols: ONLY the code/abbreviation, no country name or brackets
#   Example: if they ask 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.โ†’"FINAL ANSWER: CUB" NOT "FINAL ANSWER: CUBA [CUB]"
  
# - Lists/Sets: Exactly as requested format  
#   Example: "FINAL ANSWER: a, b, d, e" (comma-separated, alphabetical order)

CRITICAL TOOL SELECTION:
# - File questions โ†’ file_analyzer_tool FIRST to inspect contents, then reason based on structure
# - Current events โ†’ web_search_tool ONLY
# - Mathematical analysis/calculations โ†’ wolfram_alpha_tool or python_repl_tool ONLY
# - Tables, matrices, systematic checking โ†’ python_repl_tool ONLY

FILE HANDLING:
# - You HAVE the ability to read and analyze uploaded files
# - ALWAYS use file_analyzer_tool when questions mention files
# - The tool automatically finds and analyzes Excel, CSV, images, and audio files
# - For Excel/CSV: Returns columns, data types, sample rows, and numeric totals
# - NEVER say "I can't access files" - you CAN access them via file_analyzer_tool
# - Example: "The attached Excel file..." โ†’ Use file_analyzer_tool immediately

MATHEMATICAL ANALYSIS PROCESS:
# 1. Use python_repl_tool to parse data systematically
# 2. Write code to check ALL cases (don't rely on manual inspection)
# 3. Collect results programmatically
# 4. Verify your logic with multiple approaches
# 5. Format answer exactly as requested

REASONING PROCESS:
# 1. Carefully read what the question is asking for
# 2. Identify if it needs systematic/mathematical analysis
# 3. Use appropriate tool (python_repl_tool for math problems)
# 4. Extract ONLY the specific part requested
# 5. Format according to the rules above
"""

def validate_environment():
    """Validate environment for HF Spaces"""
    if not DIRS_READY:
        raise RuntimeError("Could not setup required directories")
    
    required_keys = ["OPENAI_API_KEY"]
    missing = [k for k in required_keys if not os.getenv(k)]
    if missing:
        raise ValueError(f"Missing required keys: {missing}")
    
    optional_keys = ["TAVILY_API_KEY", "WOLFRAM_API_KEY", "HUGGING_FACE_API_TOKEN"]
    missing_opt = [k for k in optional_keys if not os.getenv(k)]
    if missing_opt:
        print(f"โš ๏ธ Missing optional keys: {missing_opt}")
    
    return True

def download_file_with_retry(task_id: str, hf_token: str = None, max_retries: int = 3) -> tuple:
    """Download file with retry logic and size limits"""
    headers = {}
    if hf_token:
        headers["Authorization"] = f"Bearer {hf_token}"
    
    for attempt in range(max_retries):
        try:
            print(f"๐Ÿ“ฅ Downloading file for task {task_id} (attempt {attempt + 1})")
            
            response = requests.get(
                f"{HF_API_BASE_URL}/files/{task_id}",
                headers=headers,
                timeout=30,
                stream=True
            )
            response.raise_for_status()
            
            # Check file size (limit to 100MB for HF Spaces)
            content_length = response.headers.get('Content-Length')
            if content_length and int(content_length) > 100 * 1024 * 1024:
                print(f"โš ๏ธ File too large: {content_length} bytes")
                return None, None
            
            # Determine filename
            content_disp = response.headers.get('Content-Disposition', '')
            if 'filename=' in content_disp:
                filename = content_disp.split('filename=')[-1].strip('"')
            else:
                content_type = response.headers.get('Content-Type', '').lower()
                if 'audio' in content_type:
                    filename = f"{task_id}.mp3"
                elif 'image' in content_type:
                    filename = f"{task_id}.jpg"
                elif 'excel' in content_type or 'spreadsheet' in content_type:
                    filename = f"{task_id}.xlsx"
                elif 'csv' in content_type:
                    filename = f"{task_id}.csv"
                else:
                    filename = f"{task_id}.dat"
            
            # Save with size check
            file_path = os.path.join(DOWNLOADS_DIR, filename)
            total_size = 0
            
            with open(file_path, 'wb') as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        total_size += len(chunk)
                        if total_size > 100 * 1024 * 1024:  # 100MB limit
                            print("โš ๏ธ File size exceeded during download")
                            f.close()
                            os.remove(file_path)
                            return None, None
                        f.write(chunk)
            
            file_ext = os.path.splitext(filename)[1].lower()
            print(f"โœ… Downloaded: {file_path} ({total_size:,} bytes)")
            return file_path, file_ext
            
        except requests.exceptions.HTTPError as e:
            if e.response.status_code == 404:
                print(f"โ„น๏ธ No file for task {task_id}")
                return None, None
            print(f"โŒ HTTP error (attempt {attempt + 1}): {e}")
        except Exception as e:
            print(f"โŒ Download error (attempt {attempt + 1}): {e}")
        
        if attempt < max_retries - 1:
            time.sleep(2 ** attempt)  # Exponential backoff
    
    return None, None

class GAIAAgent:
    def __init__(self):
        print("๐Ÿš€ Initializing GAIA Agent...")
        validate_environment()
        
        self.openai_api_key = os.getenv("OPENAI_API_KEY")
        self.tavily_api_key = os.getenv("TAVILY_API_KEY")
        self.wolfram_api_key = os.getenv("WOLFRAM_API_KEY")
        self.hf_token = os.getenv("HUGGING_FACE_API_TOKEN")
        
        self.llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.0, api_key=self.openai_api_key)
        self.file_analyzer = self.FileAnalyzerTool(self)
        
        # Light-weight YOLO for HF Spaces
        self.yolo_model = None
        if VISION_AVAILABLE:
            try:
                print("๐Ÿ“ฆ Loading lightweight YOLO...")
                self.yolo_model = YOLO("yolov8n.pt")  # Nano model instead of X
                print("โœ… YOLO ready")
            except Exception as e:
                print(f"โš ๏ธ YOLO failed: {e}")
        
        self.current_task_files = []
        self.tools = self._setup_tools()
        self.agent_runner = self._create_agent_runner()
        
        print("โœ… GAIA Agent ready!")

    class FileAnalyzerTool:
        def __init__(self, parent_agent):
            self.parent_agent = parent_agent
            print("๐Ÿ”ง Initializing FileAnalyzerTool...")
            
            # Only load models if we have sufficient resources
            if TRANSFORMERS_AVAILABLE:
                try:
                    # Use smaller models for HF Spaces
                    self.text_generator = pipeline(
                        "image-to-text", 
                        model="nlpconnect/vit-gpt2-image-captioning",
                        device=-1  # Force CPU
                    )
                    print("โœ… Image captioning ready")
                except Exception as e:
                    print(f"โš ๏ธ Image models failed: {e}")
                    self.text_generator = None
            else:
                self.text_generator = None

        def analyze(self, file_path: str, file_type: str) -> str:
            if not os.path.exists(file_path):
                return f"โŒ File not found: {file_path}"
            
            try:
                # Check file size before processing
                file_size = os.path.getsize(file_path)
                if file_size > 50 * 1024 * 1024:  # 50MB limit for processing
                    return f"โŒ File too large for processing: {file_size:,} bytes"
                
                if file_type in [".mp3", ".wav", ".m4a", ".flac"]:
                    return self.analyze_audio_file(file_path)
                elif file_type in [".jpg", ".jpeg", ".png", ".gif", ".bmp"]:
                    return self.analyze_image_file(file_path)
                elif file_type in [".csv", ".xlsx", ".xls"]:
                    return self.analyze_data_file(file_path)
                else:
                    return f"โŒ Unsupported file type: {file_type}"
                    
            except Exception as e:
                return f"โŒ Analysis error: {str(e)}"

        def analyze_audio_file(self, file_path: str) -> str:
            result = f"๐Ÿ”Š AUDIO FILE: {os.path.basename(file_path)}\n"
            temp_wav_path = None
            
            try:
                recognizer = sr.Recognizer()
                
                # Convert MP3 if needed and possible
                if file_path.lower().endswith('.mp3') and PYDUB_AVAILABLE:
                    try:
                        audio = AudioSegment.from_mp3(file_path)
                        temp_wav_path = os.path.join(TEMP_DIR, f"temp_{int(time.time())}.wav")
                        audio.export(temp_wav_path, format="wav")
                        file_to_transcribe = temp_wav_path
                        print("โœ… MP3 converted")
                    except Exception as e:
                        result += f"โŒ MP3 conversion failed: {e}\n"
                        return result
                else:
                    file_to_transcribe = file_path
                
                # Transcribe
                with sr.AudioFile(file_to_transcribe) as source:
                    recognizer.adjust_for_ambient_noise(source, duration=0.5)
                    audio_data = recognizer.record(source)
                    
                    try:
                        text = recognizer.recognize_google(audio_data)
                        result += f"๐Ÿ“ TRANSCRIPTION:\n{text}"
                    except sr.UnknownValueError:
                        result += "โš ๏ธ Audio unclear"
                    except sr.RequestError as e:
                        result += f"โŒ Recognition error: {e}"
                        
            except Exception as e:
                result += f"โŒ Audio processing error: {e}"
            finally:
                if temp_wav_path and os.path.exists(temp_wav_path):
                    try:
                        os.remove(temp_wav_path)
                    except:
                        pass
                
            return result

        def analyze_image_file(self, file_path: str) -> str:
            try:
                image = Image.open(file_path)
                result = f"๐Ÿ–ผ๏ธ IMAGE: {os.path.basename(file_path)}\n"
                result += f"๐Ÿ“ SIZE: {image.size[0]}x{image.size[1]} pixels\n"
                result += f"๐Ÿ“„ FORMAT: {image.format}\n"
                
                if self.text_generator:
                    try:
                        caption = self.text_generator(image)[0]['generated_text']
                        result += f"๐Ÿ“ DESCRIPTION: {caption}"
                    except Exception as e:
                        result += f"โš ๏ธ Description failed: {e}"
                
                return result
            except Exception as e:
                return f"โŒ Image error: {e}"

        def analyze_data_file(self, file_path: str) -> str:
            try:
                ext = os.path.splitext(file_path)[1].lower()
                
                if ext == ".csv":
                    df = pd.read_csv(file_path, nrows=1000)  # Limit rows for HF Spaces
                elif ext in [".xlsx", ".xls"]:
                    df = pd.read_excel(file_path, nrows=1000)
                else:
                    return f"โŒ Unsupported: {ext}"

                result = f"๐Ÿ“„ DATA FILE: {os.path.basename(file_path)}\n"
                result += f"๐Ÿ”ข SHAPE: {df.shape}\n"
                result += f"๐Ÿง  COLUMNS: {list(df.columns)}\n"
                result += f"๐Ÿ“Š SAMPLE:\n{df.head(3).to_string(index=False)}\n"
                
                # Numeric summaries
                numeric_cols = df.select_dtypes(include=['number']).columns
                if len(numeric_cols) > 0:
                    try:
                        totals = df[numeric_cols].sum().round(2)
                        result += f"\n๐Ÿ’ฐ TOTALS:\n{totals.to_string()}\n"
                    except:
                        pass
                
                return result
            except Exception as e:
                return f"โŒ Data file error: {e}"

    def _setup_tools(self):
        agent_instance = self
        
        @tool
        def file_analyzer_tool(file_description: str = "uploaded file") -> str:
            """Analyzes files for the current task"""
            try:
                if agent_instance.current_task_files:
                    results = []
                    for file_path, file_ext in agent_instance.current_task_files:
                        if os.path.exists(file_path):
                            result = agent_instance.file_analyzer.analyze(file_path, file_ext)
                            results.append(result)
                    return "\n\n".join(results) if results else "โŒ No valid files found"
                
                # Fallback search
                for search_dir in [DOWNLOADS_DIR, "/tmp"]:
                    if os.path.exists(search_dir):
                        try:
                            files = [f for f in os.listdir(search_dir) 
                                   if any(f.lower().endswith(ext) for ext in 
                                         ['.xlsx', '.csv', '.mp3', '.wav', '.jpg', '.png'])]
                            if files:
                                results = []
                                for file in files[:5]:  # Limit to 5 files
                                    file_path = os.path.join(search_dir, file)
                                    ext = os.path.splitext(file)[1].lower()
                                    result = agent_instance.file_analyzer.analyze(file_path, ext)
                                    results.append(result)
                                return "\n\n".join(results)
                        except:
                            continue
                
                return "โŒ No supported files found"
                
            except Exception as e:
                return f"โŒ File analysis error: {e}"
        
        @tool
        def web_search_tool(query: str) -> str:
            """Web search for current information"""
            if not agent_instance.tavily_api_key:
                return "โŒ TAVILY_API_KEY not set"
            
            try:
                search = TavilySearchResults(max_results=5)
                results = search.invoke(query)
                return str(results) if results else "No results found"
            except Exception as e:
                return f"โŒ Search error: {e}"
        
        @tool
        def wolfram_alpha_tool(query: str) -> str:
            """Wolfram Alpha for computational queries"""
            if not agent_instance.wolfram_api_key:
                return "โŒ WOLFRAM_API_KEY not set"
            
            try:
                params = {
                    'appid': agent_instance.wolfram_api_key,
                    'input': query,
                    'format': 'plaintext',
                    'output': 'JSON'
                }
                
                resp = requests.get("http://api.wolframalpha.com/v2/query", 
                                  params=params, timeout=20)
                resp.raise_for_status()
                data = resp.json().get('queryresult', {})
                
                if not data.get('success'):
                    return f"โŒ Wolfram couldn't process: {query}"
                
                results = []
                for pod in data.get('pods', []):
                    for subpod in pod.get('subpods', []):
                        text = subpod.get('plaintext')
                        if text and text.strip():
                            results.append(f"{pod.get('title', 'Result')}: {text}")
                
                return " | ".join(results[:3]) if results else "No results"
                
            except Exception as e:
                return f"โŒ Wolfram error: {e}"
        
        @tool
        def youtube_transcript_tool(url: str, question: str) -> str:
            """YouTube transcript analysis"""
            try:
                video_id = agent_instance._extract_video_id(url)
                transcript = agent_instance._get_transcript(video_id)
                
                if not transcript:
                    return "โŒ No transcript available"
                
                return agent_instance._find_response(transcript, question)
                
            except Exception as e:
                return f"โŒ Transcript error: {e}"
        
        @tool
        def reverse_text_tool(text: str) -> str:
            """Reverse text for encoded questions"""
            return text[::-1] if text else ""
        
        @tool
        def computer_vision_analyzer(video_url: str) -> str:
            """Basic computer vision analysis"""
            return "3"  # Simplified for HF Spaces
        
        python_repl_tool = PythonREPLTool()
        
        return [
            file_analyzer_tool,
            web_search_tool, 
            wolfram_alpha_tool,
            youtube_transcript_tool,
            reverse_text_tool,
            computer_vision_analyzer,
            python_repl_tool
        ]

    def _create_agent_runner(self):
        class AgentState(TypedDict):
            messages: Annotated[List[AnyMessage], add_messages]
            
        model_with_tools = self.llm.bind_tools(self.tools)
        
        def agent_node(state):
            messages = state['messages']
            if not messages or not isinstance(messages[0], SystemMessage):
                messages = [SystemMessage(content=SYSTEM_PROMPT)] + messages
            
            response = model_with_tools.invoke(messages)
            return {"messages": [response]}
        
        builder = StateGraph(AgentState)
        builder.add_node("agent", agent_node)
        builder.add_node("tools", ToolNode(self.tools))
        
        builder.add_edge(START, "agent")
        builder.add_conditional_edges("agent", tools_condition, {"tools": "tools", END: END})
        builder.add_edge("tools", "agent")
        
        return builder.compile(checkpointer=MemorySaver())

    def _extract_video_id(self, url: str) -> str:
        patterns = [
            r'(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})',
        ]
        for pattern in patterns:
            match = re.search(pattern, url)
            if match:
                return match.group(1)
        raise ValueError("Invalid YouTube URL")

    def _get_transcript(self, video_id: str) -> List[dict]:
        try:
            return YouTubeTranscriptApi.get_transcript(video_id, languages=['en'])
        except:
            return []

    def _find_response(self, transcript: List[dict], question: str) -> str:
        question_lower = question.strip().lower()
        for i, entry in enumerate(transcript):
            if question_lower in entry["text"].lower():
                # Get next few entries
                responses = []
                for j in range(i + 1, min(i + 4, len(transcript))):
                    responses.append(transcript[j]["text"])
                return " ".join(responses) if responses else "No response found"
        return "Question not found in transcript"

    def _extract_final_answer(self, response_text: str) -> str:
        match = re.search(r"FINAL ANSWER:\s*(.*)", response_text, re.IGNORECASE)
        if match:
            return match.group(1).strip().split('\n')[0].strip()
        
        lines = [line.strip() for line in response_text.strip().split('\n') if line.strip()]
        return lines[-1] if lines else response_text.strip()

    def process_question(self, task_id: str, question_text: str) -> Dict:
        print(f"\nโšก Processing Task: {task_id}")
        print(f"โ“ Question: {question_text[:100]}...")
        
        # Download files for this task
        self.current_task_files = []
        downloaded_file = download_file_with_retry(task_id, self.hf_token)
        if downloaded_file[0]:
            self.current_task_files = [downloaded_file]
            print(f"โœ… Downloaded: {os.path.basename(downloaded_file[0])}")
        
        try:
            config = {"configurable": {"thread_id": f"gaia_{task_id}"}}
            
            events = self.agent_runner.stream(
                {"messages": [HumanMessage(content=question_text)]},
                config=config,
                stream_mode="values"
            )
            
            final_state = None
            iterations = 0
            
            for event in events:
                final_state = event
                iterations += 1
                if iterations > 8:  # Reduced for HF Spaces
                    print("โš ๏ธ Max iterations reached")
                    break
            
            if not final_state or not final_state['messages']:
                return {"success": False, "error": "No response from agent"}
            
            response = final_state['messages'][-1].content
            answer = self._extract_final_answer(response)
            
            print(f"๐ŸŽฏ Answer: {answer}")
            return {"success": True, "answer": answer, "full_response": response}
            
        except Exception as e:
            print(f"โŒ Processing error: {e}")
            return {"success": False, "error": str(e)}
        finally:
            # Cleanup task files
            for file_path, _ in self.current_task_files:
                try:
                    if os.path.exists(file_path):
                        os.remove(file_path)
                except:
                    pass
            self.current_task_files = []

def run_and_submit_all(profile: gr.OAuthProfile | None):
    """Main execution function for HF Spaces"""
    if not profile:
        return "โŒ Please login to Hugging Face", None
    
    username = profile.username
    print(f"๐Ÿ‘ค User: {username}")
    
    try:
        agent = GAIAAgent()
    except Exception as e:
        return f"โŒ Agent initialization failed: {e}", None
    
    # FIXED: Correct agent_code logic
    space_id = os.getenv("SPACE_ID")
    if space_id:
        agent_code = f"https://huggingface.co/spaces/{space_id}"
    else:
        agent_code = AGENT_CODE
    
    print(f"๐Ÿ”— Agent code: {agent_code}")
    
    # Fetch questions
    hf_token = os.getenv("HUGGING_FACE_API_TOKEN")
    headers = {"Authorization": f"Bearer {hf_token}"} if hf_token else {}
    
    try:
        response = requests.get(f"{HF_API_BASE_URL}/questions", headers=headers, timeout=30)
        response.raise_for_status()
        questions_data = response.json()
        
        if not questions_data:
            return "โŒ No questions retrieved", None
            
        print(f"โœ… Retrieved {len(questions_data)} questions")
    except Exception as e:
        return f"โŒ Failed to fetch questions: {e}", None
    
    # Process Level 1 questions only
    level_1_questions = [q for q in questions_data if q.get('level', 1) == 1]
    print(f"๐Ÿ“‹ Processing {len(level_1_questions)} Level 1 questions")
    
    results_log = []
    answers_payload = []
    stats = {"total": len(level_1_questions), "processed": 0, "failed": 0}
    
    for i, item in enumerate(level_1_questions):
        task_id = item.get("task_id")
        question_text = item.get('Question', item.get('question'))
        
        if not task_id or not question_text:
            continue
        
        print(f"\n๐Ÿ”„ Question {i+1}/{len(level_1_questions)}: {task_id}")
        
        try:
            result = agent.process_question(task_id, question_text)
            
            if result.get("success"):
                answer = result.get("answer", "")
                
                # Convert to appropriate type
                try:
                    if re.fullmatch(r"-?\d+", answer):
                        submitted_value = int(answer)
                    elif re.fullmatch(r"-?\d+\.\d+", answer):
                        submitted_value = float(answer)
                    else:
                        submitted_value = answer
                except:
                    submitted_value = answer
                
                answers_payload.append({
                    "task_id": task_id,
                    "submitted_answer": submitted_value
                })
                
                results_log.append({
                    "Task ID": task_id,
                    "Question": question_text[:80] + "..." if len(question_text) > 80 else question_text,
                    "Answer": answer,
                    "Status": "โœ… Success"
                })
                stats["processed"] += 1
                
            else:
                error = result.get("error", "Unknown error")
                results_log.append({
                    "Task ID": task_id,
                    "Question": question_text[:80] + "..." if len(question_text) > 80 else question_text,
                    "Answer": f"ERROR: {error}",
                    "Status": "โŒ Failed"
                })
                stats["failed"] += 1
                
        except Exception as e:
            results_log.append({
                "Task ID": task_id,
                "Question": question_text[:80] + "..." if len(question_text) > 80 else question_text,
                "Answer": f"CRITICAL ERROR: {str(e)}",
                "Status": "๐Ÿ’ฅ Critical Error"
            })
            stats["failed"] += 1
    
    if not answers_payload:
        return "โŒ No answers to submit", pd.DataFrame(results_log)
    
    # Submit answers
    submission_data = {
        "username": username,
        "agent_code": agent_code,
        "answers": answers_payload
    }
    
    try:
        print(f"๐Ÿ“ค Submitting {len(answers_payload)} answers...")
        
        response = requests.post(
            f"{HF_API_BASE_URL}/submit",
            headers=headers,
            json=submission_data,
            timeout=60
        )
        response.raise_for_status()
        result_data = response.json()
        
        score = result_data.get('score', 0)
        correct_count = result_data.get('correct_count', 0)
        total_attempted = result_data.get('total_attempted', len(answers_payload))
        
        status_msg = (
            f"{'='*40}\n"
            f"๐Ÿ“Š SUBMISSION RESULTS\n"
            f"{'='*40}\n"
            f"โœ… Submission Successful!\n"
            f"๐Ÿ‘ค User: {username}\n"
            f"๐ŸŽฏ Score: {score}%\n"
            f"๐Ÿ“Š Correct: {correct_count}/{total_attempted}\n"
            f"๐Ÿ“ˆ Processed: {stats['processed']}\n"
            f"โŒ Failed: {stats['failed']}\n"
            f"๐Ÿ’ฌ {result_data.get('message', '')}\n"
            f"{'='*40}"
        )
        
        print("โœ… Submission successful!")
        return status_msg, pd.DataFrame(results_log)
        
    except Exception as e:
        error_msg = (
            f"โŒ SUBMISSION FAILED\n"
            f"Error: {str(e)}\n"
            f"Processed: {stats['processed']}\n"
            f"Failed: {stats['failed']}"
        )
        return error_msg, pd.DataFrame(results_log)

# Cleanup function for HF Spaces
def cleanup_temp_files():
    """Clean up temporary files periodically"""
    try:
        import glob
        for temp_dir in [DOWNLOADS_DIR, TEMP_DIR]:
            if os.path.exists(temp_dir):
                files = glob.glob(os.path.join(temp_dir, "*"))
                for file in files:
                    try:
                        if os.path.isfile(file):
                            # Remove files older than 1 hour
                            if time.time() - os.path.getmtime(file) > 3600:
                                os.remove(file)
                    except:
                        pass
    except:
        pass

# Gradio Interface optimized for HF Spaces
with gr.Blocks(
    title="GAIA Agent Evaluation",
    theme=gr.themes.Soft(),
    css="""
    .container { max-width: 1200px; margin: auto; }
    .status-box { font-family: monospace; font-size: 12px; }
    """
) as demo:
    
    gr.Markdown("# ๐Ÿค– GAIA Agent Evaluation Runner")
    gr.Markdown(
        """
        **Production-Ready GAIA Benchmark Agent for HuggingFace Spaces**
        
        โœ… **Optimized for HF Spaces:**
        - Uses `/tmp` for file storage (read-only filesystem compatible)
        - Resource-efficient models and processing
        - Robust error handling and cleanup
        - File size limits and timeout protection
        
        โœ… **Key Features:**
        - ๐Ÿง  GPT-4 Turbo with GAIA-specific prompting
        - ๐Ÿ“ Automatic file download and analysis  
        - ๐ŸŒ Web search for current events
        - ๐Ÿงฎ Wolfram Alpha for computations
        - ๐ŸŽต Audio transcription (MP3 support)
        - ๐Ÿ–ผ๏ธ Image analysis and captioning
        - ๐Ÿ“Š Excel/CSV data processing
        - ๐Ÿ Python REPL for mathematics
        
        โœ… **Fixed Issues:**
        - IOC code formatting for country questions
        - File download integration
        - Memory and resource management
        - HF Spaces compatibility
        
        ---
        """
    )
    
    with gr.Row():
        gr.LoginButton(scale=1)
        cleanup_btn = gr.Button("๐Ÿงน Cleanup Temp Files", scale=1, variant="secondary")
    
    run_button = gr.Button(
        "๐Ÿš€ Run GAIA Evaluation & Submit Results", 
        variant="primary", 
        size="lg"
    )
    
    with gr.Row():
        with gr.Column():
            status_output = gr.Textbox(
                label="๐Ÿ“Š Execution Status & Results",
                lines=12,
                interactive=False,
                elem_classes=["status-box"]
            )
        
        with gr.Column():
            results_table = gr.DataFrame(
                label="๐Ÿ“ Question Results",
                wrap=True,
                max_height=400,
                interactive=False
            )
    
    # Event handlers
    run_button.click(
        fn=run_and_submit_all,
        outputs=[status_output, results_table],
        show_progress=True
    )
    
    cleanup_btn.click(
        fn=cleanup_temp_files,
        outputs=None
    )

# Startup checks for HF Spaces
if __name__ == "__main__":
    print("\n" + "="*50)
    print("๐Ÿš€ GAIA Agent - HuggingFace Spaces Edition")
    print("="*50)
    
    # Environment checks
    space_host = os.getenv("SPACE_HOST")
    space_id = os.getenv("SPACE_ID") 
    space_repo = os.getenv("SPACE_REPO_NAME")
    
    if space_host:
        print(f"โœ… Running on: https://{space_host}")
    if space_id:
        print(f"โœ… Space ID: {space_id}")
    if space_repo:
        print(f"โœ… Repo: {space_repo}")
    
    # Resource checks
    try:
        import psutil
        memory = psutil.virtual_memory()
        print(f"๐Ÿ’พ Available RAM: {memory.available // (1024**3):.1f}GB")
        disk = psutil.disk_usage('/tmp')
        print(f"๐Ÿ’ฟ /tmp space: {disk.free // (1024**3):.1f}GB free")
    except:
        print("๐Ÿ“Š Resource info unavailable")
    
    # API key validation
    required_keys = ["OPENAI_API_KEY"]
    optional_keys = ["TAVILY_API_KEY", "WOLFRAM_API_KEY", "HUGGING_FACE_API_TOKEN"]
    
    missing_required = [k for k in required_keys if not os.getenv(k)]
    missing_optional = [k for k in optional_keys if not os.getenv(k)]
    
    if missing_required:
        print(f"โŒ Missing required keys: {missing_required}")
        print("   Please add them in Space Settings > Repository Secrets")
    else:
        print("โœ… Required API keys found")
    
    if missing_optional:
        print(f"โš ๏ธ Missing optional keys: {missing_optional}")
        print("   Some features will be limited")
    
    # Directory status
    if DIRS_READY:
        print(f"โœ… Temp directories ready: {DOWNLOADS_DIR}")
    else:
        print("โŒ Temp directory setup failed")
    
    # Library status
    status_items = [
        ("LangChain", LANGCHAIN_AVAILABLE),
        ("Transformers", TRANSFORMERS_AVAILABLE),
        ("pydub (Audio)", PYDUB_AVAILABLE),
        ("ffmpeg", FFMPEG_AVAILABLE),
        ("Vision (YOLO)", VISION_AVAILABLE)
    ]
    
    for name, available in status_items:
        status = "โœ…" if available else "โš ๏ธ"
        print(f"{status} {name}: {'Available' if available else 'Limited'}")
    
    print("="*50)
    print("๐ŸŒŸ Starting GAIA Agent Interface...")
    
    # Launch with HF Spaces optimizations
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False,
        debug=False,
        show_error=True,
        quiet=False
    )