Spaces:
Sleeping
Sleeping
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() |