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.
Files changed (37) hide show
  1. package/bin/cli.js +6 -6
  2. package/package.json +2 -2
  3. package/python/agents/__init__.py +19 -0
  4. package/python/agents/analysis_agent.py +234 -0
  5. package/python/agents/base.py +377 -0
  6. package/python/agents/collector_agent.py +304 -0
  7. package/python/agents/manager_agent.py +251 -0
  8. package/python/agents/planning_agent.py +161 -0
  9. package/python/agents/product_agent.py +672 -0
  10. package/python/agents/report_agent.py +172 -0
  11. package/python/analyzers/__init__.py +10 -0
  12. package/python/analyzers/hot_score.py +123 -0
  13. package/python/analyzers/ranker.py +225 -0
  14. package/python/analyzers/route_planner.py +86 -0
  15. package/python/cli/commands.py +254 -0
  16. package/python/collectors/__init__.py +14 -0
  17. package/python/collectors/ota/ctrip.py +120 -0
  18. package/python/collectors/ota/fliggy.py +152 -0
  19. package/python/collectors/weibo.py +235 -0
  20. package/python/collectors/wenlv.py +155 -0
  21. package/python/collectors/xiaohongshu.py +170 -0
  22. package/python/config/__init__.py +30 -0
  23. package/python/config/models.py +119 -0
  24. package/python/config/prompts.py +105 -0
  25. package/python/config/settings.py +172 -0
  26. package/python/export/__init__.py +6 -0
  27. package/python/export/report.py +192 -0
  28. package/python/main.py +632 -0
  29. package/python/pyproject.toml +51 -0
  30. package/python/scheduler/tasks.py +77 -0
  31. package/python/tools/fliggy_mcp.py +553 -0
  32. package/python/tools/flyai_tools.py +251 -0
  33. package/python/tools/mcp_tools.py +412 -0
  34. package/python/utils/__init__.py +9 -0
  35. package/python/utils/http.py +73 -0
  36. package/python/utils/storage.py +288 -0
  37. package/scripts/postinstall.js +59 -65
@@ -0,0 +1,304 @@
1
+ """采集 Agent - 负责数据采集任务
2
+
3
+ 支持 Tool Use 模式,Claude 可以自主决定调用哪个采集工具
4
+ """
5
+ from typing import Dict, Any, List
6
+ from agents.base import BaseAgent
7
+ from tools.mcp_tools import build_tool, string_property, integer_property
8
+ from collectors.xiaohongshu import XiaohongshuCollector
9
+ from collectors.weibo import WeiboCollector
10
+ from collectors.wenlv import WenlvCollector
11
+ from collectors.ota.fliggy import FliggyCollector
12
+ from collectors.ota.ctrip import CtripCollector
13
+
14
+
15
+ class CollectionAgent(BaseAgent):
16
+ """采集 Agent
17
+
18
+ 职责:
19
+ - 采集社交媒体热点(小红书、微博)
20
+ - 采集文旅局官方信息
21
+ - 采集 OTA 机酒价格
22
+
23
+ 可用工具(MCP 标准格式):
24
+ - search_social_media: 搜索社交媒体
25
+ - collect_wenlv_info: 采集文旅信息
26
+ - search_flights: 搜索机票
27
+ - search_hotels: 搜索酒店
28
+ """
29
+
30
+ name = "collection_agent"
31
+ role = "旅行数据采集专家"
32
+ goal = "从多个来源采集高质量的旅行相关数据,包括社交媒体热点、官方政策和机酒价格"
33
+
34
+ # 定义可用的工具(MCP 标准格式)
35
+ available_tools = {
36
+ "search_social_media": build_tool(
37
+ name="search_social_media",
38
+ description="搜索社交媒体平台上的旅行相关内容",
39
+ properties={
40
+ "keyword": string_property(
41
+ "搜索关键词,例如 '三亚旅行'、'海岛游'"
42
+ ),
43
+ "platform": string_property(
44
+ "目标平台:xiaohongshu(小红书), weibo(微博), all(全部)",
45
+ enum=["xiaohongshu", "weibo", "all"],
46
+ default="all"
47
+ ),
48
+ "limit": integer_property(
49
+ "返回结果数量限制",
50
+ minimum=1,
51
+ maximum=100,
52
+ default=20
53
+ )
54
+ },
55
+ required=["keyword"]
56
+ ),
57
+ "collect_wenlv_info": build_tool(
58
+ name="collect_wenlv_info",
59
+ description="采集各地文旅局官网的政策、活动和推荐路线信息",
60
+ properties={
61
+ "region": string_property(
62
+ "可选的地区名称,例如 '云南'、'浙江',不传则采集全部"
63
+ )
64
+ },
65
+ required=[]
66
+ ),
67
+ "search_flights": build_tool(
68
+ name="search_flights",
69
+ description="搜索航班价格信息",
70
+ properties={
71
+ "departure_city": string_property(
72
+ "出发城市,例如 '北京'、'上海'"
73
+ ),
74
+ "arrival_city": string_property(
75
+ "到达城市,例如 '三亚'、'成都'"
76
+ ),
77
+ "date": string_property(
78
+ "出发日期,格式 YYYY-MM-DD,不传则搜索近期"
79
+ )
80
+ },
81
+ required=["departure_city", "arrival_city"]
82
+ ),
83
+ "search_hotels": build_tool(
84
+ name="search_hotels",
85
+ description="搜索酒店价格信息",
86
+ properties={
87
+ "destination": string_property(
88
+ "目的地城市,例如 '三亚'、'成都'"
89
+ ),
90
+ "price_min": integer_property(
91
+ "最低价格(元/晚)"
92
+ ),
93
+ "price_max": integer_property(
94
+ "最高价格(元/晚)"
95
+ )
96
+ },
97
+ required=["destination"]
98
+ )
99
+ }
100
+
101
+ def __init__(self, provider: str = None, model: str = None, use_tools: bool = True):
102
+ # 先设置工具处理函数,再调用父类初始化
103
+ # 这样 _register_tools() 才能正确注册
104
+ self.tool_handlers = {
105
+ "search_social_media": self._handle_search_social_media,
106
+ "collect_wenlv_info": self._handle_collect_wenlv,
107
+ "search_flights": self._handle_search_flights,
108
+ "search_hotels": self._handle_search_hotels,
109
+ }
110
+
111
+ super().__init__(provider, model, use_tools)
112
+
113
+ async def execute_local(self, task: str, context: Dict[str, Any]) -> str:
114
+ """本地执行采集"""
115
+ sources = context.get("sources", ["xiaohongshu", "weibo", "wenlv"])
116
+ keyword = context.get("keyword", "旅行")
117
+
118
+ results = []
119
+ for source in sources:
120
+ if source in self.collectors:
121
+ results.append(f"✓ {source}: 采集完成(模拟数据)")
122
+
123
+ return f"采集结果:\n" + "\n".join(results)
124
+
125
+ # ========== 工具处理函数 ==========
126
+
127
+ async def _handle_search_social_media(
128
+ self,
129
+ keyword: str,
130
+ platform: str = "all",
131
+ limit: int = 20
132
+ ) -> str:
133
+ """处理社交媒体搜索工具"""
134
+ result = {"posts": [], "stats": {}}
135
+
136
+ platforms = ["xiaohongshu", "weibo"] if platform == "all" else [platform]
137
+
138
+ if "xiaohongshu" in platforms:
139
+ collector = XiaohongshuCollector()
140
+ posts = await collector.search(keyword=keyword, page_size=limit)
141
+ result["posts"].extend([p.model_dump() for p in posts])
142
+ result["stats"]["xiaohongshu_count"] = len(posts)
143
+
144
+ if "weibo" in platforms:
145
+ collector = WeiboCollector()
146
+ posts = await collector.search(keyword=keyword, count=limit)
147
+ result["posts"].extend([p.model_dump() for p in posts])
148
+ result["stats"]["weibo_count"] = len(posts)
149
+
150
+ import json
151
+ return json.dumps(result, ensure_ascii=False, default=str)
152
+
153
+ async def _handle_collect_wenlv(self, region: str = None) -> str:
154
+ """处理文旅信息采集工具"""
155
+ collector = WenlvCollector()
156
+ infos = await collector.collect()
157
+
158
+ # 按地区过滤
159
+ if region:
160
+ infos = [i for i in infos if region in i.region]
161
+
162
+ result = {
163
+ "infos": [i.model_dump() for i in infos],
164
+ "stats": {"wenlv_count": len(infos)}
165
+ }
166
+
167
+ import json
168
+ return json.dumps(result, ensure_ascii=False, default=str)
169
+
170
+ async def _handle_search_flights(
171
+ self,
172
+ departure_city: str,
173
+ arrival_city: str,
174
+ date: str = None
175
+ ) -> str:
176
+ """处理航班搜索工具"""
177
+ collector = FliggyCollector()
178
+ flights = await collector.search_flights(departure_city, arrival_city, date)
179
+
180
+ result = {
181
+ "flights": [f.model_dump() for f in flights],
182
+ "stats": {"count": len(flights)}
183
+ }
184
+
185
+ import json
186
+ return json.dumps(result, ensure_ascii=False, default=str)
187
+
188
+ async def _handle_search_hotels(
189
+ self,
190
+ destination: str,
191
+ price_min: int = None,
192
+ price_max: int = None
193
+ ) -> str:
194
+ """处理酒店搜索工具"""
195
+ collector = FliggyCollector()
196
+ hotels = await collector.search_hotels(
197
+ destination,
198
+ price_min=price_min,
199
+ price_max=price_max
200
+ )
201
+
202
+ result = {
203
+ "hotels": [h.model_dump() for h in hotels],
204
+ "stats": {"count": len(hotels)}
205
+ }
206
+
207
+ import json
208
+ return json.dumps(result, ensure_ascii=False, default=str)
209
+
210
+ # ========== 高级方法(供 Manager 调用) ==========
211
+
212
+ async def collect_social_media(
213
+ self,
214
+ keyword: str = "旅行",
215
+ sources: List[str] = None
216
+ ) -> Dict[str, Any]:
217
+ """采集社交媒体数据"""
218
+ if sources is None:
219
+ sources = ["xiaohongshu", "weibo"]
220
+
221
+ result = {"posts": [], "stats": {}}
222
+
223
+ if "xiaohongshu" in sources:
224
+ collector = XiaohongshuCollector()
225
+ posts = await collector.search(keyword=keyword)
226
+ result["posts"].extend([p.model_dump() for p in posts])
227
+ result["stats"]["xiaohongshu_count"] = len(posts)
228
+
229
+ if "weibo" in sources:
230
+ collector = WeiboCollector()
231
+ posts = await collector.search(keyword=keyword)
232
+ result["posts"].extend([p.model_dump() for p in posts])
233
+ result["stats"]["weibo_count"] = len(posts)
234
+
235
+ return result
236
+
237
+ async def collect_wenlv(self) -> Dict[str, Any]:
238
+ """采集文旅局信息"""
239
+ collector = WenlvCollector()
240
+ infos = await collector.collect()
241
+
242
+ return {
243
+ "infos": [i.model_dump() for i in infos],
244
+ "stats": {"wenlv_count": len(infos)}
245
+ }
246
+
247
+ async def collect_ota(
248
+ self,
249
+ destinations: List[str],
250
+ departure_city: str = "北京"
251
+ ) -> Dict[str, Any]:
252
+ """采集 OTA 机酒数据"""
253
+ result = {"flights": [], "hotels": [], "stats": {}}
254
+
255
+ flight_collector = FliggyCollector()
256
+ hotel_collector = FliggyCollector()
257
+
258
+ for dest in destinations[:5]:
259
+ flights = await flight_collector.search_flights(departure_city, dest)
260
+ hotels = await hotel_collector.search_hotels(dest)
261
+ result["flights"].extend([f.model_dump() for f in flights])
262
+ result["hotels"].extend([h.model_dump() for h in hotels])
263
+
264
+ result["stats"]["flight_count"] = len(result["flights"])
265
+ result["stats"]["hotel_count"] = len(result["hotels"])
266
+
267
+ return result
268
+
269
+ async def collect_all(
270
+ self,
271
+ keyword: str = "旅行",
272
+ include_ota: bool = True
273
+ ) -> Dict[str, Any]:
274
+ """执行完整采集流程"""
275
+ result = {
276
+ "social_media": await self.collect_social_media(keyword),
277
+ "wenlv": await self.collect_wenlv(),
278
+ }
279
+
280
+ if include_ota:
281
+ destinations = self._extract_destinations_from_posts(
282
+ result["social_media"]["posts"]
283
+ )
284
+ result["ota"] = await self.collect_ota(destinations)
285
+
286
+ return result
287
+
288
+ def _extract_destinations_from_posts(self, posts: List[Dict]) -> List[str]:
289
+ """从帖子中提取目的地"""
290
+ common_destinations = [
291
+ "三亚", "云南", "大理", "丽江", "四川", "成都",
292
+ "北京", "上海", "浙江", "杭州", "江苏", "苏州",
293
+ "陕西", "西安", "广西", "桂林", "海南", "西藏",
294
+ "新疆", "甘肃", "青海", "黑龙江", "哈尔滨"
295
+ ]
296
+
297
+ destinations = set()
298
+ for post in posts:
299
+ text = f"{post.get('title', '')} {post.get('content', '')}"
300
+ for dest in common_destinations:
301
+ if dest in text:
302
+ destinations.add(dest)
303
+
304
+ return list(destinations)[:10]
@@ -0,0 +1,251 @@
1
+ """Manager Agent - 多 Agent 系统协调者
2
+
3
+ 负责任务分配、Agent 间协调、结果汇总
4
+ """
5
+ from typing import Dict, Any, List, Optional
6
+ from datetime import datetime
7
+ import json
8
+
9
+ from agents.base import BaseAgent
10
+ from agents.collector_agent import CollectionAgent
11
+ from agents.analysis_agent import AnalysisAgent
12
+ from agents.planning_agent import PlanningAgent
13
+ from agents.report_agent import ReportAgent
14
+
15
+
16
+ class ManagerAgent(BaseAgent):
17
+ """Manager Agent - 多 Agent 系统协调者
18
+
19
+ 职责:
20
+ - 接收用户请求
21
+ - 分解任务并分配给子 Agent
22
+ - 协调 Agent 间的数据流
23
+ - 汇总最终结果
24
+
25
+ 工作流程:
26
+ 1. CollectionAgent → 采集数据
27
+ 2. AnalysisAgent → 分析排序
28
+ 3. PlanningAgent → 路线规划
29
+ 4. ReportAgent → 报告生成
30
+ """
31
+
32
+ name = "manager_agent"
33
+ role = "旅行推荐系统总协调员"
34
+ goal = "协调多个专业 Agent,高效完成旅行目的地推荐任务"
35
+
36
+ def __init__(
37
+ self,
38
+ provider: Optional[str] = None,
39
+ model: Optional[str] = None,
40
+ use_tools: bool = False,
41
+ ):
42
+ super().__init__(provider, model, use_tools)
43
+
44
+ # 初始化子 Agent 团队(使用相同的配置)
45
+ self.team = {
46
+ "collector": CollectionAgent(provider, model, use_tools),
47
+ "analyst": AnalysisAgent(provider, model, use_tools),
48
+ "planner": PlanningAgent(provider, model, use_tools),
49
+ "reporter": ReportAgent(provider, model, use_tools),
50
+ }
51
+
52
+ async def execute_local(self, task: str, context: Dict[str, Any]) -> str:
53
+ """本地执行(降级模式)"""
54
+ return f"Manager Agent 本地执行:{task}"
55
+
56
+ async def run_workflow(
57
+ self,
58
+ keyword: str = "旅行",
59
+ include_ota: bool = True,
60
+ top_n: int = 10,
61
+ plan_top_n: int = 3,
62
+ verbose: bool = True
63
+ ) -> Dict[str, Any]:
64
+ """运行完整工作流
65
+
66
+ Args:
67
+ keyword: 采集关键词
68
+ include_ota: 是否包含 OTA 数据
69
+ top_n: 推荐目的地数量
70
+ plan_top_n: 规划路线的目的地数量
71
+ verbose: 是否输出详细日志
72
+
73
+ Returns:
74
+ 完整结果字典
75
+ """
76
+ result = {
77
+ "status": "running",
78
+ "started_at": datetime.now().isoformat(),
79
+ "stages": {}
80
+ }
81
+
82
+ # ========== Stage 1: 数据采集 ==========
83
+ if verbose:
84
+ print(f"\n[{self.name}] 🚀 开始 Stage 1: 数据采集")
85
+
86
+ collector = self.team["collector"]
87
+ collection_result = await collector.collect_all(keyword, include_ota)
88
+
89
+ result["stages"]["collection"] = {
90
+ "social_media_count": len(collection_result.get("social_media", {}).get("posts", [])),
91
+ "wenlv_count": len(collection_result.get("wenlv", {}).get("infos", [])),
92
+ "flight_count": len(collection_result.get("ota", {}).get("flights", [])),
93
+ "hotel_count": len(collection_result.get("ota", {}).get("hotels", [])),
94
+ }
95
+
96
+ if verbose:
97
+ print(f" ✓ 社交媒体:{result['stages']['collection']['social_media_count']} 条")
98
+ print(f" ✓ 文旅信息:{result['stages']['collection']['wenlv_count']} 条")
99
+ if include_ota:
100
+ print(f" ✓ 航班:{result['stages']['collection']['flight_count']} 条")
101
+ print(f" ✓ 酒店:{result['stages']['collection']['hotel_count']} 条")
102
+
103
+ # ========== Stage 2: 数据分析 ==========
104
+ if verbose:
105
+ print(f"\n[{self.name}] 📊 开始 Stage 2: 数据分析")
106
+
107
+ analyst = self.team["analyst"]
108
+ all_posts = collection_result.get("social_media", {}).get("posts", [])
109
+ all_wenlv = collection_result.get("wenlv", {}).get("infos", [])
110
+ all_flights = collection_result.get("ota", {}).get("flights", [])
111
+ all_hotels = collection_result.get("ota", {}).get("hotels", [])
112
+
113
+ analysis_result = await analyst.analyze_destinations(
114
+ posts=all_posts,
115
+ wenlv_infos=all_wenlv,
116
+ flights=all_flights,
117
+ hotels=all_hotels
118
+ )
119
+
120
+ result["stages"]["analysis"] = {
121
+ "total_destinations": analysis_result.get("total_analyzed", 0),
122
+ "top_destinations": analysis_result.get("top_destinations", [])
123
+ }
124
+
125
+ if verbose:
126
+ print(f" ✓ 分析目的地:{analysis_result.get('total_analyzed')} 个")
127
+ print(f" ✓ TOP 目的地:{', '.join(analysis_result.get('top_destinations', [])[:5])}")
128
+
129
+ # 生成推荐
130
+ recommendations_json = await analyst.generate_recommendations(
131
+ analysis_result,
132
+ top_n=top_n
133
+ )
134
+
135
+ try:
136
+ recommendations = json.loads(recommendations_json)
137
+ except json.JSONDecodeError:
138
+ recommendations = {"destinations": [], "summary": ""}
139
+
140
+ result["stages"]["recommendations"] = recommendations
141
+
142
+ if verbose:
143
+ print(f" ✓ 生成推荐:{len(recommendations.get('destinations', []))} 个")
144
+
145
+ # ========== Stage 3: 路线规划 ==========
146
+ if verbose:
147
+ print(f"\n[{self.name}] 🗺️ 开始 Stage 3: 路线规划")
148
+
149
+ planner = self.team["planner"]
150
+ destinations_for_planning = recommendations.get("destinations", [])[:plan_top_n]
151
+
152
+ routes = await planner.plan_top_destinations(
153
+ destinations_for_planning,
154
+ top_n=plan_top_n
155
+ )
156
+
157
+ result["stages"]["routes"] = routes
158
+
159
+ if verbose:
160
+ print(f" ✓ 规划路线:{len(routes)} 条")
161
+
162
+ # ========== Stage 4: 报告生成 ==========
163
+ if verbose:
164
+ print(f"\n[{self.name}] 📝 开始 Stage 4: 报告生成")
165
+
166
+ reporter = self.team["reporter"]
167
+
168
+ # 生成摘要
169
+ summary = await reporter.write_summary(recommendations.get("destinations", []))
170
+
171
+ # 撰写完整报告
172
+ report_content = await reporter.write_report(
173
+ recommendations=recommendations.get("destinations", []),
174
+ routes=routes,
175
+ summary=summary
176
+ )
177
+
178
+ result["stages"]["report"] = {
179
+ "summary": summary,
180
+ "content": report_content,
181
+ "generated_at": datetime.now().isoformat()
182
+ }
183
+
184
+ if verbose:
185
+ print(f" ✓ 报告生成完成")
186
+ print(f" ✓ 报告长度:{len(report_content)} 字符")
187
+
188
+ # ========== 完成 ==========
189
+ result["status"] = "completed"
190
+ result["completed_at"] = datetime.now().isoformat()
191
+
192
+ if verbose:
193
+ print(f"\n[{self.name}] ✅ 工作流完成!")
194
+
195
+ return result
196
+
197
+ async def quick_recommend(
198
+ self,
199
+ keyword: str = "旅行",
200
+ top_n: int = 5
201
+ ) -> str:
202
+ """快速推荐(简化流程)
203
+
204
+ 仅执行采集和分析,不生成详细路线和报告
205
+
206
+ Args:
207
+ keyword: 搜索关键词
208
+ top_n: 推荐数量
209
+
210
+ Returns:
211
+ 简化推荐结果
212
+ """
213
+ print(f"[{self.name}] 执行快速推荐:{keyword}")
214
+
215
+ # 采集
216
+ collector = self.team["collector"]
217
+ collection = await collector.collect_all(keyword, include_ota=False)
218
+
219
+ # 分析
220
+ analyst = self.team["analyst"]
221
+ posts = collection.get("social_media", {}).get("posts", [])
222
+ wenlv = collection.get("wenlv", {}).get("infos", [])
223
+
224
+ analysis = await analyst.analyze_destinations(posts, wenlv)
225
+ recommendations = await analyst.generate_recommendations(analysis, top_n)
226
+
227
+ return recommendations
228
+
229
+ def get_team_status(self) -> Dict[str, Any]:
230
+ """获取团队状态
231
+
232
+ Returns:
233
+ 各 Agent 状态信息
234
+ """
235
+ return {
236
+ "manager": {
237
+ "name": self.name,
238
+ "role": self.role,
239
+ "goal": self.goal,
240
+ "model": self.model
241
+ },
242
+ "team_members": {
243
+ name: {
244
+ "name": agent.name,
245
+ "role": agent.role,
246
+ "goal": agent.goal
247
+ }
248
+ for name, agent in self.team.items()
249
+ },
250
+ "api_configured": self.client is not None
251
+ }