travel-agent-cli 0.2.0 → 0.2.2
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.
- package/bin/cli.js +6 -6
- package/package.json +2 -2
- package/python/agents/__init__.py +19 -0
- package/python/agents/analysis_agent.py +234 -0
- package/python/agents/base.py +377 -0
- package/python/agents/collector_agent.py +304 -0
- package/python/agents/manager_agent.py +251 -0
- package/python/agents/planning_agent.py +161 -0
- package/python/agents/product_agent.py +672 -0
- package/python/agents/report_agent.py +172 -0
- package/python/analyzers/__init__.py +10 -0
- package/python/analyzers/hot_score.py +123 -0
- package/python/analyzers/ranker.py +225 -0
- package/python/analyzers/route_planner.py +86 -0
- package/python/cli/commands.py +254 -0
- package/python/collectors/__init__.py +14 -0
- package/python/collectors/ota/ctrip.py +120 -0
- package/python/collectors/ota/fliggy.py +152 -0
- package/python/collectors/weibo.py +235 -0
- package/python/collectors/wenlv.py +155 -0
- package/python/collectors/xiaohongshu.py +170 -0
- package/python/config/__init__.py +30 -0
- package/python/config/models.py +119 -0
- package/python/config/prompts.py +105 -0
- package/python/config/settings.py +172 -0
- package/python/export/__init__.py +6 -0
- package/python/export/report.py +192 -0
- package/python/main.py +632 -0
- package/python/pyproject.toml +51 -0
- package/python/scheduler/tasks.py +77 -0
- package/python/tools/fliggy_mcp.py +553 -0
- package/python/tools/flyai_tools.py +251 -0
- package/python/tools/mcp_tools.py +412 -0
- package/python/utils/__init__.py +9 -0
- package/python/utils/http.py +73 -0
- package/python/utils/storage.py +288 -0
- package/scripts/postinstall.js +59 -65
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""FlyAI 风格工具 - 旅行搜索服务
|
|
3
|
+
|
|
4
|
+
基于 ClawHub FlyAI 设计模式,集成 Fliggy MCP 真实 API
|
|
5
|
+
提供:
|
|
6
|
+
- 🔍 Search: 自然语言旅行搜索
|
|
7
|
+
- ✈️ Flights: 航班价格查询
|
|
8
|
+
- 🏨 Hotels: 酒店搜索
|
|
9
|
+
- 🎭 Attractions: 景点推荐
|
|
10
|
+
"""
|
|
11
|
+
from typing import Dict, Any, List, Optional
|
|
12
|
+
from tools.mcp_tools import build_tool, string_property, integer_property
|
|
13
|
+
from tools.fliggy_mcp import FliggyMCPClient, FliggyToolHandlers
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# FlyAI 工具定义(MCP 格式)
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
def build_flyai_tools() -> Dict[str, Any]:
|
|
21
|
+
"""构建 FlyAI 风格的工具集"""
|
|
22
|
+
return {
|
|
23
|
+
"travel_search": build_travel_search_tool(),
|
|
24
|
+
"search_flights": build_search_flights_tool(),
|
|
25
|
+
"search_hotels": build_search_hotels_tool(),
|
|
26
|
+
"search_attractions": build_search_attractions_tool(),
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_travel_search_tool() -> Dict[str, Any]:
|
|
31
|
+
"""🔍 自然语言旅行搜索工具"""
|
|
32
|
+
return build_tool(
|
|
33
|
+
name="travel_search",
|
|
34
|
+
description="使用自然语言搜索旅行相关信息,包括目的地、行程、价格等综合信息",
|
|
35
|
+
properties={
|
|
36
|
+
"query": string_property("搜索查询,例如'5 天日本行程规划'、'三亚海岛游'等"),
|
|
37
|
+
"destination": string_property("目的地(可选)"),
|
|
38
|
+
"duration": string_property("行程天数,例如'5 天'、'3-5 天'"),
|
|
39
|
+
"budget": string_property("预算范围,例如'5000-10000 元'"),
|
|
40
|
+
},
|
|
41
|
+
required=["query"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_search_flights_tool() -> Dict[str, Any]:
|
|
46
|
+
"""✈️ 航班搜索工具"""
|
|
47
|
+
return build_tool(
|
|
48
|
+
name="search_flights",
|
|
49
|
+
description="搜索航班价格和时间",
|
|
50
|
+
properties={
|
|
51
|
+
"origin": string_property("出发城市,例如'北京'、'上海'"),
|
|
52
|
+
"destination": string_property("目的地城市,例如'东京'、'曼谷'"),
|
|
53
|
+
"departure_date": string_property("出发日期,格式 YYYY-MM-DD 或'下周五'"),
|
|
54
|
+
"return_date": string_property("返程日期(可选),格式 YYYY-MM-DD"),
|
|
55
|
+
"cabin_class": string_property(
|
|
56
|
+
"舱位等级",
|
|
57
|
+
enum=["economy", "premium", "business", "first"],
|
|
58
|
+
default="economy"
|
|
59
|
+
),
|
|
60
|
+
},
|
|
61
|
+
required=["origin", "destination"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_search_hotels_tool() -> Dict[str, Any]:
|
|
66
|
+
"""🏨 酒店搜索工具"""
|
|
67
|
+
return build_tool(
|
|
68
|
+
name="search_hotels",
|
|
69
|
+
description="搜索酒店价格和房源",
|
|
70
|
+
properties={
|
|
71
|
+
"destination": string_property("目的地城市或地区"),
|
|
72
|
+
"check_in": string_property("入住日期,格式 YYYY-MM-DD 或'明天'"),
|
|
73
|
+
"check_out": string_property("退房日期(可选),格式 YYYY-MM-DD"),
|
|
74
|
+
"guests": integer_property("入住人数", minimum=1, maximum=10, default=2),
|
|
75
|
+
"star_rating": string_property(
|
|
76
|
+
"星级要求",
|
|
77
|
+
enum=["3", "4", "5", "luxury"],
|
|
78
|
+
default="4"
|
|
79
|
+
),
|
|
80
|
+
"price_min": integer_property("最低价格", minimum=0, default=0),
|
|
81
|
+
"price_max": integer_property("最高价格", minimum=0, default=5000),
|
|
82
|
+
},
|
|
83
|
+
required=["destination"],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def build_search_attractions_tool() -> Dict[str, Any]:
|
|
88
|
+
"""🎭 景点搜索工具"""
|
|
89
|
+
return build_tool(
|
|
90
|
+
name="search_attractions",
|
|
91
|
+
description="搜索目的地景点和活动",
|
|
92
|
+
properties={
|
|
93
|
+
"destination": string_property("目的地城市或地区"),
|
|
94
|
+
"category": string_property(
|
|
95
|
+
"景点类型",
|
|
96
|
+
enum=["nature", "culture", "entertainment", "food", "shopping", "all"],
|
|
97
|
+
default="all"
|
|
98
|
+
),
|
|
99
|
+
"duration": string_property("游玩时长,例如'半天'、'一天'"),
|
|
100
|
+
"group_type": string_property(
|
|
101
|
+
"出行类型",
|
|
102
|
+
enum=["family", "couple", "solo", "friends"],
|
|
103
|
+
default="family"
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
required=["destination"],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# =============================================================================
|
|
111
|
+
# FlyAI 工具处理器
|
|
112
|
+
# =============================================================================
|
|
113
|
+
|
|
114
|
+
class FlyAIToolHandlers:
|
|
115
|
+
"""FlyAI 工具处理器(集成 Fliggy MCP)"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, app_key: Optional[str] = None, app_secret: Optional[str] = None):
|
|
118
|
+
"""初始化
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
app_key: 飞猪开放平台 App Key(可选,不传则使用沙箱模式)
|
|
122
|
+
app_secret: 飞猪开放平台 App Secret(可选)
|
|
123
|
+
"""
|
|
124
|
+
self.client = FliggyMCPClient(
|
|
125
|
+
app_key=app_key,
|
|
126
|
+
app_secret=app_secret,
|
|
127
|
+
use_sandbox=(app_key is None), # 未配置 App Key 时使用沙箱
|
|
128
|
+
)
|
|
129
|
+
self.fliggy_handlers = FliggyToolHandlers(self.client)
|
|
130
|
+
|
|
131
|
+
async def handle_travel_search(
|
|
132
|
+
self,
|
|
133
|
+
query: str,
|
|
134
|
+
destination: Optional[str] = None,
|
|
135
|
+
duration: Optional[str] = None,
|
|
136
|
+
budget: Optional[str] = None
|
|
137
|
+
) -> str:
|
|
138
|
+
"""处理旅行搜索请求"""
|
|
139
|
+
# 智能识别查询类型并调用相应的 Fliggy API
|
|
140
|
+
import re
|
|
141
|
+
|
|
142
|
+
# 检测航班查询
|
|
143
|
+
if re.search(r'(航班 | 机票|flight|fly|机场 | 飞)', query.lower()):
|
|
144
|
+
return await self.handle_search_flights(
|
|
145
|
+
origin=query,
|
|
146
|
+
destination=destination or "目的地",
|
|
147
|
+
departure_date=duration,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# 检测酒店查询
|
|
151
|
+
if re.search(r'(酒店|住宿|hotel|民宿 | 旅馆)', query.lower()):
|
|
152
|
+
return await self.handle_search_hotels(
|
|
153
|
+
destination=destination or query.replace("酒店", "").replace("hotel", "").strip(),
|
|
154
|
+
check_in=duration,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# 检测景点查询
|
|
158
|
+
if re.search(r'(景点 | 景区 | 游玩 | 玩|attraction|tourist)', query.lower()):
|
|
159
|
+
return await self.handle_search_attractions(
|
|
160
|
+
destination=destination or query,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# 综合旅行搜索 - 调用多个 API
|
|
164
|
+
results = []
|
|
165
|
+
|
|
166
|
+
# 1. 搜索景点
|
|
167
|
+
dest = destination or "旅行目的地"
|
|
168
|
+
attraction_result = await self.handle_search_attractions(dest)
|
|
169
|
+
results.append(f"🎭 景点推荐\n{attraction_result}")
|
|
170
|
+
|
|
171
|
+
# 2. 搜索酒店
|
|
172
|
+
hotel_result = await self.handle_search_hotels(dest)
|
|
173
|
+
results.append(f"🏨 酒店推荐\n{hotel_result}")
|
|
174
|
+
|
|
175
|
+
return "\n".join(results)
|
|
176
|
+
|
|
177
|
+
async def handle_search_flights(
|
|
178
|
+
self,
|
|
179
|
+
origin: str,
|
|
180
|
+
destination: str,
|
|
181
|
+
departure_date: Optional[str] = None,
|
|
182
|
+
return_date: Optional[str] = None,
|
|
183
|
+
cabin_class: str = "economy"
|
|
184
|
+
) -> str:
|
|
185
|
+
"""处理航班搜索请求"""
|
|
186
|
+
try:
|
|
187
|
+
return await self.fliggy_handlers.search_flights(
|
|
188
|
+
origin=origin,
|
|
189
|
+
destination=destination,
|
|
190
|
+
departure_date=departure_date,
|
|
191
|
+
return_date=return_date,
|
|
192
|
+
cabin_class=cabin_class,
|
|
193
|
+
)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return f"航班搜索失败:{e}"
|
|
196
|
+
|
|
197
|
+
async def handle_search_hotels(
|
|
198
|
+
self,
|
|
199
|
+
destination: str,
|
|
200
|
+
check_in: Optional[str] = None,
|
|
201
|
+
check_out: Optional[str] = None,
|
|
202
|
+
guests: int = 2,
|
|
203
|
+
star_rating: str = "4",
|
|
204
|
+
price_min: int = 0,
|
|
205
|
+
price_max: int = 5000
|
|
206
|
+
) -> str:
|
|
207
|
+
"""处理酒店搜索请求"""
|
|
208
|
+
try:
|
|
209
|
+
return await self.fliggy_handlers.search_hotels(
|
|
210
|
+
destination=destination,
|
|
211
|
+
check_in=check_in,
|
|
212
|
+
check_out=check_out,
|
|
213
|
+
guests=guests,
|
|
214
|
+
star_rating=star_rating,
|
|
215
|
+
price_min=price_min,
|
|
216
|
+
price_max=price_max,
|
|
217
|
+
)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
return f"酒店搜索失败:{e}"
|
|
220
|
+
|
|
221
|
+
async def handle_search_attractions(
|
|
222
|
+
self,
|
|
223
|
+
destination: str,
|
|
224
|
+
category: str = "all",
|
|
225
|
+
duration: Optional[str] = None,
|
|
226
|
+
group_type: str = "family"
|
|
227
|
+
) -> str:
|
|
228
|
+
"""处理景点搜索请求"""
|
|
229
|
+
try:
|
|
230
|
+
return await self.fliggy_handlers.search_attractions(
|
|
231
|
+
destination=destination,
|
|
232
|
+
category=category,
|
|
233
|
+
duration=duration,
|
|
234
|
+
)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
return f"景点搜索失败:{e}"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# =============================================================================
|
|
240
|
+
# 工具工厂函数
|
|
241
|
+
# =============================================================================
|
|
242
|
+
|
|
243
|
+
def get_flyai_tool_handlers() -> Dict[str, callable]:
|
|
244
|
+
"""获取 FlyAI 工具处理器"""
|
|
245
|
+
handlers = FlyAIToolHandlers()
|
|
246
|
+
return {
|
|
247
|
+
"travel_search": handlers.handle_travel_search,
|
|
248
|
+
"search_flights": handlers.handle_search_flights,
|
|
249
|
+
"search_hotels": handlers.handle_search_hotels,
|
|
250
|
+
"search_attractions": handlers.handle_search_attractions,
|
|
251
|
+
}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""MCP 风格工具定义模块
|
|
2
|
+
|
|
3
|
+
参考 @modelcontextprotocol/sdk 的 Tool 接口设计
|
|
4
|
+
https://github.com/modelcontextprotocol/python-sdk
|
|
5
|
+
|
|
6
|
+
核心设计原则:
|
|
7
|
+
1. 工具定义与执行逻辑分离
|
|
8
|
+
2. 使用标准的 JSON Schema 格式
|
|
9
|
+
3. 支持动态工具注册
|
|
10
|
+
4. 工具元数据(名称、描述、schema)集中管理
|
|
11
|
+
"""
|
|
12
|
+
from typing import Any, Callable, Dict, List, Optional, TypedDict, Union
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# MCP 风格 Tool 定义(参考 @modelcontextprotocol/sdk/types.js)
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
class JsonSchemaProperty(TypedDict, total=False):
|
|
21
|
+
"""JSON Schema 属性定义"""
|
|
22
|
+
type: str
|
|
23
|
+
description: str
|
|
24
|
+
enum: List[Any]
|
|
25
|
+
default: Any
|
|
26
|
+
minimum: int
|
|
27
|
+
maximum: int
|
|
28
|
+
items: Dict[str, Any]
|
|
29
|
+
minItems: int
|
|
30
|
+
maxItems: int
|
|
31
|
+
required: List[str]
|
|
32
|
+
properties: Dict[str, "JsonSchemaProperty"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JsonSchemaObject(TypedDict, total=False):
|
|
36
|
+
"""JSON Schema 对象定义"""
|
|
37
|
+
type: str
|
|
38
|
+
description: str
|
|
39
|
+
properties: Dict[str, JsonSchemaProperty]
|
|
40
|
+
required: List[str]
|
|
41
|
+
items: Dict[str, Any]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ToolInputSchema(TypedDict):
|
|
45
|
+
"""工具输入 Schema(MCP 标准格式)"""
|
|
46
|
+
type: str # 固定为 "object"
|
|
47
|
+
properties: Dict[str, JsonSchemaProperty]
|
|
48
|
+
required: List[str]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Tool(TypedDict):
|
|
52
|
+
"""MCP Tool 接口定义
|
|
53
|
+
|
|
54
|
+
参考:@modelcontextprotocol/sdk/types.js:Tool
|
|
55
|
+
|
|
56
|
+
示例:
|
|
57
|
+
{
|
|
58
|
+
"name": "search_social_media",
|
|
59
|
+
"description": "搜索社交媒体平台上的旅行相关内容",
|
|
60
|
+
"inputSchema": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"keyword": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"description": "搜索关键词"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"required": ["keyword"]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
"""
|
|
72
|
+
name: str
|
|
73
|
+
description: str
|
|
74
|
+
inputSchema: ToolInputSchema
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# =============================================================================
|
|
78
|
+
# 工具处理器类型定义
|
|
79
|
+
# =============================================================================
|
|
80
|
+
|
|
81
|
+
# 同步工具处理函数类型
|
|
82
|
+
SyncToolHandler = Callable[..., Any]
|
|
83
|
+
|
|
84
|
+
# 异步工具处理函数类型
|
|
85
|
+
AsyncToolHandler = Callable[..., Any]
|
|
86
|
+
|
|
87
|
+
# 工具处理函数(同步或异步)
|
|
88
|
+
ToolHandler = Union[SyncToolHandler, AsyncToolHandler]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# =============================================================================
|
|
92
|
+
# 工具注册表
|
|
93
|
+
# =============================================================================
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class ToolRegistration:
|
|
97
|
+
"""工具注册信息
|
|
98
|
+
|
|
99
|
+
将工具定义与处理函数绑定在一起
|
|
100
|
+
"""
|
|
101
|
+
definition: Tool
|
|
102
|
+
handler: ToolHandler
|
|
103
|
+
is_async: bool = False
|
|
104
|
+
|
|
105
|
+
def __post_init__(self):
|
|
106
|
+
# 检测是否为异步函数
|
|
107
|
+
import inspect
|
|
108
|
+
if inspect.iscoroutinefunction(self.handler):
|
|
109
|
+
self.is_async = True
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ToolRegistry:
|
|
113
|
+
"""工具注册表
|
|
114
|
+
|
|
115
|
+
管理所有已注册工具的定义和处理器
|
|
116
|
+
|
|
117
|
+
使用示例:
|
|
118
|
+
registry = ToolRegistry()
|
|
119
|
+
|
|
120
|
+
# 定义工具
|
|
121
|
+
search_tool = Tool(
|
|
122
|
+
name="search_social_media",
|
|
123
|
+
description="搜索社交媒体",
|
|
124
|
+
inputSchema={
|
|
125
|
+
"type": "object",
|
|
126
|
+
"properties": {
|
|
127
|
+
"keyword": {"type": "string", "description": "关键词"}
|
|
128
|
+
},
|
|
129
|
+
"required": ["keyword"]
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# 注册工具
|
|
134
|
+
async def search_handler(keyword: str) -> str:
|
|
135
|
+
return f"搜索结果:{keyword}"
|
|
136
|
+
|
|
137
|
+
registry.register(search_tool, search_handler)
|
|
138
|
+
|
|
139
|
+
# 调用工具
|
|
140
|
+
result = await registry.call("search_social_media", keyword="旅行")
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def __init__(self):
|
|
144
|
+
self._tools: Dict[str, ToolRegistration] = {}
|
|
145
|
+
|
|
146
|
+
def register(
|
|
147
|
+
self,
|
|
148
|
+
definition: Tool,
|
|
149
|
+
handler: ToolHandler,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""注册工具
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
definition: 工具定义(MCP 格式)
|
|
155
|
+
handler: 工具处理函数
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ValueError: 工具名称已存在
|
|
159
|
+
"""
|
|
160
|
+
tool_name = definition["name"]
|
|
161
|
+
if tool_name in self._tools:
|
|
162
|
+
raise ValueError(f"工具 '{tool_name}' 已注册")
|
|
163
|
+
|
|
164
|
+
self._tools[tool_name] = ToolRegistration(
|
|
165
|
+
definition=definition,
|
|
166
|
+
handler=handler,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def unregister(self, tool_name: str) -> None:
|
|
170
|
+
"""注销工具"""
|
|
171
|
+
if tool_name in self._tools:
|
|
172
|
+
del self._tools[tool_name]
|
|
173
|
+
|
|
174
|
+
def get_definition(self, tool_name: str) -> Optional[Tool]:
|
|
175
|
+
"""获取工具定义"""
|
|
176
|
+
reg = self._tools.get(tool_name)
|
|
177
|
+
return reg.definition if reg else None
|
|
178
|
+
|
|
179
|
+
def get_definitions(self) -> List[Tool]:
|
|
180
|
+
"""获取所有工具定义(用于传递给 LLM)"""
|
|
181
|
+
return [reg.definition for reg in self._tools.values()]
|
|
182
|
+
|
|
183
|
+
def get_handler(self, tool_name: str) -> Optional[ToolHandler]:
|
|
184
|
+
"""获取工具处理函数"""
|
|
185
|
+
reg = self._tools.get(tool_name)
|
|
186
|
+
return reg.handler if reg else None
|
|
187
|
+
|
|
188
|
+
async def call(
|
|
189
|
+
self,
|
|
190
|
+
tool_name: str,
|
|
191
|
+
**kwargs: Any,
|
|
192
|
+
) -> Any:
|
|
193
|
+
"""调用工具
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
tool_name: 工具名称
|
|
197
|
+
**kwargs: 工具参数
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
工具执行结果
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
ValueError: 工具不存在
|
|
204
|
+
"""
|
|
205
|
+
reg = self._tools.get(tool_name)
|
|
206
|
+
if not reg:
|
|
207
|
+
raise ValueError(f"未知工具:{tool_name}")
|
|
208
|
+
|
|
209
|
+
# 执行处理函数
|
|
210
|
+
result = reg.handler(**kwargs)
|
|
211
|
+
|
|
212
|
+
# 如果是异步函数,等待结果
|
|
213
|
+
if reg.is_async:
|
|
214
|
+
return await result
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
def has_tool(self, tool_name: str) -> bool:
|
|
218
|
+
"""检查工具是否已注册"""
|
|
219
|
+
return tool_name in self._tools
|
|
220
|
+
|
|
221
|
+
def list_tools(self) -> List[str]:
|
|
222
|
+
"""列出所有已注册工具名称"""
|
|
223
|
+
return list(self._tools.keys())
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# =============================================================================
|
|
227
|
+
# 工具定义辅助函数
|
|
228
|
+
# =============================================================================
|
|
229
|
+
|
|
230
|
+
def string_property(
|
|
231
|
+
description: str,
|
|
232
|
+
enum: List[str] = None,
|
|
233
|
+
default: str = None,
|
|
234
|
+
) -> JsonSchemaProperty:
|
|
235
|
+
"""创建字符串类型属性定义
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
description: 属性描述
|
|
239
|
+
enum: 可选的枚举值
|
|
240
|
+
default: 默认值
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
JSON Schema 属性定义
|
|
244
|
+
"""
|
|
245
|
+
prop: JsonSchemaProperty = {
|
|
246
|
+
"type": "string",
|
|
247
|
+
"description": description,
|
|
248
|
+
}
|
|
249
|
+
if enum:
|
|
250
|
+
prop["enum"] = enum
|
|
251
|
+
if default:
|
|
252
|
+
prop["default"] = default
|
|
253
|
+
return prop
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def integer_property(
|
|
257
|
+
description: str,
|
|
258
|
+
minimum: int = None,
|
|
259
|
+
maximum: int = None,
|
|
260
|
+
default: int = None,
|
|
261
|
+
) -> JsonSchemaProperty:
|
|
262
|
+
"""创建整数类型属性定义"""
|
|
263
|
+
prop: JsonSchemaProperty = {
|
|
264
|
+
"type": "integer",
|
|
265
|
+
"description": description,
|
|
266
|
+
}
|
|
267
|
+
if minimum is not None:
|
|
268
|
+
prop["minimum"] = minimum
|
|
269
|
+
if maximum is not None:
|
|
270
|
+
prop["maximum"] = maximum
|
|
271
|
+
if default is not None:
|
|
272
|
+
prop["default"] = default
|
|
273
|
+
return prop
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def boolean_property(
|
|
277
|
+
description: str,
|
|
278
|
+
default: bool = None,
|
|
279
|
+
) -> JsonSchemaProperty:
|
|
280
|
+
"""创建布尔类型属性定义"""
|
|
281
|
+
prop: JsonSchemaProperty = {
|
|
282
|
+
"type": "boolean",
|
|
283
|
+
"description": description,
|
|
284
|
+
}
|
|
285
|
+
if default is not None:
|
|
286
|
+
prop["default"] = default
|
|
287
|
+
return prop
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def array_property(
|
|
291
|
+
description: str,
|
|
292
|
+
items: Dict[str, Any],
|
|
293
|
+
minItems: int = None,
|
|
294
|
+
maxItems: int = None,
|
|
295
|
+
) -> JsonSchemaProperty:
|
|
296
|
+
"""创建数组类型属性定义"""
|
|
297
|
+
prop: JsonSchemaProperty = {
|
|
298
|
+
"type": "array",
|
|
299
|
+
"description": description,
|
|
300
|
+
"items": items,
|
|
301
|
+
}
|
|
302
|
+
if minItems is not None:
|
|
303
|
+
prop["minItems"] = minItems
|
|
304
|
+
if maxItems is not None:
|
|
305
|
+
prop["maxItems"] = maxItems
|
|
306
|
+
return prop
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def object_property(
|
|
310
|
+
description: str,
|
|
311
|
+
properties: Dict[str, JsonSchemaProperty],
|
|
312
|
+
required: List[str] = None,
|
|
313
|
+
) -> JsonSchemaProperty:
|
|
314
|
+
"""创建对象类型属性定义"""
|
|
315
|
+
prop: JsonSchemaProperty = {
|
|
316
|
+
"type": "object",
|
|
317
|
+
"description": description,
|
|
318
|
+
"properties": properties,
|
|
319
|
+
}
|
|
320
|
+
if required:
|
|
321
|
+
prop["required"] = required
|
|
322
|
+
return prop
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def build_tool(
|
|
326
|
+
name: str,
|
|
327
|
+
description: str,
|
|
328
|
+
properties: Dict[str, JsonSchemaProperty],
|
|
329
|
+
required: List[str] = None,
|
|
330
|
+
) -> Tool:
|
|
331
|
+
"""构建 MCP 格式的工具定义
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
name: 工具名称
|
|
335
|
+
description: 工具描述
|
|
336
|
+
properties: 输入属性定义
|
|
337
|
+
required: 必填参数列表
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
MCP 格式 Tool 对象
|
|
341
|
+
|
|
342
|
+
使用示例:
|
|
343
|
+
tool = build_tool(
|
|
344
|
+
name="search_social_media",
|
|
345
|
+
description="搜索社交媒体内容",
|
|
346
|
+
properties={
|
|
347
|
+
"keyword": string_property("搜索关键词"),
|
|
348
|
+
"platform": string_property(
|
|
349
|
+
"目标平台",
|
|
350
|
+
enum=["xiaohongshu", "weibo", "all"],
|
|
351
|
+
default="all"
|
|
352
|
+
),
|
|
353
|
+
"limit": integer_property("结果数量", minimum=1, maximum=100, default=20)
|
|
354
|
+
},
|
|
355
|
+
required=["keyword"]
|
|
356
|
+
)
|
|
357
|
+
"""
|
|
358
|
+
return Tool(
|
|
359
|
+
name=name,
|
|
360
|
+
description=description,
|
|
361
|
+
inputSchema=ToolInputSchema(
|
|
362
|
+
type="object",
|
|
363
|
+
properties=properties,
|
|
364
|
+
required=required or [],
|
|
365
|
+
),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# =============================================================================
|
|
370
|
+
# Agent 基类集成
|
|
371
|
+
# =============================================================================
|
|
372
|
+
|
|
373
|
+
class ToolEnabledMixin:
|
|
374
|
+
"""工具启用 Mixin
|
|
375
|
+
|
|
376
|
+
为 Agent 提供工具注册和调用能力
|
|
377
|
+
|
|
378
|
+
使用示例:
|
|
379
|
+
class CollectionAgent(BaseAgent, ToolEnabledMixin):
|
|
380
|
+
def __init__(self):
|
|
381
|
+
super().__init__()
|
|
382
|
+
ToolEnabledMixin.__init__(self)
|
|
383
|
+
|
|
384
|
+
# 注册工具
|
|
385
|
+
self.register_tool(
|
|
386
|
+
build_tool(...),
|
|
387
|
+
self._handle_search
|
|
388
|
+
)
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
def __init__(self):
|
|
392
|
+
self.tool_registry = ToolRegistry()
|
|
393
|
+
|
|
394
|
+
def register_tool(
|
|
395
|
+
self,
|
|
396
|
+
definition: Tool,
|
|
397
|
+
handler: ToolHandler,
|
|
398
|
+
) -> None:
|
|
399
|
+
"""注册工具到 Agent"""
|
|
400
|
+
self.tool_registry.register(definition, handler)
|
|
401
|
+
|
|
402
|
+
def get_tool_definitions(self) -> List[Tool]:
|
|
403
|
+
"""获取所有工具定义(用于传递给 LLM)"""
|
|
404
|
+
return self.tool_registry.get_definitions()
|
|
405
|
+
|
|
406
|
+
async def call_tool(
|
|
407
|
+
self,
|
|
408
|
+
tool_name: str,
|
|
409
|
+
**kwargs: Any,
|
|
410
|
+
) -> Any:
|
|
411
|
+
"""调用工具"""
|
|
412
|
+
return await self.tool_registry.call(tool_name, **kwargs)
|