File size: 39,326 Bytes
be2339f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
be2339f
 
 
 
 
5d4bed3
be2339f
 
5d4bed3
 
 
0ed9c08
 
 
 
 
 
 
 
 
 
be2339f
0ed9c08
be2339f
0ed9c08
 
 
 
 
 
 
 
be2339f
 
 
 
 
 
 
0ed9c08
be2339f
0ed9c08
 
5d4bed3
0ed9c08
 
 
 
 
 
 
be2339f
5d4bed3
 
 
be2339f
5d4bed3
be2339f
5d4bed3
 
 
 
 
 
 
be2339f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed9c08
5d4bed3
 
 
 
0ed9c08
be2339f
0ed9c08
 
5d4bed3
 
 
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
be2339f
 
 
 
 
 
 
5d4bed3
be2339f
5d4bed3
 
 
 
 
 
be2339f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
 
 
 
 
 
 
be2339f
 
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed9c08
5d4bed3
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
0ed9c08
 
 
 
 
 
5d4bed3
0ed9c08
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be2339f
 
 
 
 
 
 
 
 
 
 
 
 
 
5d4bed3
 
 
 
be2339f
 
5d4bed3
be2339f
 
 
 
 
 
 
 
 
 
5d4bed3
 
be2339f
 
 
 
 
5d4bed3
be2339f
0ed9c08
 
5d4bed3
 
be2339f
 
 
 
 
 
5d4bed3
be2339f
5d4bed3
be2339f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
基于 smolagents 框架的 Hugging Face 数据集自然语言查询智能代理

功能:
1. 启动时自动获取数据集文件列表,作为系统提示词告知用户可用数据
2. 接收用户自然语言输入,从中提取数据集查询所需的参数信息
3. 使用提取的参数从 Hugging Face 数据集获取数据(支持 ZIP/CSV/Excel 等格式)
4. 返回结构化的查询结果

依赖安装:
    pip install 'smolagents[gradio]' requests pandas huggingface_hub openpyxl

使用前:
    1. 将文件顶部 HF_TOKEN 常量的值替换为个人 Hugging Face Settings 下申请的 Read Token(用于数据集访问)
    2. 将文件顶部 OPENAI_API_KEY 常量的值替换为你的 OpenAI API Key(用于 LLM 接入)
    3. 如使用第三方兼容服务,修改 OPENAI_API_BASE 常量的地址
"""

import os
import json
import logging
import re
from typing import Optional, List

from huggingface_hub import HfApi
from smolagents import Tool, ToolCallingAgent, OpenAIServerModel, GradioUI

# 导入专用查询函数(从 query_tools 子模块导入)
from query_tools.logbook_availability_query import query_logbook_availability
from query_tools.query_gfw import query_gfw

# ---------------------------------------------------------------------------
# 日志配置(提前配置,以便在后续代码中使用)
# ---------------------------------------------------------------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
)
logger = logging.getLogger("hf_data_agent")

# ===========================================================================
# Hugging Face 数据集访问 Token(从环境变量加载)
# ===========================================================================
HF_TOKEN = os.getenv('HF_TOKEN')  # 从环境变量 HF_TOKEN 读取

# 启动时校验 HF_TOKEN 是否存在,并给出提示
if not HF_TOKEN:
    logger.warning(
        "未找到环境变量 HF_TOKEN,将无法访问 Hugging Face 数据集。"
        "请设置环境变量: export HF_TOKEN=hf_你的Token"
    )

# ===========================================================================
# 数据集仓库 ID
# ===========================================================================
HF_DATASET_REPO = "squid-lab/squid_dataset"  # Hugging Face 数据集仓库 ID

# ===========================================================================
# OpenAI 模型配置常量(从环境变量加载)
# ===========================================================================
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')  # 从环境变量 OPENAI_API_KEY 读取
OPENAI_API_BASE = os.getenv('OPENAI_API_BASE', 'https://api.deepseek.com/v1')  # 默认 DeepSeek API 地址
MODEL_ID = os.getenv('MODEL_ID', 'deepseek-v4-flash')  # 默认使用 deepseek-v4-flash(推荐模型)

# 启动时校验 OPENAI_API_KEY 是否存在,并给出提示
if not OPENAI_API_KEY:
    logger.warning(
        "未找到环境变量 OPENAI_API_KEY,Agent 将无法调用 LLM。"
        "请设置环境变量: export OPENAI_API_KEY=sk_你的Key"
    )

# 注意:deepseek-chat 和 deepseek-reasoner 将于 2026/07/24 弃用
# 推荐使用新模型:deepseek-v4-flash(快速)或 deepseek-v4-pro(专业)

# ===========================================================================
# DeepSeek 思考模式配置(已停用)
# ===========================================================================
# 注意:DeepSeek 思考模式已停用,原因如下:
# 1. deepseek-chat 和 deepseek-reasoner 将于 2026/07/24 弃用
# 2. 新模型 deepseek-v4-flash/v4-pro 不需要特殊的思考模式适配
# 3. 思考模式不支持 tool_choice 参数,限制 Agent 工具调用能力
# 4. 使用标准 OpenAIServerModel 更稳定,兼容性更好
THINKING_MODE_ENABLED = False  # 已停用思考模式
THINKING_EFFORT = "high"  # 保留参数但不再使用


# ===========================================================================
# 数据集文件列表获取函数
# ===========================================================================
def get_dataset_file_list() -> List[str]:
    """
    从 Hugging Face 数据集仓库获取一级文件和文件夹名称列表。

    使用 HfApi.dataset_info() 获取数据集元信息,提取 siblings 中的文件路径,
    只保留一级目录/文件(不包含深层嵌套的文件)。

    Returns:
        一级文件和文件夹名称列表,如 ['中西太平洋WCPFC/', 'README.md', 'data.csv']
    """
    logger.info("正在获取数据集文件列表: %s", HF_DATASET_REPO)

    try:
        api = HfApi(token=HF_TOKEN)
        dataset_info = api.dataset_info(repo_id=HF_DATASET_REPO)

        # 提取所有文件路径
        all_files = [sibling.rfilename for sibling in dataset_info.siblings]

        # 只保留一级目录/文件(路径中不包含 '/' 的项,或者一级文件夹)
        first_level_items = set()
        for file_path in all_files:
            parts = file_path.split("/")
            # 一级文件(无子目录)
            if len(parts) == 1:
                first_level_items.add(parts[0])
            # 一级文件夹(取第一部分,添加 '/' 后缀标识为文件夹)
            else:
                first_level_items.add(parts[0] + "/")

        file_list = sorted(list(first_level_items))
        logger.info("获取到 %d 个一级文件/文件夹: %s", len(file_list), file_list)
        return file_list

    except Exception as e:
        logger.error("获取数据集文件列表失败: %s", e, exc_info=True)
        return []


def build_system_prompt_with_file_list() -> str:
    """
    构建包含数据集文件列表的系统提示词。

    在对话启动时告知用户当前可用的数据集文件,帮助用户了解可查询的内容。

    Returns:
        格式化的系统提示词字符串
    """
    file_list = get_dataset_file_list()

    if not file_list:
        return (
            "你是一个 Hugging Face 数据集查询助手。"
            "用户可以询问数据集内容,你将帮助用户查询和获取数据。"
            "当前无法获取数据集文件列表,请检查 HF_TOKEN 配置是否正确。"
        )

    # 格式化文件列表为提示词
    files_str = "\n".join([f"  - {item}" for item in file_list])
    system_prompt = (
        "你是一个渔业数据查询助手,拥有以下工具:\n\n"
        "1. **logbook_query** — 查询 logbook(捕捞日志)数据可用性,支持按海区、年份、物种、数据类型筛选\n"
        "2. **gfw_query** — 查询 GFW(Global Fishing Watch)渔船作业努力量,需要指定年份(2012-2024)、月份和空间范围\n"
        "3. **literature_cpue_query** — 查询 CPUE(单位捕捞努力量渔获量)相关文献数据,支持按海区、年份、物种、响应变量筛选\n"
        "4. **sprfmo_query** — 查询 SPRFMO(南太平洋区域渔业管理组织)数据,支持捕捞量或努力量查询,可按国家、年份、物种等筛选\n\n"
        "当前可查询的 Hugging Face 数据集仓库: " + HF_DATASET_REPO + "\n\n"
        "数据集一级文件和文件夹列表:\n" + files_str + "\n\n"
        "用户查询示例:\n"
        "  - '2010-2020年东南太平洋有哪些鱿鱼捕捞日志数据?' → 使用 logbook_query\n"
        "  - '查询2012年1月太平洋区域的渔船作业努力量' → 使用 gfw_query\n"
        "  - '有哪些关于鱿鱼CPUE的研究文献?' → 使用 literature_cpue_query\n"
        "  - '查询中国2015-2020年在南太平洋的渔获量' → 使用 sprfmo_query\n\n"
        "请根据用户需求选择合适的工具,不要混用。\n\n"
        "【重要】最终回答格式要求:\n"
        "在展示查询数据后,必须附加以下信息:\n"
        "---\n"
        "**查询执行详情:**\n"
        "- 使用工具:[本次调用的Tool名称]\n"
        "- 查询参数:\n"
        "  - 参数1名称: 参数1值\n"
        "  - 参数2名称: 参数2值\n"
        "  - ...(列出所有实际传入的参数)\n"
        "---\n"
        "请严格遵守此格式,确保用户清楚了解每次查询的具体执行过程。"
    )

    logger.info("系统提示词已构建,包含 %d 个文件/文件夹", len(file_list))
    return system_prompt


# ===========================================================================
# DeepSeek 思考模式自定义模型类(已移除)
# ===========================================================================
# DeepSeekThinkingModel 类已被移除,原因如下:
# 1. deepseek-chat 和 deepseek-reasoner 将于 2026/07/24 弃用
# 2. 新模型 deepseek-v4-flash/v4-pro 使用标准 OpenAI API,无需特殊适配
# 3. 思考模式不支持 tool_choice,限制了 Agent 的工具调用能力
# 4. 使用标准 OpenAIServerModel 更稳定,兼容性更好
# 现在使用标准 OpenAIServerModel,参见 create_hf_data_agent 函数


# ===========================================================================
# Tool 1: 参数提取 —— 从自然语言中解析出数据集查询参数
# ===========================================================================
class ParameterExtractionTool(Tool):
    """从用户的自然语言描述中提取 Hugging Face 数据集查询所需的结构化参数。"""

    name = "parameter_extractor"
    description = (
        "从用户的自然语言查询请求中提取数据集查询参数,包括:"
        "region(区域,如中西太平洋)、time_range(时间范围,如1967-2024)、"
        "time_scale(时间尺度,如月尺度)、spatial_resolution(空间分辨率,如1x1)、"
        "file_name(文件名)。返回 JSON 格式的参数字典。"
    )
    inputs = {
        "user_query": {
            "type": "string",
            "description": (
                "用户的自然语言查询请求,例如'查询中西太平洋1967-2024年月尺度1x1分辨率的渔获数据'"
            ),
        }
    }
    output_type = "string"

    # --- 区域关键字映射表 ---
    REGION_KEYWORDS = {
        "中西太平洋": "中西太平洋WCPFC",
        "wcpfc": "中西太平洋WCPFC",
        "东太平洋": "东太平洋",
        "太平洋": "中西太平洋WCPFC",
        "印度洋": "印度洋",
        "大西洋": "大西洋",
        "南海": "南海",
    }

    # --- 时间尺度映射表 ---
    TIME_SCALE_KEYWORDS = {
        "月尺度": "月尺度",
        "月度": "月尺度",
        "月": "月尺度",
        "年尺度": "年尺度",
        "年度": "年尺度",
        "年": "年尺度",
        "日尺度": "日尺度",
        "日度": "日尺度",
        "日": "日尺度",
    }

    # --- 空间分辨率映射表 ---
    SPATIAL_RESOLUTION_KEYWORDS = {
        "1x1": "1x1",
        "1度": "1x1",
        "0.5x0.5": "0.5x0.5",
        "0.5度": "0.5x0.5",
        "0.1x0.1": "0.1x0.1",
        "0.1度": "0.1x0.1",
    }

    def forward(self, user_query: str) -> str:
        """
        解析自然语言,提取数据集查询参数。

        参数提取流程:
        1. 识别目标区域(优先匹配关键字映射表)
        2. 识别时间范围(起始年份-结束年份)
        3. 识别时间尺度(月尺度/年尺度/日尺度)
        4. 识别空间分辨率(1x1/0.5x0.5等)
        5. 识别文件名(用户指定的具体文件名)
        """
        logger.info("开始提取参数,输入: %s", user_query)

        try:
            params = {
                "region": self._extract_region(user_query),
                "time_range": self._extract_time_range(user_query),
                "time_scale": self._extract_time_scale(user_query),
                "spatial_resolution": self._extract_spatial_resolution(user_query),
                "file_name": self._extract_file_name(user_query),
            }

            # 校验必要参数:区域不能为空
            if not params["region"]:
                error_msg = (
                    "无法从输入中识别目标区域,请在查询中明确指定区域。"
                    f"支持的区域关键字: {list(self.REGION_KEYWORDS.keys())}"
                )
                logger.warning(error_msg)
                return json.dumps({"error": error_msg}, ensure_ascii=False)

            logger.info("参数提取成功: %s", params)
            return json.dumps(params, ensure_ascii=False)

        except Exception as e:
            logger.error("参数提取失败: %s", e, exc_info=True)
            return json.dumps(
                {"error": f"参数提取过程发生错误: {str(e)}"},
                ensure_ascii=False,
            )

    # ----- 以下为私有辅助方法,按提取维度拆分 -----

    def _extract_region(self, query: str) -> Optional[str]:
        """
        从自然语言中识别目标区域。
        优先级:关键字映射 → 直接匹配区域名称
        """
        # 优先通过关键字映射识别
        for cn_keyword, region_name in self.REGION_KEYWORDS.items():
            if cn_keyword.lower() in query.lower():
                return region_name

        return None

    def _extract_time_range(self, query: str) -> Optional[str]:
        """
        从自然语言中识别时间范围。
        匹配"XXXX-XXXX"格式或"XXXX年到XXXX年"格式。
        """
        # 匹配"1967-2024"格式
        match = re.search(r"(\d{4})-(\d{4})", query)
        if match:
            return f"{match.group(1)}-{match.group(2)}"

        # 匹配"1967年到2024年"或"1967至2024"格式
        match = re.search(r"(\d{4})\s*(?:年到|至|-)\s*(\d{4})", query)
        if match:
            return f"{match.group(1)}-{match.group(2)}"

        # 匹配单一年份"2024年"
        match = re.search(r"(\d{4})\s*年", query)
        if match:
            year = match.group(1)
            return f"{year}-{year}"

        return None

    def _extract_time_scale(self, query: str) -> Optional[str]:
        """
        从自然语言中识别时间尺度。
        优先级:关键字映射 → 默认月尺度
        """
        for cn_keyword, scale_name in self.TIME_SCALE_KEYWORDS.items():
            if cn_keyword in query:
                return scale_name

        return "月尺度"  # 默认月尺度

    def _extract_spatial_resolution(self, query: str) -> Optional[str]:
        """
        从自然语言中识别空间分辨率。
        匹配"XxX"格式或"X度"格式。
        """
        # 优先通过关键字映射识别
        for cn_keyword, resolution in self.SPATIAL_RESOLUTION_KEYWORDS.items():
            if cn_keyword in query:
                return resolution

        # 匹配"1x1"格式
        match = re.search(r"(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)", query)
        if match:
            return f"{match.group(1)}x{match.group(2)}"

        return "1x1"  # 默认1x1分辨率

    def _extract_file_name(self, query: str) -> Optional[str]:
        """
        从自然语言中识别具体文件名。
        匹配"xxx.zip"或"xxx数据"模式。
        """
        # 匹配".zip"结尾的文件名
        match = re.search(r"(\S+)\.zip", query)
        if match:
            return f"{match.group(1)}.zip"

        # 匹配"xxx文件"或"xxx数据"模式
        match = re.search(r"(\w+)\s*(?:文件|数据)", query)
        if match:
            return f"{match.group(1)}.zip"

        return None


# ===========================================================================
# Tool 2: Hugging Face 数据集获取 —— 从云端获取并解析数据
# ===========================================================================
# ===========================================================================
# HFDataQueryTool 工具类(已移除)
# ===========================================================================
# HFDataQueryTool 已被移除,原因:
# - 该工具的功能已整合到其他工具中,不再需要单独的数据获取工具
# - 移除了约 350 行代码,包括文件解析、ZIP 处理、多种格式支持等
# - Agent 现在主要使用 logbook_query 和 gfw_query 两个专用查询工具


# ===========================================================================
# Tool 3: Logbook 数据可用性查询
# ===========================================================================
class LogbookQueryTool(Tool):
    """查询 logbook 数据可用性,支持按海区、年份、物种、数据类型筛选。"""

    name = "logbook_query"
    description = (
        "查询 logbook(捕捞日志)数据的可用性。"
        "支持按海区(region)、年份范围(year_start/year_end)、"
        "物种(species)、数据类型(data_type)筛选。"
        "返回统计摘要和预览数据。"
    )
    inputs = {
        "region": {
            "type": "string",
            "description": "海区名称(模糊匹配),如 '东南太平洋'、'中西太平洋'。不指定则不筛选。",
            "nullable": True,
        },
        "year_start": {
            "type": "integer",
            "description": "起始年份(包含),如 2010。不指定则不筛选。",
            "nullable": True,
        },
        "year_end": {
            "type": "integer",
            "description": "结束年份(包含),如 2020。不指定则不筛选。",
            "nullable": True,
        },
        "species": {
            "type": "string",
            "description": "物种名称(模糊匹配),如 '鱿鱼'。不指定则不筛选。",
            "nullable": True,
        },
        "data_type": {
            "type": "string",
            "description": "数据类型(模糊匹配),如 '捕捞日志'。不指定则不筛选。",
            "nullable": True,
        },
    }
    output_type = "string"

    def forward(
        self,
        region: Optional[str] = None,
        year_start: Optional[int] = None,
        year_end: Optional[int] = None,
        species: Optional[str] = None,
        data_type: Optional[str] = None,
    ) -> str:
        """
        调用 query_logbook_availability 查询 logbook 数据可用性。

        处理流程:
        1. 将参数传递给 query_logbook_availability 函数
        2. 捕获异常并返回错误信息
        3. 将结果转为 JSON 字符串返回
        """
        logger.info(
            "Logbook 查询: region=%s, year_start=%s, year_end=%s, species=%s, data_type=%s",
            region, year_start, year_end, species, data_type,
        )

        try:
            result = query_logbook_availability(
                region=region if region else None,
                year_start=year_start,
                year_end=year_end,
                species=species if species else None,
                data_type=data_type if data_type else None,
                output_format="markdown",  # 默认使用 markdown 输出,不写文件
            )

            # 格式化 summary 为可读字符串(兼容新旧版本)
            summary_data = result.get("summary", {})
            if isinstance(summary_data, dict):
                # 新版本:summary 是字典,格式化为友好字符串
                summary_str = (
                    f"找到 {summary_data.get('records_count', 0)} 条 logbook 数据记录\n"
                    f"覆盖年份: {summary_data.get('year_range', ['未知', '未知'])[0]}-{summary_data.get('year_range', ['未知', '未知'])[1]}\n"
                    f"海区: {', '.join(summary_data.get('regions', ['未知']))}\n"
                    f"物种: {', '.join(summary_data.get('species', ['未知']))}"
                )
            else:
                # 旧版本:summary 已经是字符串
                summary_str = summary_data

            # 确保返回结构完整
            complete_result = {
                "summary": summary_str,
                "records": result.get("records", []),
                "preview_markdown": result.get("preview_markdown", ""),
                "source_files": result.get("source_files", []),
            }

            return json.dumps(complete_result, ensure_ascii=False, default=str)

        except Exception as e:
            logger.error("Logbook 查询失败: %s", e, exc_info=True)
            return json.dumps(
                {"error": f"Logbook 查询失败: {str(e)}"},
                ensure_ascii=False,
            )


# ===========================================================================
# Tool 4: GFW 渔船作业努力量查询
# ===========================================================================
class GfwQueryTool(Tool):
    """查询 GFW(Global Fishing Watch)渔船作业努力量数据。"""

    name = "gfw_query"
    description = (
        "查询 GFW 渔船作业努力量数据。"
        "需要指定年份(仅支持 2012-2024)、月份和空间范围(经纬度)。"
        "可选按船旗国(flag)和渔具类型(geartype)筛选。"
        "注意:lon_min > lon_max 表示查询范围跨越 180° 经线。"
    )
    inputs = {
        "year": {
            "type": "integer",
            "description": "查询年份(仅支持 2012-2024),如 2012。",
        },
        "month": {
            "type": "integer",
            "description": "查询月份(1-12)。",
        },
        "lat_min": {
            "type": "number",
            "description": "最小纬度,如 -10。",
        },
        "lat_max": {
            "type": "number",
            "description": "最大纬度,如 10。",
        },
        "lon_min": {
            "type": "number",
            "description": "最小经度,如 145。大于 lon_max 时表示跨越 180° 经线。",
        },
        "lon_max": {
            "type": "number",
            "description": "最大经度,如 -175。",
        },
        "flag": {
            "type": "string",
            "description": "船旗国代码(精确匹配,不区分大小写),如 'CN'。不指定则不筛选。",
            "nullable": True,
        },
        "geartype": {
            "type": "string",
            "description": "渔具类型(精确匹配,不区分大小写),如 'drifting_longlines'。不指定则不筛选。",
            "nullable": True,
        },
        "min_fishing_hours": {
            "type": "number",
            "description": "最小捕捞小时数阈值,如 10。不指定则不筛选。",
            "nullable": True,
        },
    }
    output_type = "string"

    def forward(
        self,
        year: int,
        month: int,
        lat_min: float,
        lat_max: float,
        lon_min: float,
        lon_max: float,
        flag: Optional[str] = None,
        geartype: Optional[str] = None,
        min_fishing_hours: Optional[float] = None,
    ) -> str:
        """
        调用 query_gfw 查询 GFW 渔船作业努力量数据。

        处理流程:
        1. 将参数传递给 query_gfw 函数
        2. 捕获异常并返回错误信息
        3. 将 DataFrame 结果和 summary 转为 JSON 字符串返回
        """
        logger.info(
            "GFW 查询: year=%s, month=%s, lat=[%s,%s], lon=[%s,%s], flag=%s, geartype=%s, min_fishing_hours=%s",
            year, month, lat_min, lat_max, lon_min, lon_max, flag, geartype, min_fishing_hours,
        )

        try:
            result_df, summary = query_gfw(
                year=year,
                month=month,
                lat_min=lat_min,
                lat_max=lat_max,
                lon_min=lon_min,
                lon_max=lon_max,
                flag=flag if flag else None,
                geartype=geartype if geartype else None,
                min_fishing_hours=min_fishing_hours,
            )

            # 将 DataFrame 转为记录列表(限制预览条数)
            display_limit = 100
            records = result_df.head(display_limit).to_dict(orient="records")

            output = {
                "summary": summary,
                "record_count": len(result_df),
                "display_records": min(len(result_df), display_limit),
                "data_preview": records,
                "columns": list(result_df.columns) if not result_df.empty else [],
            }

            return json.dumps(output, ensure_ascii=False, default=str)

        except FileNotFoundError as e:
            # GFW 数据文件未找到
            logger.error("GFW 数据文件未找到: %s", e)
            return json.dumps(
                {"error": f"GFW 数据文件未找到: {str(e)}"},
                ensure_ascii=False,
            )
        except ValueError as e:
            # 参数校验失败(如月份越界、纬度范围错误)
            logger.error("GFW 查询参数错误: %s", e)
            return json.dumps(
                {"error": f"查询参数错误: {str(e)}"},
                ensure_ascii=False,
            )
        except Exception as e:
            logger.error("GFW 查询失败: %s", e, exc_info=True)
            return json.dumps(
                {"error": f"GFW 查询失败: {str(e)}"},
                ensure_ascii=False,
            )


# ===========================================================================
# Tool 5: 文献 CPUE 查询
# ===========================================================================
class LiteratureCpueQueryTool(Tool):
    """查询 CPUE(单位捕捞努力量渔获量)相关文献数据。"""

    name = "literature_cpue_query"
    description = (
        "查询 CPUE(单位捕捞努力量渔获量)相关文献数据。"
        "支持按海区、年份范围、物种、响应变量、论文类型筛选。"
        "返回文献统计摘要和预览数据。"
    )
    inputs = {
        "region": {
            "type": "string",
            "description": "海区名称(模糊匹配),如 '东南太平洋'、'中西太平洋'。不指定则不筛选。",
            "nullable": True,
        },
        "year_start": {
            "type": "integer",
            "description": "起始年份(包含),如 2010。不指定则不筛选。",
            "nullable": True,
        },
        "year_end": {
            "type": "integer",
            "description": "结束年份(包含),如 2020。不指定则不筛选。",
            "nullable": True,
        },
        "species": {
            "type": "string",
            "description": "物种名称(模糊匹配),如 '鱿鱼'。不指定则不筛选。",
            "nullable": True,
        },
        "response_variable": {
            "type": "string",
            "description": "响应变量(模糊匹配),如 'CPUE'、'biomass'。不指定则不筛选。",
            "nullable": True,
        },
        "paper_type": {
            "type": "string",
            "description": "论文类型(模糊匹配)。不指定则不筛选。",
            "nullable": True,
        },
    }
    output_type = "string"

    def forward(
        self,
        region: Optional[str] = None,
        year_start: Optional[int] = None,
        year_end: Optional[int] = None,
        species: Optional[str] = None,
        response_variable: Optional[str] = None,
        paper_type: Optional[str] = None,
    ) -> str:
        """
        调用 query_literature_cpue 查询 CPUE 文献数据。

        处理流程:
        1. 将参数传递给 query_literature_cpue 函数
        2. 捕获异常并返回错误信息
        3. 将结果转为 JSON 字符串返回
        """
        logger.info(
            "CPUE 文献查询: region=%s, year_start=%s, year_end=%s, species=%s, response_variable=%s, paper_type=%s",
            region, year_start, year_end, species, response_variable, paper_type,
        )

        try:
            # 动态导入以避免循环依赖
            from query_tools.literature_cpue_query import query_literature_cpue

            result = query_literature_cpue(
                region=region if region else None,
                year_start=year_start,
                year_end=year_end,
                species=species if species else None,
                response_variable=response_variable if response_variable else None,
                paper_type=paper_type if paper_type else None,
                output_format="markdown",  # 默认使用 markdown 输出,不写文件
            )

            # 格式化 summary 为可读字符串
            summary_data = result.get("summary", {})
            if isinstance(summary_data, dict):
                summary_str = (
                    f"找到 {summary_data.get('paper_count', 0)} 篇 CPUE 相关文献\n"
                    f"覆盖年份: {summary_data.get('year_range', ['未知', '未知'])[0]}-{summary_data.get('year_range', ['未知', '未知'])[1]}\n"
                    f"涉及海区: {', '.join(summary_data.get('regions', ['未知']))}\n"
                    f"涉及物种: {', '.join(summary_data.get('species', ['未知']))}\n"
                    f"使用模型: {', '.join(summary_data.get('models', ['未知']))}"
                )
            else:
                summary_str = summary_data

            # 确保返回结构完整
            complete_result = {
                "summary": summary_str,
                "records": result.get("records", []),
                "preview_markdown": result.get("preview_markdown", ""),
                "source_files": result.get("source_files", []),
            }

            return json.dumps(complete_result, ensure_ascii=False, default=str)

        except Exception as e:
            logger.error("CPUE 文献查询失败: %s", e, exc_info=True)
            return json.dumps(
                {"error": f"CPUE 文献查询失败: {str(e)}"},
                ensure_ascii=False,
            )


# ===========================================================================
# Tool 6: SPRFMO 南太平洋数据查询
# ===========================================================================
class SprfmoQueryTool(Tool):
    """查询 SPRFMO(南太平洋区域渔业管理组织)数据。"""

    name = "sprfmo_query"
    description = (
        "查询 SPRFMO(南太平洋区域渔业管理组织)数据。"
        "支持查询捕捞量(catch)或努力量(effort)数据。"
        "可按国家、年份范围、物种等筛选,支持按维度聚合统计。"
        "空间分辨率:5x5 度;时间分辨率:年度。"
    )
    inputs = {
        "data_type": {
            "type": "string",
            "description": "数据类型,'catch'(捕捞量)或 'effort'(努力量)。必填。",
        },
        "country": {
            "type": "string",
            "description": "国家代码或名称(如 'CHN'、'中国'、'JPN')。不指定则不筛选。",
            "nullable": True,
        },
        "year_start": {
            "type": "integer",
            "description": "起始年份(包含)。不指定则不筛选。",
            "nullable": True,
        },
        "year_end": {
            "type": "integer",
            "description": "结束年份(包含)。不指定则不筛选。",
            "nullable": True,
        },
        "species": {
            "type": "string",
            "description": "物种名称(仅捕捞量数据)。不指定则不筛选。",
            "nullable": True,
        },
        "gear_type": {
            "type": "string",
            "description": "渔具类型(仅努力量数据)。不指定则不筛选。",
            "nullable": True,
        },
        "group_by": {
            "type": "string",
            "description": "聚合维度,如 'year'、'country'、'species'。多个维度用逗号分隔。不指定则不聚合。",
            "nullable": True,
        },
    }
    output_type = "string"

    def forward(
        self,
        data_type: str,
        country: Optional[str] = None,
        year_start: Optional[int] = None,
        year_end: Optional[int] = None,
        species: Optional[str] = None,
        gear_type: Optional[str] = None,
        group_by: Optional[str] = None,
    ) -> str:
        """
        调用 query_sprfmo 查询 SPRFMO 数据。

        处理流程:
        1. 构建 filters 字典
        2. 解析 group_by 参数
        3. 调用 query_sprfmo 函数
        4. 捕获异常并返回错误信息
        5. 将结果转为 JSON 字符串返回
        """
        logger.info(
            "SPRFMO 查询: data_type=%s, country=%s, year=[%s,%s], species=%s, gear_type=%s, group_by=%s",
            data_type, country, year_start, year_end, species, gear_type, group_by,
        )

        try:
            # 动态导入以避免循环依赖
            from query_tools.query_sprfmo import query_sprfmo

            # 构建 filters 字典
            filters = {}
            if country:
                filters["country"] = country
            if year_start is not None:
                filters["year_start"] = year_start
            if year_end is not None:
                filters["year_end"] = year_end
            if species and data_type == "catch":
                filters["species"] = species
            if gear_type and data_type == "effort":
                filters["gear_type"] = gear_type

            # 解析 group_by 参数
            group_by_list = None
            if group_by:
                group_by_list = [g.strip() for g in group_by.split(",")]

            # 调用查询函数
            result = query_sprfmo(
                filters=filters,
                group_by=group_by_list,
                metrics=data_type,  # metrics 参数直接使用 data_type
                data_type=data_type,
                output_format="markdown",
            )

            # 格式化 summary
            summary_data = result.get("summary", {})
            summary_str = (
                f"数据类型: {summary_data.get('data_type', '未知')}\n"
                f"总记录数: {summary_data.get('total_records', 0)}\n"
                f"数据来源: {summary_data.get('data_source', 'SPRFMO')}\n"
            )
            if "year_range" in summary_data:
                summary_str += f"年份范围: {summary_data['year_range']}\n"
            if "total_catch" in summary_data:
                summary_str += f"总捕捞量: {summary_data['total_catch']:.2f} kg\n"
            if "total_effort" in summary_data:
                summary_str += f"总努力量: {summary_data['total_effort']:.2f} 天\n"

            # 确保返回结构完整
            complete_result = {
                "summary": summary_str,
                "records": result.get("records", []),
                "preview_markdown": result.get("preview_markdown", ""),
                "source_files": result.get("source_files", []),
                "warnings": result.get("warnings", []),
                "metadata": result.get("metadata", {}),
            }

            return json.dumps(complete_result, ensure_ascii=False, default=str)

        except FileNotFoundError as e:
            # SPRFMO 数据文件未找到
            logger.error("SPRFMO 数据文件未找到: %s", e)
            return json.dumps(
                {"error": f"SPRFMO 数据文件未找到: {str(e)}"},
                ensure_ascii=False,
            )
        except Exception as e:
            logger.error("SPRFMO 查询失败: %s", e, exc_info=True)
            return json.dumps(
                {"error": f"SPRFMO 查询失败: {str(e)}"},
                ensure_ascii=False,
            )


# ===========================================================================
# Agent 工厂函数:创建配置好的 ToolCallingAgent
# ===========================================================================
def create_hf_data_agent(
    model_id: str = MODEL_ID,
    api_key: Optional[str] = None,
    api_base: Optional[str] = None,
    max_steps: int = 5,
) -> ToolCallingAgent:
    """
    创建并返回一个配置好的 Hugging Face 数据集查询智能代理。

    在 Agent 启动时会自动获取数据集文件列表并作为系统提示词告知用户。

    注意:DeepSeek 思考模式已停用,现在使用标准 OpenAIServerModel。
    原因:deepseek-chat/deepseek-reasoner 将于 2026/07/24 弃用,
          新模型 deepseek-v4-flash/v4-pro 使用标准 OpenAI API。

    Args:
        model_id: 模型 ID,默认使用文件顶部 MODEL_ID 常量
                  推荐: deepseek-v4-flash 或 deepseek-v4-pro
        api_key: API Key,默认使用文件顶部 OPENAI_API_KEY 常量
        api_base: API 地址,默认使用文件顶部 OPENAI_API_BASE 常量
        max_steps: Agent 最大推理步骤数

    Returns:
        配置好的 ToolCallingAgent 实例
    """
    key = api_key or OPENAI_API_KEY
    base = api_base or OPENAI_API_BASE

    # 初始化 LLM 模型(使用标准 OpenAIServerModel,已停用思考模式)
    model = OpenAIServerModel(
        model_id=model_id,
        api_key=key,
        api_base=base,
    )

    # 实例化工具(包含新增的 CPUE 文献和 SPRFMO 查询工具)
    param_tool = ParameterExtractionTool()
    logbook_tool = LogbookQueryTool()
    gfw_tool = GfwQueryTool()
    literature_cpue_tool = LiteratureCpueQueryTool()
    sprfmo_tool = SprfmoQueryTool()

    # 构建包含数据集文件列表的指令(会被插入到系统提示词中)
    instructions = build_system_prompt_with_file_list()
    logger.info("Agent 自定义指令: %s", instructions[:200] + "...")  # 只打印前200字符

    # 创建 ToolCallingAgent,使用 instructions 参数设置自定义指令
    # 工具列表包含5个查询工具(已移除 hf_data_tool)
    agent = ToolCallingAgent(
        tools=[param_tool, logbook_tool, gfw_tool, literature_cpue_tool, sprfmo_tool],
        model=model,
        max_steps=max_steps,
        instructions=instructions,
    )

    logger.info("Hugging Face 数据集查询代理创建成功,模型: %s", model_id)
    return agent


# ===========================================================================
# 主入口:GradioUI 交互式 Web 界面
# ===========================================================================
def main():
    """
    使用 GradioUI 启动交互式 Web 界面。
    用户在浏览器中输入自然语言查询,代理实时展示思考过程和数据获取结果。
    界面基于 gr.ChatInterface,支持流式输出和步骤可视化。
    """
    agent = create_hf_data_agent()

    # GradioUI 将 agent 包装为 gr.ChatInterface Web 应用
    # - share=True: 生成公网可访问的临时链接(72小时有效)
    # - reset_agent_memory=False: 保留对话上下文,允许多轮交互
    demo = GradioUI(
        agent,
        reset_agent_memory=False,
    )

    logger.info("正在启动 GradioUI Web 界面...")
    demo.launch(share=True)


if __name__ == "__main__":
    main()