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
package/python/main.py ADDED
@@ -0,0 +1,632 @@
1
+ # -*- coding: utf-8 -*-
2
+ """旅行 Agent CLI - 新入口(参考 Claude Code 设计)
3
+
4
+ 设计原则:
5
+ 1. 简洁的命令定义
6
+ 2. 清晰的帮助信息
7
+ 3. 支持交互和非交互模式
8
+ 4. 统一的输出格式
9
+
10
+ 使用方法:
11
+ python main.py <command> [args]
12
+
13
+ 或直接运行:
14
+ travel-agent <command> [args]
15
+ """
16
+ import sys
17
+ import asyncio
18
+ from typing import Optional, List
19
+ from pathlib import Path
20
+
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.table import Table
24
+ from rich import box
25
+
26
+ from cli.commands import CommandRegistry, Command, OutputFormatter
27
+ from config.settings import get_settings
28
+
29
+ # 创建命令注册表
30
+ registry = CommandRegistry()
31
+ console = Console()
32
+
33
+
34
+ # =============================================================================
35
+ # 命令定义(参考 Claude Code 的命令模式)
36
+ # =============================================================================
37
+
38
+ @registry.command(
39
+ name="run",
40
+ aliases=["start", "execute"],
41
+ description="运行完整工作流:采集数据 → 分析评分 → 路线规划 → 报告生成",
42
+ argument_hint="[options]",
43
+ immediate=True,
44
+ )
45
+ async def cmd_run(keyword: str = "旅行", top_n: int = 10, plan_top_n: int = 3, no_ota: bool = False):
46
+ """运行完整工作流"""
47
+ from agents.manager_agent import ManagerAgent
48
+
49
+ console.print(Panel(
50
+ f"关键词:[bold]{keyword}[/bold]\n"
51
+ f"推荐数量:[bold]{top_n}[/bold]\n"
52
+ f"路线规划:[bold]{plan_top_n}[/bold]\n"
53
+ f"OTA 采集:[bold]{'关闭' if no_ota else '开启'}[/bold]",
54
+ title="🚀 运行工作流",
55
+ border_style="blue",
56
+ ))
57
+
58
+ manager = ManagerAgent()
59
+ result = await manager.run_workflow(
60
+ keyword=keyword,
61
+ include_ota=not no_ota,
62
+ top_n=top_n,
63
+ plan_top_n=plan_top_n,
64
+ verbose=True,
65
+ )
66
+
67
+ # 显示结果摘要
68
+ _show_result_summary(result)
69
+
70
+
71
+ @registry.command(
72
+ name="analyze",
73
+ aliases=["research"],
74
+ description="分析旅行产品可行性:市场调研 → 资源评估 → SWOT 分析 → 生成报告",
75
+ argument_hint="<topic> [options]",
76
+ immediate=True,
77
+ )
78
+ async def cmd_analyze(topic: str, no_ota: bool = False, output: Optional[str] = None):
79
+ """分析旅行产品可行性"""
80
+ from agents.product_agent import ProductAnalysisAgent
81
+
82
+ console.print(Panel(
83
+ f"分析话题:[bold]{topic}[/bold]\n"
84
+ f"OTA 采集:[bold]{'关闭' if no_ota else '开启'}[/bold]",
85
+ title="🔍 产品可行性分析",
86
+ border_style="cyan",
87
+ ))
88
+
89
+ agent = ProductAnalysisAgent()
90
+ result = await agent.analyze_topic(
91
+ topic=topic,
92
+ include_news=True,
93
+ include_ota=not no_ota,
94
+ verbose=True,
95
+ )
96
+
97
+ _show_product_analysis_result(result)
98
+
99
+ # 保存报告
100
+ report_content = result.get("stages", {}).get("report", {}).get("content", "")
101
+ if report_content and output:
102
+ output_path = Path(output)
103
+ output_path.parent.mkdir(parents=True, exist_ok=True)
104
+ with open(output_path, "w", encoding="utf-8") as f:
105
+ f.write(report_content)
106
+ console.print(f"\n[green]✓ 报告已保存:{output_path}[/green]")
107
+
108
+
109
+ @registry.command(
110
+ name="model",
111
+ description="管理 LLM 模型配置:查看/切换 提供商和模型",
112
+ argument_hint="<subcommand> [options]",
113
+ immediate=True,
114
+ )
115
+ async def cmd_model(action: Optional[str] = None, provider: Optional[str] = None, model_name: Optional[str] = None):
116
+ """管理 LLM 模型配置"""
117
+ settings = get_settings()
118
+
119
+ if action == "list" or action is None:
120
+ _show_model_list(settings)
121
+ elif action == "status":
122
+ _show_model_status(settings)
123
+ elif action == "use":
124
+ if not provider:
125
+ console.print("[red]错误:请使用 -p 指定提供商[/red]")
126
+ return
127
+ _switch_model(settings, provider, model_name)
128
+ else:
129
+ console.print(f"[red]未知操作:{action}[/red]")
130
+ console.print("使用 `travel-agent model list` 查看所有支持的模型")
131
+
132
+
133
+ @registry.command(
134
+ name="config",
135
+ aliases=["settings"],
136
+ description="管理配置:显示当前配置或初始化配置文件",
137
+ argument_hint="[options]",
138
+ immediate=True,
139
+ )
140
+ async def cmd_config(show: bool = False, init: bool = False):
141
+ """管理配置"""
142
+ settings = get_settings()
143
+
144
+ if init:
145
+ _init_config()
146
+
147
+ if show:
148
+ _show_config(settings)
149
+
150
+ if not show and not init:
151
+ console.print("使用方法:travel-agent config [选项]")
152
+ console.print(" --show 显示当前配置")
153
+ console.print(" --init 初始化配置文件")
154
+
155
+
156
+ @registry.command(
157
+ name="status",
158
+ aliases=["health"],
159
+ description="显示系统状态:版本、模型、API 配置、数据目录",
160
+ immediate=True,
161
+ )
162
+ async def cmd_status():
163
+ """显示系统状态"""
164
+ settings = get_settings()
165
+
166
+ console.print(Panel(
167
+ f"版本:[bold]0.2.0[/bold]\n"
168
+ f"提供商:[bold]{settings.llm_provider}[/bold]\n"
169
+ f"模型:[bold]{settings.get_active_model()}[/bold]\n"
170
+ f"API 已配置:{'是' if settings.get_active_api_key() else '否'}\n"
171
+ f"可用提供商:{', '.join(settings.get_configured_providers()) or '无'}",
172
+ title="📊 系统状态",
173
+ border_style="green",
174
+ ))
175
+
176
+
177
+ @registry.command(
178
+ name="flyai",
179
+ aliases=["search", "travel"],
180
+ description="FlyAI 旅行搜索:搜索航班、酒店、景点和行程规划",
181
+ argument_hint="<query> [options]",
182
+ immediate=True,
183
+ )
184
+ async def cmd_flyai(query: Optional[str] = None, destination: Optional[str] = None, duration: Optional[str] = None, budget: Optional[str] = None):
185
+ """FlyAI 旅行搜索
186
+
187
+ 使用自然语言搜索旅行相关信息,包括:
188
+ - 🔍 综合旅行搜索
189
+ - ✈️ 航班价格查询
190
+ - 🏨 酒店搜索
191
+ - 🎭 景点推荐
192
+
193
+ 示例:
194
+ travel-agent flyai "5 天日本行程规划"
195
+ travel-agent flyai "北京到上海航班" --destination 上海
196
+ travel-agent flyai "三亚酒店" --destination 三亚
197
+ travel-agent flyai "东京景点推荐" --destination 东京
198
+ """
199
+ from tools.flyai_tools import FlyAIToolHandlers, build_flyai_tools
200
+
201
+ if not query:
202
+ console.print("[red]错误:请提供搜索查询[/red]")
203
+ console.print("示例:travel-agent flyai \"5 天日本行程规划\"")
204
+ return
205
+
206
+ console.print(Panel(
207
+ f"查询:[bold]{query}[/bold]\n"
208
+ f"目的地:[bold]{destination or '未指定'}[/bold]\n"
209
+ f"行程:[bold]{duration or '未指定'}[/bold]\n"
210
+ f"预算:[bold]{budget or '未指定'}[/bold]",
211
+ title="🔍 FlyAI 旅行搜索",
212
+ border_style="blue",
213
+ ))
214
+
215
+ handler = FlyAIToolHandlers()
216
+
217
+ # 根据查询内容自动选择工具
218
+ query_lower = query.lower()
219
+ if any(kw in query_lower for kw in ["航班", "flight", "fly", "机票", "机场"]):
220
+ # 航班搜索
221
+ result = await handler.handle_search_flights(
222
+ origin=query.replace("航班", "").replace("flight", "").strip(),
223
+ destination=destination or "未指定",
224
+ departure_date=duration,
225
+ )
226
+ elif any(kw in query_lower for kw in ["酒店", "hotel", "住宿", "民宿"]):
227
+ # 酒店搜索
228
+ result = await handler.handle_search_hotels(
229
+ destination=destination or query.replace("酒店", "").replace("hotel", "").strip(),
230
+ check_in=duration,
231
+ )
232
+ elif any(kw in query_lower for kw in ["景点", "attraction", "play", "玩", "旅游"]):
233
+ # 景点搜索
234
+ result = await handler.handle_search_attractions(
235
+ destination=destination or query,
236
+ )
237
+ else:
238
+ # 综合旅行搜索
239
+ result = await handler.handle_travel_search(
240
+ query=query,
241
+ destination=destination,
242
+ duration=duration,
243
+ budget=budget,
244
+ )
245
+
246
+ console.print(result)
247
+
248
+
249
+ @registry.command(
250
+ name="agents",
251
+ aliases=["team"],
252
+ description="查看 Agent 团队信息",
253
+ argument_hint="[action]",
254
+ immediate=True,
255
+ )
256
+ async def cmd_agents(action: Optional[str] = "info"):
257
+ """查看 Agent 团队信息"""
258
+ from agents.manager_agent import ManagerAgent
259
+
260
+ if action == "info":
261
+ manager = ManagerAgent()
262
+ team_status = manager.get_team_status()
263
+ _show_team_info(team_status)
264
+ else:
265
+ console.print("使用方法:travel-agent agents [info]")
266
+
267
+
268
+ @registry.command(
269
+ name="help",
270
+ description="显示帮助信息",
271
+ argument_hint="[command]",
272
+ immediate=True,
273
+ supports_non_interactive=True,
274
+ )
275
+ async def cmd_help(command_name: Optional[str] = None):
276
+ """显示帮助信息"""
277
+ if command_name:
278
+ cmd = registry.get(command_name)
279
+ if cmd:
280
+ console.print(OutputFormatter.format_command_help(cmd))
281
+ else:
282
+ console.print(f"[red]未知命令:{command_name}[/red]")
283
+ else:
284
+ _show_help()
285
+
286
+
287
+ # =============================================================================
288
+ # 主入口
289
+ # =============================================================================
290
+
291
+ def main():
292
+ """CLI 主入口"""
293
+ args = sys.argv[1:]
294
+
295
+ # 处理全局选项
296
+ if "--version" in args or "-v" in args:
297
+ console.print("travel-agent v0.2.0")
298
+ return
299
+
300
+ # 检查是否请求特定命令的帮助
301
+ if len(args) >= 2 and ("--help" in args or "-h" in args):
302
+ # 例如:python main.py analyze --help
303
+ cmd_name = args[0]
304
+ cmd = registry.get(cmd_name)
305
+ if cmd:
306
+ console.print(OutputFormatter.format_command_help(cmd))
307
+ return
308
+
309
+ if "--help" in args or "-h" in args or len(args) == 0:
310
+ asyncio.run(cmd_help.handler())
311
+ return
312
+
313
+ # 解析命令和参数
314
+ command_name = args[0]
315
+ command_args = _parse_args(args[1:], command_name)
316
+
317
+ # 执行命令
318
+ cmd = registry.get(command_name)
319
+ if not cmd:
320
+ console.print(f"[red]未知命令:{command_name}[/red]")
321
+ console.print("")
322
+ console.print("使用 `travel-agent help` 查看所有可用命令")
323
+ sys.exit(1)
324
+
325
+ # 检查非交互模式支持
326
+ if not sys.stdin.isatty() and not cmd.supports_non_interactive:
327
+ console.print("[red]错误:此命令不支持非交互模式[/red]")
328
+ sys.exit(1)
329
+
330
+ # 执行命令
331
+ try:
332
+ asyncio.run(cmd.handler(**command_args))
333
+ except Exception as e:
334
+ console.print(f"[red]错误:{e}[/red]")
335
+ sys.exit(1)
336
+
337
+
338
+ def _parse_args(args: List[str], command_name: str = "") -> dict:
339
+ """解析命令行参数"""
340
+ result = {}
341
+ i = 0
342
+ while i < len(args):
343
+ arg = args[i]
344
+ if arg.startswith("--"):
345
+ key = arg[2:].replace("-", "_")
346
+ # 布尔标志
347
+ if i + 1 >= len(args) or args[i + 1].startswith("--"):
348
+ result[key] = True
349
+ else:
350
+ result[key] = args[i + 1]
351
+ i += 1
352
+ elif arg.startswith("-") and len(arg) == 2:
353
+ short_key = arg[1]
354
+ # 为 run 命令映射短选项
355
+ if command_name == "run":
356
+ if short_key == "k":
357
+ short_key = "keyword"
358
+ elif short_key == "n":
359
+ short_key = "top_n"
360
+ elif short_key == "p":
361
+ short_key = "plan_top_n"
362
+ # 为 model 命令映射短选项
363
+ elif command_name == "model":
364
+ if short_key == "p":
365
+ short_key = "provider"
366
+ elif short_key == "m":
367
+ short_key = "model_name"
368
+ if i + 1 < len(args) and not args[i + 1].startswith("-"):
369
+ result[short_key] = args[i + 1]
370
+ i += 1
371
+ else:
372
+ result[short_key] = True
373
+ else:
374
+ # 位置参数:根据命令类型分配
375
+ if command_name == "analyze":
376
+ result["topic"] = arg
377
+ elif command_name == "model":
378
+ if "action" not in result:
379
+ result["action"] = arg
380
+ elif "model_name" not in result and arg.startswith("-m") is False:
381
+ result["model_name"] = arg
382
+ elif command_name == "help":
383
+ result["command_name"] = arg
384
+ elif command_name == "agents":
385
+ result["action"] = arg
386
+ elif command_name == "flyai":
387
+ # flyai 命令的参数
388
+ if "query" not in result:
389
+ result["query"] = arg
390
+ elif "destination" not in result:
391
+ result["destination"] = arg
392
+ elif "duration" not in result:
393
+ result["duration"] = arg
394
+ elif "budget" not in result:
395
+ result["budget"] = arg
396
+ elif command_name == "run":
397
+ # run 命令的第一个位置参数是 keyword
398
+ if "keyword" not in result:
399
+ result["keyword"] = arg
400
+ else:
401
+ result["top_n"] = arg
402
+ else:
403
+ # 默认:第一个位置参数作为 topic 或 keyword
404
+ if "topic" not in result:
405
+ result["topic"] = arg
406
+ elif "keyword" not in result:
407
+ result["keyword"] = arg
408
+ i += 1
409
+ return result
410
+
411
+
412
+ # =============================================================================
413
+ # 辅助函数
414
+ # =============================================================================
415
+
416
+ def _show_help():
417
+ """显示帮助信息"""
418
+ console.print(Panel(
419
+ "AI 驱动的旅行目的地推荐 Agent",
420
+ title="🤖 travel-agent",
421
+ border_style="blue",
422
+ ))
423
+ console.print("")
424
+
425
+ commands = registry.list_commands()
426
+ console.print(OutputFormatter.format_command_list(commands))
427
+
428
+ console.print("")
429
+ console.print("[dim]使用 `travel-agent help <command>` 查看单个命令的详细帮助[/dim]")
430
+
431
+
432
+ def _show_model_list(settings):
433
+ """显示模型列表"""
434
+ from config.settings import LLM_PROVIDERS
435
+
436
+ console.print("[bold]支持的 LLM 提供商和模型:[/bold]\n")
437
+
438
+ for prov, info in LLM_PROVIDERS.items():
439
+ configured = "✓" if settings.is_provider_configured(prov) else " "
440
+ console.print(f"[{configured}] {prov}:")
441
+ for model in info["models"]:
442
+ console.print(f" - {model}")
443
+ console.print()
444
+
445
+
446
+ def _show_model_status(settings):
447
+ """显示模型状态"""
448
+ config = settings.get_llm_config()
449
+
450
+ console.print(Panel(
451
+ f"当前提供商:[bold]{config['provider']}[/bold]\n"
452
+ f"当前模型:[bold]{config['model']}[/bold]\n"
453
+ f"API 已配置:{'是' if config['api_key_configured'] else '否'}\n"
454
+ f"可用提供商:{', '.join(config['available_providers']) or '无'}",
455
+ title="🤖 LLM 模型状态",
456
+ border_style="blue",
457
+ ))
458
+
459
+
460
+ def _switch_model(settings, provider: str, model_name: Optional[str]):
461
+ """切换模型"""
462
+ from config.settings import LLM_PROVIDERS
463
+
464
+ if provider not in LLM_PROVIDERS:
465
+ console.print(f"[red]错误:不支持的提供商 {provider}[/red]")
466
+ console.print(f"支持的提供商:{', '.join(LLM_PROVIDERS.keys())}")
467
+ return
468
+
469
+ if not settings.is_provider_configured(provider):
470
+ console.print(f"[red]错误:{provider} 未配置[/red]")
471
+ console.print("请先在 .env 文件中配置对应的 API Key")
472
+ return
473
+
474
+ # 更新配置
475
+ _update_model_config(provider, model_name)
476
+
477
+
478
+ def _update_model_config(provider: str, model_name: Optional[str]):
479
+ """更新模型配置"""
480
+ env_path = Path(".env")
481
+ if not env_path.exists():
482
+ console.print("[yellow].env 文件不存在,正在创建...[/yellow]")
483
+ import shutil
484
+ shutil.copy(".env.example", env_path)
485
+
486
+ config_lines = []
487
+ with open(env_path, "r") as f:
488
+ config_lines = f.readlines()
489
+
490
+ updated = False
491
+ new_lines = []
492
+ for line in config_lines:
493
+ if line.startswith("LLM_PROVIDER="):
494
+ new_lines.append(f"LLM_PROVIDER={provider}\n")
495
+ updated = True
496
+ elif line.startswith("LLM_MODEL="):
497
+ if model_name:
498
+ new_lines.append(f"LLM_MODEL={model_name}\n")
499
+ updated = True
500
+ else:
501
+ new_lines.append("LLM_MODEL=\n")
502
+ updated = True
503
+ else:
504
+ new_lines.append(line)
505
+
506
+ if not updated:
507
+ new_lines.append(f"\nLLM_PROVIDER={provider}\n")
508
+ if model_name:
509
+ new_lines.append(f"LLM_MODEL={model_name}\n")
510
+ else:
511
+ new_lines.append("LLM_MODEL=\n")
512
+
513
+ with open(env_path, "w") as f:
514
+ f.writelines(new_lines)
515
+
516
+ msg = f"✓ 已切换到 {provider}"
517
+ if model_name:
518
+ msg += f"/{model_name}"
519
+ else:
520
+ msg += " (使用默认模型)"
521
+ console.print(f"[green]{msg}[/green]")
522
+
523
+
524
+ def _init_config():
525
+ """初始化配置文件"""
526
+ import shutil
527
+ src = Path(".env.example")
528
+ dst = Path(".env")
529
+ if not dst.exists():
530
+ shutil.copy(src, dst)
531
+ console.print("[green]已创建 .env 配置文件[/green]")
532
+ else:
533
+ console.print("[yellow].env 文件已存在[/yellow]")
534
+
535
+
536
+ def _show_config(settings):
537
+ """显示配置"""
538
+ console.print("[bold]当前配置:[/bold]")
539
+ console.print(f"数据库路径:{settings.database_path}")
540
+ console.print(f"输出目录:{settings.output_dir}")
541
+ console.print()
542
+ console.print("[bold]LLM 配置:[/bold]")
543
+ console.print(f"当前提供商:{settings.llm_provider}")
544
+ console.print(f"当前模型:{settings.get_active_model()}")
545
+ console.print()
546
+ console.print("[bold]API Key 配置状态:[/bold]")
547
+ providers = {
548
+ "anthropic": ("Anthropic", settings.anthropic_api_key),
549
+ "openai": ("OpenAI", settings.openai_api_key),
550
+ "deepseek": ("DeepSeek", settings.deepseek_api_key),
551
+ "azure": ("Azure OpenAI", settings.azure_openai_api_key and settings.azure_openai_endpoint),
552
+ "qwen": ("阿里云通义千问", settings.dashscope_api_key),
553
+ "ollama": ("Ollama (本地)", "✓"),
554
+ }
555
+ for prov, (name, configured) in providers.items():
556
+ status = "✓ 已配置" if configured else "✗ 未配置"
557
+ marker = "→" if prov == settings.llm_provider else " "
558
+ console.print(f" {marker} {name}: {status}")
559
+
560
+
561
+ def _show_team_info(team_status: dict):
562
+ """显示 Agent 团队信息"""
563
+ settings = get_settings()
564
+
565
+ console.print("\n[bold cyan]🤖 Agent 团队信息[/bold cyan]")
566
+
567
+ table = Table(show_header=True, header_style="bold magenta", box=box.SIMPLE)
568
+ table.add_column("Agent", style="cyan")
569
+ table.add_column("角色", style="green")
570
+ table.add_column("职责", style="yellow")
571
+
572
+ m = team_status["manager"]
573
+ table.add_row("Manager", m["role"], m["goal"])
574
+
575
+ for name, info in team_status["team_members"].items():
576
+ table.add_row(name.capitalize(), info["role"], info["goal"])
577
+
578
+ console.print(table)
579
+
580
+ console.print()
581
+ console.print(f"LLM 提供商:[bold]{settings.llm_provider}[/bold]")
582
+ console.print(f"模型:[bold]{settings.get_active_model()}[/bold]")
583
+ console.print(f"可用提供商:{', '.join(settings.get_configured_providers()) or '无'}")
584
+
585
+
586
+ def _show_result_summary(result: dict):
587
+ """显示结果摘要"""
588
+ console.print("\n[bold green]✓ 工作流完成![/bold green]")
589
+
590
+ stages = result.get("stages", {})
591
+
592
+ collection = stages.get("collection", {})
593
+ console.print(f" • 采集数据:社交媒体 {collection.get('social_media_count', 0)} 条,"
594
+ f"文旅 {collection.get('wenlv_count', 0)} 条")
595
+
596
+ analysis = stages.get("analysis", {})
597
+ top_dests = analysis.get("top_destinations", [])
598
+ if top_dests:
599
+ console.print(f" • TOP 目的地:{', '.join(top_dests[:5])}")
600
+
601
+ routes = stages.get("routes", [])
602
+ console.print(f" • 规划路线:{len(routes)} 条")
603
+
604
+ report = stages.get("report", {})
605
+ console.print(f" • 报告生成:{len(report.get('content', ''))} 字符")
606
+
607
+
608
+ def _show_product_analysis_result(result: dict):
609
+ """显示产品分析结果摘要"""
610
+ console.print("\n[bold green]✓ 分析完成![/bold green]")
611
+
612
+ stages = result.get("stages", {})
613
+
614
+ collection = stages.get("collection", {})
615
+ console.print(f" • 采集数据:社交媒体 {collection.get('social_media_count', 0)} 条,"
616
+ f"文旅 {collection.get('wenlv_count', 0)} 条")
617
+
618
+ market_demand = stages.get("market_demand", {})
619
+ console.print(f" • 社交媒体热度:{market_demand.get('heat_level', '未知')}")
620
+
621
+ resources = stages.get("resources", {})
622
+ console.print(f" • 航线资源:{resources.get('flight_availability', '未知')}")
623
+ console.print(f" • 酒店资源:{resources.get('hotel_options', '未知')}")
624
+
625
+ assessment = stages.get("assessment", {})
626
+ console.print(f" • 推荐指数:{assessment.get('recommendation_score', 0)}/10")
627
+ console.print(f" • 市场潜力:{assessment.get('market_potential', '未知')}")
628
+ console.print(f" • 风险等级:{assessment.get('risk_level', '未知')}")
629
+
630
+
631
+ if __name__ == "__main__":
632
+ main()
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "travel-agent"
7
+ version = "0.2.1"
8
+ description = "AI-powered travel destination recommendation agent"
9
+ authors = [{ name = "Your Name", email = "your.email@example.com" }]
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "anthropic>=0.18.0",
14
+ "openai>=1.0.0",
15
+ "playwright>=1.40.0",
16
+ "beautifulsoup4>=4.12.0",
17
+ "typer>=0.9.0",
18
+ "APScheduler>=3.10.0",
19
+ "pydantic>=2.5.0",
20
+ "pydantic-settings>=2.1.0",
21
+ "jinja2>=3.1.0",
22
+ "python-dotenv>=1.0.0",
23
+ "httpx>=0.26.0",
24
+ "aiosqlite>=0.19.0",
25
+ "rich>=13.7.0",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=7.4.0",
31
+ "pytest-asyncio>=0.21.0",
32
+ "black>=23.12.0",
33
+ "ruff>=0.1.0",
34
+ ]
35
+ pdf = [
36
+ "weasyprint>=60.0",
37
+ ]
38
+
39
+ [project.scripts]
40
+ travel-agent = "main:app"
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["."]
44
+
45
+ [tool.ruff]
46
+ line-length = 100
47
+ target-version = "py310"
48
+
49
+ [tool.black]
50
+ line-length = 100
51
+ target-version = ["py310"]