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,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
|
+
}
|