panda-data-tools 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.4
2
+ Name: panda-data-tools
3
+ Version: 1.0.0
4
+ Summary: PandaAI 金融数据 API 的 LLM Tool 封装层,将 38 个数据查询方法封装为符合 function calling 规范的 tools。
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: duckdb
7
+ Requires-Dist: panda-data
8
+ Requires-Dist: pandas
9
+ Requires-Dist: python-dotenv>=1.0.0
10
+ Provides-Extra: test
11
+ Requires-Dist: duckdb; extra == 'test'
12
+ Requires-Dist: hypothesis; extra == 'test'
13
+ Requires-Dist: openpyxl; extra == 'test'
14
+ Requires-Dist: pytest; extra == 'test'
@@ -0,0 +1,16 @@
1
+ panda_tools/__init__.py,sha256=l8w31xU7Vl8ueiJgwkYjCbIILE3AzR4zEDXYJkGayL4,133
2
+ panda_tools/cache.py,sha256=_ZqqUVAa0Oy-sKMiIZ7RIpigQ9lWhxMZjr4KesITWtg,5693
3
+ panda_tools/credential.py,sha256=-eX0BIGsCvJ08DpIthxLRUCr7JszSWtstzYPeaQQPZw,4578
4
+ panda_tools/exporter.py,sha256=DoWZM6f57py57PN_lDCqWe281kiABEy32MAn_0C4OLE,2482
5
+ panda_tools/formatter.py,sha256=I8tYlSW6XwZBZqGO5MJT8o6_ZYADIAeZ_3rsZKyefqE,2334
6
+ panda_tools/registry.py,sha256=m4YzPVB88rWMtZaSFbCnBveynqQ5ljE73hNmzy2qYD8,5460
7
+ panda_tools/sdk.py,sha256=vh5fob4eHkhrqfYjzYlYX7NFA0KtnnGo8-UeZvCXyPI,899
8
+ panda_tools/tools/__init__.py,sha256=ErJIG706K2fa-Ztj22Gy1TvvDkGp2AFRIVIHis8jISc,569
9
+ panda_tools/tools/financial.py,sha256=pE1KP_K-snJVJpXbnEKYlvS3DSvBjemZKTTPfBI6t4g,13962
10
+ panda_tools/tools/futures.py,sha256=J2TjxUl6RmPwPdQWarTq_GiknGnwlHF8EkinM6xxQPc,7006
11
+ panda_tools/tools/market_data.py,sha256=5ABbCvalcR5vIxz6p0YtGkei2-qYlG0n0RUW5qTjh6s,9188
12
+ panda_tools/tools/market_ref.py,sha256=aK-cU48OKg9ngOFO3yRjScvab4FkQ44ikLanYo4t3f4,42112
13
+ panda_tools/tools/trade_tools.py,sha256=SYYpU4whkRm4NKSM8ItXDO-wRePBJRWD_AzHYXxYShI,9234
14
+ panda_data_tools-1.0.0.dist-info/METADATA,sha256=X4bOrOh_NLUPS-zByv_O6Pwso1P31RVPmJaqebhSy6w,512
15
+ panda_data_tools-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
16
+ panda_data_tools-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ """PandaAI 数据服务 Tool 层 — 将 panda_data 的 38 个 API 封装为 LLM function calling tools。"""
2
+
3
+ __version__ = "2.0.0"
panda_tools/cache.py ADDED
@@ -0,0 +1,163 @@
1
+ """DuckDB 本地数据缓存管理器模块。
2
+
3
+ 提供 CacheManager 类,基于 DuckDB 实现查询结果的本地持久化存储和读取。
4
+ DuckDB 未安装时优雅降级,所有方法返回空结果,不抛异常。
5
+ """
6
+
7
+ import logging
8
+ from typing import Any, List, Optional
9
+
10
+ import pandas as pd
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class CacheManager:
16
+ """DuckDB 本地数据缓存管理器。
17
+
18
+ 使用 DuckDB 作为本地存储引擎,提供查询结果的持久化缓存能力。
19
+ DuckDB 未安装时自动降级,所有方法返回空结果或提示信息。
20
+ """
21
+
22
+ def __init__(self, db_path: str = "./panda_data_cache.duckdb") -> None:
23
+ """初始化缓存管理器。
24
+
25
+ Args:
26
+ db_path: DuckDB 数据库文件路径,默认 ./panda_data_cache.duckdb。
27
+ """
28
+ self._available = False
29
+ self._conn = None
30
+ self._db_path = db_path
31
+
32
+ try:
33
+ import duckdb
34
+
35
+ self._conn = duckdb.connect(db_path)
36
+ self._available = True
37
+ except ImportError:
38
+ logger.warning(
39
+ "DuckDB 未安装,缓存功能不可用。请运行 pip install duckdb 安装。"
40
+ )
41
+ except Exception as e:
42
+ logger.warning(f"DuckDB 连接失败:{type(e).__name__}: {e}")
43
+
44
+ def save(self, table_name: str, df: pd.DataFrame) -> str:
45
+ """将 DataFrame 存入 DuckDB 表(追加模式)。
46
+
47
+ Args:
48
+ table_name: 表名,通常为 API 方法名(如 "get_market_data")。
49
+ df: 要缓存的 DataFrame。
50
+
51
+ Returns:
52
+ 成功信息,包含表名和行数。
53
+ """
54
+ if not self._available:
55
+ return "缓存不可用:DuckDB 未安装"
56
+
57
+ try:
58
+ # 使用唯一别名注册 DataFrame,避免与表名冲突(如表名恰好为 "df")
59
+ _alias = "__panda_cache_tmp_df__"
60
+ self._conn.register(_alias, df)
61
+ self._conn.execute(
62
+ f"CREATE TABLE IF NOT EXISTS \"{table_name}\" AS SELECT * FROM \"{_alias}\" WHERE 1=0"
63
+ )
64
+ self._conn.execute(f"INSERT INTO \"{table_name}\" SELECT * FROM \"{_alias}\"")
65
+ self._conn.unregister(_alias)
66
+ return f"已缓存 {len(df)} 行数据到表 {table_name}"
67
+ except Exception as e:
68
+ return f"缓存写入失败:{type(e).__name__}: {e}"
69
+
70
+ def read(self, table_name: str, **filters: Any) -> pd.DataFrame:
71
+ """从 DuckDB 读取缓存数据,支持可选过滤条件。
72
+
73
+ Args:
74
+ table_name: 表名。
75
+ **filters: 过滤条件,键为列名,值为筛选值。
76
+
77
+ Returns:
78
+ 查询结果 DataFrame,表不存在时返回空 DataFrame。
79
+ """
80
+ if not self._available:
81
+ return pd.DataFrame()
82
+
83
+ try:
84
+ # 检查表是否存在
85
+ tables = self._conn.execute("SHOW TABLES").fetchall()
86
+ table_names = [t[0] for t in tables]
87
+ if table_name not in table_names:
88
+ return pd.DataFrame()
89
+
90
+ # 获取表的实际列名,用于过滤无效列
91
+ columns_info = self._conn.execute(
92
+ f"SELECT column_name FROM information_schema.columns WHERE table_name = ?",
93
+ [table_name],
94
+ ).fetchall()
95
+ valid_columns = {row[0] for row in columns_info}
96
+
97
+ # 构建 WHERE 子句,忽略不存在的列
98
+ where_clauses = []
99
+ params = []
100
+ for col, val in filters.items():
101
+ if col in valid_columns:
102
+ where_clauses.append(f"\"{col}\" = ?")
103
+ params.append(val)
104
+
105
+ query = f"SELECT * FROM \"{table_name}\""
106
+ if where_clauses:
107
+ query += " WHERE " + " AND ".join(where_clauses)
108
+
109
+ result = self._conn.execute(query, params).fetchdf()
110
+ return result
111
+ except Exception as e:
112
+ logger.warning(f"缓存读取失败:{type(e).__name__}: {e}")
113
+ return pd.DataFrame()
114
+
115
+ def clear(self, table_name: Optional[str] = None) -> str:
116
+ """清除缓存数据。
117
+
118
+ Args:
119
+ table_name: 指定表名则只清除该表;None 则清除所有表。
120
+
121
+ Returns:
122
+ 成功信息。
123
+ """
124
+ if not self._available:
125
+ return "缓存不可用:DuckDB 未安装"
126
+
127
+ try:
128
+ if table_name is not None:
129
+ self._conn.execute(f"DROP TABLE IF EXISTS \"{table_name}\"")
130
+ return f"已清除缓存表 {table_name}"
131
+ else:
132
+ tables = self._conn.execute("SHOW TABLES").fetchall()
133
+ for t in tables:
134
+ self._conn.execute(f"DROP TABLE IF EXISTS \"{t[0]}\"")
135
+ return "已清除所有缓存表"
136
+ except Exception as e:
137
+ return f"缓存清除失败:{type(e).__name__}: {e}"
138
+
139
+ def list_tables(self) -> List[str]:
140
+ """列出所有缓存表名。
141
+
142
+ Returns:
143
+ 表名列表。
144
+ """
145
+ if not self._available:
146
+ return []
147
+
148
+ try:
149
+ tables = self._conn.execute("SHOW TABLES").fetchall()
150
+ return [t[0] for t in tables]
151
+ except Exception as e:
152
+ logger.warning(f"缓存表列表获取失败:{type(e).__name__}: {e}")
153
+ return []
154
+
155
+ def close(self) -> None:
156
+ """关闭 DuckDB 连接。"""
157
+ if self._conn is not None:
158
+ try:
159
+ self._conn.close()
160
+ except Exception:
161
+ pass
162
+ self._conn = None
163
+ self._available = False
@@ -0,0 +1,135 @@
1
+ """PandaAI 凭证管理器模块。
2
+
3
+ 提供 CredentialManager 类,支持通过环境变量或直接传入用户名密码两种方式
4
+ 完成 PandaAI 认证初始化。使用模块级标志位避免重复初始化。
5
+ """
6
+
7
+ import os
8
+ from typing import Optional
9
+
10
+ import panda_data
11
+
12
+
13
+ def _load_dotenv_if_available() -> None:
14
+ """从当前工作目录向上查找 `.env` 并加载到 `os.environ`。
15
+
16
+ 默认不覆盖已存在的环境变量,与 shell 中 `export` 的优先级一致。
17
+ 解决「在项目根目录用 pip install -e . 时能读到凭证,打成 whl 后从别处运行读不到」
18
+ 的常见情况:开发时 IDE / 工具注入了 `.env`,而仅安装 whl 时进程里没有这些变量。
19
+ """
20
+ try:
21
+ from dotenv import find_dotenv, load_dotenv
22
+ except ImportError:
23
+ return
24
+ path = find_dotenv(usecwd=True)
25
+ if path:
26
+ load_dotenv(path, override=False)
27
+
28
+
29
+ # 模块级标志位,避免重复初始化
30
+ _initialized: bool = False
31
+ # 最近一次 init_from_env / init_with_credentials 的说明(成功或失败),供 safe_call 展示
32
+ _last_init_message: str = ""
33
+
34
+
35
+ class CredentialManager:
36
+ """PandaAI 凭证管理器,支持环境变量和直接传入两种方式。"""
37
+
38
+ @staticmethod
39
+ def init_from_env() -> str:
40
+ """从环境变量 PANDA_DATA_USERNAME 和 PANDA_DATA_PASSWORD 读取凭证并初始化。
41
+
42
+ Returns:
43
+ 成功信息或错误描述。
44
+ """
45
+ global _initialized, _last_init_message
46
+
47
+ if _initialized:
48
+ msg = "PandaAI 认证已完成,无需重复初始化。"
49
+ _last_init_message = msg
50
+ return msg
51
+
52
+ _load_dotenv_if_available()
53
+
54
+ username: Optional[str] = os.environ.get("PANDA_DATA_USERNAME")
55
+ password: Optional[str] = os.environ.get("PANDA_DATA_PASSWORD")
56
+
57
+ if not username or not username.strip():
58
+ msg = "认证失败:环境变量 PANDA_DATA_USERNAME 未设置或为空,请配置有效的用户名。"
59
+ _last_init_message = msg
60
+ return msg
61
+
62
+ if not password or not password.strip():
63
+ msg = "认证失败:环境变量 PANDA_DATA_PASSWORD 未设置或为空,请配置有效的密码。"
64
+ _last_init_message = msg
65
+ return msg
66
+
67
+ try:
68
+ panda_data.init_token(username=username, password=password)
69
+ _initialized = True
70
+ msg = "PandaAI 认证初始化成功。"
71
+ _last_init_message = msg
72
+ return msg
73
+ except Exception as e:
74
+ msg = f"认证失败:{type(e).__name__}: {e}"
75
+ _last_init_message = msg
76
+ return msg
77
+
78
+ @staticmethod
79
+ def init_with_credentials(username: str, password: str) -> str:
80
+ """直接传入用户名密码初始化。
81
+
82
+ Args:
83
+ username: PandaAI 用户名。
84
+ password: PandaAI 密码。
85
+
86
+ Returns:
87
+ 成功信息或错误描述。
88
+ """
89
+ global _initialized, _last_init_message
90
+
91
+ if _initialized:
92
+ msg = "PandaAI 认证已完成,无需重复初始化。"
93
+ _last_init_message = msg
94
+ return msg
95
+
96
+ if username is None or not str(username).strip():
97
+ msg = "认证失败:用户名为空,请提供有效的用户名。"
98
+ _last_init_message = msg
99
+ return msg
100
+
101
+ if password is None or not str(password).strip():
102
+ msg = "认证失败:密码为空,请提供有效的密码。"
103
+ _last_init_message = msg
104
+ return msg
105
+
106
+ try:
107
+ panda_data.init_token(username=username, password=password)
108
+ _initialized = True
109
+ msg = "PandaAI 认证初始化成功。"
110
+ _last_init_message = msg
111
+ return msg
112
+ except Exception as e:
113
+ msg = f"认证失败:{type(e).__name__}: {e}"
114
+ _last_init_message = msg
115
+ return msg
116
+
117
+ @staticmethod
118
+ def ensure_initialized() -> bool:
119
+ """检查是否已完成认证初始化。如果未初始化,尝试从环境变量初始化。
120
+
121
+ Returns:
122
+ True 表示已初始化,False 表示初始化失败。
123
+ """
124
+ global _initialized
125
+
126
+ if _initialized:
127
+ return True
128
+
129
+ CredentialManager.init_from_env()
130
+ return _initialized
131
+
132
+ @staticmethod
133
+ def last_init_message() -> str:
134
+ """最近一次认证尝试的说明(成功或失败),供调用方展示。"""
135
+ return _last_init_message
@@ -0,0 +1,77 @@
1
+ """PandaAI 数据导出模块。
2
+
3
+ 提供 export_data 函数,支持将 DataFrame 导出为 CSV、Excel、DuckDB 三种格式。
4
+ 导出失败时返回结构化错误消息,不抛异常。
5
+ """
6
+
7
+ import os
8
+
9
+ import pandas as pd
10
+
11
+
12
+ def export_data(
13
+ df: pd.DataFrame,
14
+ path: str,
15
+ format: str = "csv",
16
+ table_name: str = "exported_data",
17
+ ) -> str:
18
+ """将 DataFrame 导出为指定格式的文件。
19
+
20
+ Args:
21
+ df: 要导出的 DataFrame。
22
+ path: 输出文件路径(CSV/Excel)或 DuckDB 数据库路径。
23
+ format: 导出格式,可选 "csv"、"excel"、"duckdb"。
24
+ table_name: DuckDB 导出时的表名,默认 "exported_data"。
25
+
26
+ Returns:
27
+ 成功信息,包含文件路径和导出行数。
28
+ 失败时返回 "导出失败:{ErrorType}: {message}"。
29
+ """
30
+ try:
31
+ # 自动创建输出目录
32
+ dir_name = os.path.dirname(path)
33
+ if dir_name:
34
+ os.makedirs(dir_name, exist_ok=True)
35
+
36
+ if format == "csv":
37
+ return _export_csv(df, path)
38
+ elif format == "excel":
39
+ return _export_excel(df, path)
40
+ elif format == "duckdb":
41
+ return _export_duckdb(df, path, table_name)
42
+ else:
43
+ return f"导出失败:ValueError: 不支持的格式 '{format}',可选:csv, excel, duckdb"
44
+ except Exception as e:
45
+ return f"导出失败:{type(e).__name__}: {e}"
46
+
47
+
48
+ def _export_csv(df: pd.DataFrame, path: str) -> str:
49
+ """导出为 CSV 文件(UTF-8 with BOM 编码)。"""
50
+ df.to_csv(path, index=False, encoding="utf-8-sig")
51
+ return f"已导出 {len(df)} 行数据到 {path}"
52
+
53
+
54
+ def _export_excel(df: pd.DataFrame, path: str) -> str:
55
+ """导出为 Excel 文件(openpyxl 引擎)。"""
56
+ try:
57
+ import openpyxl # noqa: F401
58
+ except ImportError:
59
+ return "导出失败:ImportError: Excel 导出需要安装 openpyxl"
60
+
61
+ df.to_excel(path, index=False, engine="openpyxl")
62
+ return f"已导出 {len(df)} 行数据到 {path}"
63
+
64
+
65
+ def _export_duckdb(df: pd.DataFrame, path: str, table_name: str) -> str:
66
+ """导出为 DuckDB 表。"""
67
+ try:
68
+ import duckdb
69
+ except ImportError:
70
+ return "导出失败:ImportError: DuckDB 导出需要安装 duckdb"
71
+
72
+ conn = duckdb.connect(path)
73
+ try:
74
+ conn.execute(f'CREATE OR REPLACE TABLE "{table_name}" AS SELECT * FROM df')
75
+ return f"已导出 {len(df)} 行数据到 {path}"
76
+ finally:
77
+ conn.close()
@@ -0,0 +1,70 @@
1
+ """PandaAI 返回值格式化器模块。
2
+
3
+ 提供 format_result 和 safe_call 两个核心函数:
4
+ - format_result:将 API 返回结果(DataFrame / str / None)格式化为 LLM 可读文本
5
+ - safe_call:统一的 API 调用包装器,自动认证、异常捕获、结果格式化
6
+ """
7
+
8
+ from typing import Any, Callable
9
+
10
+ import pandas as pd
11
+
12
+ from panda_tools.credential import CredentialManager
13
+
14
+
15
+ def format_result(result: Any, max_rows: int = 20) -> str:
16
+ """将 API 返回结果格式化为 LLM 可读的文本。
17
+
18
+ 格式化规则:
19
+ - DataFrame:转为表格文本(to_string),超过 max_rows 行截断并提示总行数
20
+ - str:直接返回
21
+ - None / 空 DataFrame:返回"未查询到数据"
22
+
23
+ Args:
24
+ result: API 返回的原始结果,可能是 DataFrame、str 或 None。
25
+ max_rows: 最大显示行数,默认 20。
26
+
27
+ Returns:
28
+ 格式化后的字符串。
29
+ """
30
+ if result is None:
31
+ return "未查询到数据"
32
+
33
+ if isinstance(result, pd.DataFrame):
34
+ if result.empty:
35
+ return "未查询到数据"
36
+ total_rows = len(result)
37
+ if total_rows <= max_rows:
38
+ return result.to_string(index=False)
39
+ truncated = result.head(max_rows).to_string(index=False)
40
+ return f"{truncated}\n\n... 共 {total_rows} 行,仅显示前 {max_rows} 行"
41
+
42
+ if isinstance(result, str):
43
+ return result
44
+
45
+ return "未查询到数据"
46
+
47
+
48
+ def safe_call(func: Callable, **kwargs: Any) -> str:
49
+ """统一的 API 调用包装器。
50
+
51
+ 执行流程:
52
+ 1. 自动调用 CredentialManager.ensure_initialized() 确保认证已完成
53
+ 2. 调用目标函数并传入参数
54
+ 3. 使用 format_result 格式化返回值
55
+ 4. 捕获所有异常,返回包含错误类型和错误信息的结构化描述
56
+
57
+ Args:
58
+ func: 要调用的 API 函数。
59
+ **kwargs: 传递给 API 函数的关键字参数。
60
+
61
+ Returns:
62
+ 格式化后的结果字符串,或结构化的错误描述。
63
+ """
64
+ try:
65
+ if not CredentialManager.ensure_initialized():
66
+ return f"API 调用失败:{CredentialManager.last_init_message()}"
67
+ result = func(**kwargs)
68
+ return format_result(result)
69
+ except Exception as e:
70
+ return f"API 调用失败:{type(e).__name__}: {e}"
@@ -0,0 +1,156 @@
1
+ """Tool 注册中心模块。
2
+
3
+ 汇总 tools/ 下 6 个分类模块的所有 tool 定义,提供统一的查询和调用入口。
4
+ 支持懒加载:tool 模块尚未实现时自动跳过,不影响已有模块的注册。
5
+ """
6
+
7
+ import importlib
8
+ import logging
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ from panda_tools.tools import TOOL_MODULES
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ToolRegistry:
17
+ """Tool 注册中心,汇总所有 tool 的定义和调用入口。
18
+
19
+ 从 panda_tools.tools 下的 6 个分类模块自动收集 tool 定义,
20
+ 构建名称→函数映射和类别→tool 列表映射,提供统一的查询与调用接口。
21
+ """
22
+
23
+ def __init__(self) -> None:
24
+ # 名称 → tool 定义(含 function)
25
+ self._tools: Dict[str, Dict[str, Any]] = {}
26
+ # 类别 → tool 定义列表(不含 function,用于对外暴露 schema)
27
+ self._categories: Dict[str, List[Dict[str, Any]]] = {}
28
+ # 名称 → 可调用函数
29
+ self._functions: Dict[str, Any] = {}
30
+
31
+ self._load_all_modules()
32
+
33
+ # ------------------------------------------------------------------
34
+ # 公开接口
35
+ # ------------------------------------------------------------------
36
+
37
+ def get_all_tools(self) -> List[Dict[str, Any]]:
38
+ """返回所有已注册 tool 的 JSON Schema 定义列表。
39
+
40
+ 每个元素包含 name、description、parameters 三个字段,
41
+ 不包含内部使用的 function 引用。
42
+
43
+ Returns:
44
+ tool 定义列表。
45
+ """
46
+ return [self._schema_only(t) for t in self._tools.values()]
47
+
48
+ def get_tools_by_category(self, category: str) -> List[Dict[str, Any]]:
49
+ """按类别返回 tool 的 JSON Schema 定义列表。
50
+
51
+ Args:
52
+ category: 类别标识符,可选值:
53
+ market_data / market_ref / financial / trade / futures
54
+
55
+ Returns:
56
+ 该类别下的 tool 定义列表;类别不存在时返回空列表。
57
+ """
58
+ tools = self._categories.get(category, [])
59
+ return [self._schema_only(t) for t in tools]
60
+
61
+ def call_tool(self, name: str, **kwargs: Any) -> str:
62
+ """根据 tool 名称调用对应函数,返回格式化结果。
63
+
64
+ Args:
65
+ name: tool 名称(与 panda_data API 方法名一致)。
66
+ **kwargs: 传递给 tool 函数的关键字参数。
67
+
68
+ Returns:
69
+ 格式化后的结果字符串,或错误描述。
70
+
71
+ Raises:
72
+ KeyError: 当 tool 名称未注册时抛出。
73
+ """
74
+ if name not in self._functions:
75
+ raise KeyError(f"未注册的 tool:{name}")
76
+ func = self._functions[name]
77
+ return func(**kwargs)
78
+
79
+ def get_tool_names(self) -> List[str]:
80
+ """返回所有已注册 tool 的名称列表。
81
+
82
+ Returns:
83
+ tool 名称列表。
84
+ """
85
+ return list(self._tools.keys())
86
+
87
+ def get_categories(self) -> List[str]:
88
+ """返回所有已注册的类别标识符列表。
89
+
90
+ Returns:
91
+ 类别标识符列表。
92
+ """
93
+ return list(self._categories.keys())
94
+
95
+ @property
96
+ def tool_count(self) -> int:
97
+ """已注册 tool 总数。"""
98
+ return len(self._tools)
99
+
100
+ # ------------------------------------------------------------------
101
+ # 内部方法
102
+ # ------------------------------------------------------------------
103
+
104
+ def _load_all_modules(self) -> None:
105
+ """从 TOOL_MODULES 列表中逐个导入分类模块并注册 tool。
106
+
107
+ 模块尚未实现时捕获 ImportError / ModuleNotFoundError 并跳过,
108
+ 确保已实现的模块可以正常注册。
109
+ """
110
+ for module_path in TOOL_MODULES:
111
+ try:
112
+ module = importlib.import_module(module_path)
113
+ except (ImportError, ModuleNotFoundError) as exc:
114
+ logger.debug("跳过尚未实现的模块 %s: %s", module_path, exc)
115
+ continue
116
+
117
+ category: Optional[str] = getattr(module, "CATEGORY", None)
118
+ tools: Optional[List[Dict]] = getattr(module, "TOOLS", None)
119
+
120
+ if category is None or tools is None:
121
+ logger.warning(
122
+ "模块 %s 缺少 CATEGORY 或 TOOLS 导出,已跳过", module_path
123
+ )
124
+ continue
125
+
126
+ if category not in self._categories:
127
+ self._categories[category] = []
128
+
129
+ for tool_def in tools:
130
+ name = tool_def.get("name")
131
+ if not name:
132
+ logger.warning("模块 %s 中存在缺少 name 的 tool 定义,已跳过", module_path)
133
+ continue
134
+
135
+ self._tools[name] = tool_def
136
+ self._categories[category].append(tool_def)
137
+
138
+ func = tool_def.get("function")
139
+ if func is not None:
140
+ self._functions[name] = func
141
+
142
+ @staticmethod
143
+ def _schema_only(tool_def: Dict[str, Any]) -> Dict[str, Any]:
144
+ """返回 tool 定义的纯 schema 部分(去除 function 引用)。
145
+
146
+ Args:
147
+ tool_def: 完整的 tool 定义字典。
148
+
149
+ Returns:
150
+ 仅包含 name、description、parameters 的字典。
151
+ """
152
+ return {
153
+ "name": tool_def["name"],
154
+ "description": tool_def["description"],
155
+ "parameters": tool_def["parameters"],
156
+ }
panda_tools/sdk.py ADDED
@@ -0,0 +1,30 @@
1
+ """解析 panda_data 顶层 API,兼容不同 SDK / wheel 中的新旧函数名。
2
+
3
+ 若本地 panda_data 与官方 __init__ 导出不一致,会按候选名依次尝试。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Callable
9
+
10
+ import panda_data
11
+
12
+
13
+ def resolve_panda_fn(*names: str) -> Callable[..., Any]:
14
+ """返回 ``panda_data`` 上第一个存在的可调用对象。
15
+
16
+ Args:
17
+ names: 按优先级排列的函数名(新名在前、旧名在后)。
18
+
19
+ Raises:
20
+ AttributeError: 所有候选名均不存在或不可调用。
21
+ """
22
+ for n in names:
23
+ fn = getattr(panda_data, n, None)
24
+ if callable(fn):
25
+ return fn
26
+ raise AttributeError(
27
+ "当前环境的 panda_data 未导出以下任一函数: "
28
+ + ", ".join(names)
29
+ + "。请安装与 PandaAI 文档一致的 panda_data 包并检查版本。"
30
+ )