travel-agent-cli 0.3.2 → 0.3.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/python/interactive.py +151 -238
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "travel-agent-cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "AI 驱动的旅行目的地推荐 Agent - 命令行工具(集成 FlyAI 旅行搜索)",
5
5
  "bin": {
6
6
  "travel-agent": "bin/cli.js",
@@ -7,209 +7,103 @@
7
7
  import asyncio
8
8
  from textual.app import App, ComposeResult
9
9
  from textual.widgets import Header, Footer, Static, Input, RichLog
10
- from textual.containers import Container, Horizontal, Vertical
11
- from textual.screen import Screen
10
+ from textual.containers import Container, Vertical
12
11
  from textual.binding import Binding
13
12
  from textual import work
14
- from rich.panel import Panel
13
+ from textual.command import CommandPalette, Provider, Hit
14
+ from textual.screen import Screen
15
15
  from rich.markdown import Markdown
16
16
  from rich.text import Text
17
+ from rich.panel import Panel
17
18
 
18
19
  from main import run_command
19
20
  from config.settings import get_settings
20
21
 
21
22
 
22
- class ChatOutput(Static):
23
- """聊天输出区域"""
24
-
25
- def __init__(self):
26
- super().__init__()
27
- self.messages = []
28
-
29
- def compose(self) -> ComposeResult:
30
- yield Static(id="chat-content")
31
-
32
- def add_message(self, role: str, content: str):
33
- """添加消息到聊天记录"""
34
- self.messages.append((role, content))
35
- self._update_display()
36
-
37
- def _update_display(self):
38
- """更新显示内容"""
39
- content_widget = self.query_one("#chat-content", Static)
40
- lines = []
41
-
42
- for role, content in self.messages:
43
- if role == "user":
44
- lines.append(f"[bold blue]> {content}[/bold blue]")
45
- else:
46
- lines.append(f"{content}")
47
- lines.append("")
48
-
49
- content_widget.update("\n".join(lines))
50
-
51
-
52
- class CommandPalette(Screen):
53
- """命令选择面板(按 / 唤起)"""
23
+ class CommandProvider(Provider):
24
+ """命令提供者 - 用于命令面板"""
25
+
26
+ @property
27
+ def commands(self) -> list[tuple[str, str, str]]:
28
+ return [
29
+ ("run", "运行完整工作流", "搜索关键词 -n 推荐数"),
30
+ ("flyai", "FlyAI 旅行搜索", "查询词如:5 天日本行程"),
31
+ ("analyze", "产品可行性分析", "分析话题如:北欧极光"),
32
+ ("model", "模型管理", "list/status/use"),
33
+ ("config", "配置管理", "--show/--init"),
34
+ ("status", "系统状态", "查看版本/API 配置"),
35
+ ("agents", "Agent 团队", "查看 Agent 信息"),
36
+ ("help", "帮助信息", "显示帮助"),
37
+ ]
38
+
39
+ async def search(self, query: str) -> list[Hit]:
40
+ results = []
41
+ query_lower = query.lower()
42
+ for cmd, desc, usage in self.commands:
43
+ if query_lower in cmd.lower() or query_lower in desc.lower():
44
+ results.append(
45
+ Hit(
46
+ score=1.0,
47
+ match=Text(f"/{cmd} - {desc}"),
48
+ text=Text(f"用法:{usage}", style="dim"),
49
+ )
50
+ )
51
+ return results
52
+
53
+
54
+ class ChatScreen(Screen):
55
+ """主聊天界面"""
56
+
57
+ AUTO_FOCUS = "#chat-input" # 自动聚焦输入框
54
58
 
55
59
  BINDINGS = [
56
- Binding("escape", "close", "关闭", show=True),
57
- Binding("up", "navigate_up", "", show=False),
58
- Binding("down", "navigate_down", "", show=False),
59
- Binding("enter", "select", "选择", show=True),
60
- ]
61
-
62
- COMMANDS = [
63
- ("run", "运行完整工作流", "/run"),
64
- ("flyai", "FlyAI 旅行搜索", "/flyai <查询>"),
65
- ("analyze", "产品可行性分析", "/analyze <话题>"),
66
- ("model", "模型管理", "/model list|status|use"),
67
- ("config", "配置管理", "/config --show|--init"),
68
- ("status", "系统状态", "/status"),
69
- ("agents", "Agent 团队", "/agents"),
70
- ("help", "帮助信息", "/help"),
60
+ Binding("ctrl+q", "quit", "退出"),
61
+ Binding("ctrl+g", "submit", "提交"),
62
+ Binding("/", "command_palette", "命令"),
63
+ Binding("ctrl+l", "clear", "清屏"),
71
64
  ]
72
65
 
73
- def __init__(self):
74
- super().__init__()
75
- self.selected_index = 0
76
-
77
- def compose(self) -> ComposeResult:
78
- yield Static(self._render_commands(), id="command-list")
79
-
80
- def _render_commands(self) -> str:
81
- """渲染命令列表"""
82
- lines = ["[bold]可用命令[/bold]\n"]
83
- for i, (cmd, desc, usage) in enumerate(self.COMMANDS):
84
- marker = "→ " if i == self.selected_index else " "
85
- lines.append(f"{marker}[cyan]/{cmd}[/cyan] - {desc}")
86
- lines.append(f" [dim]{usage}[/dim]")
87
- lines.append("")
88
- return "\n".join(lines)
89
-
90
- def on_key(self, event):
91
- """处理键盘事件"""
92
- if event.key == "up":
93
- self.selected_index = (self.selected_index - 1) % len(self.COMMANDS)
94
- elif event.key == "down":
95
- self.selected_index = (self.selected_index + 1) % len(self.COMMANDS)
96
- elif event.key == "enter":
97
- self.dismiss(self.COMMANDS[self.selected_index][0])
98
- elif event.key == "escape":
99
- self.dismiss(None)
100
-
101
- self.query_one("#command-list", Static).update(self._render_commands())
102
-
103
-
104
- class TravelAgentApp(App):
105
- """travel-agent 交互式应用"""
106
-
107
- TITLE = "travel-agent"
108
- SUB_TITLE = "AI 驱动的旅行目的地推荐"
109
-
110
- CSS = """
111
- Screen {
112
- background: $surface;
113
- }
114
-
115
- #main-container {
116
- height: 100%;
117
- }
118
-
119
- #chat-output {
120
- height: 1fr;
121
- border: solid $primary;
122
- margin: 1 1 0 1;
123
- padding: 1;
124
- background: $surface;
125
- }
126
-
127
- #input-area {
128
- height: auto;
129
- max-height: 3;
130
- margin: 1;
131
- padding: 1;
132
- background: $primary-background;
133
- border: solid $primary;
134
- }
135
-
136
- #command-palette {
137
- width: 60;
138
- height: 20;
139
- align: center middle;
140
- background: $surface;
141
- border: solid $primary;
142
- padding: 1 2;
143
- }
144
- """
145
-
146
- BINDINGS = [
147
- Binding("ctrl+q", "quit", "退出", show=True),
148
- Binding("/", "command_palette", "命令", show=True),
149
- Binding("ctrl+h", "toggle_help", "帮助", show=True),
150
- Binding("ctrl+g", "submit_input", "提交", show=True),
151
- ]
152
-
153
- def __init__(self):
154
- super().__init__()
155
- self.messages = []
156
- self.command_history = []
157
- self.history_index = -1
158
- self.chat_output = None
159
- self.input_widget = None
160
-
161
66
  def compose(self) -> ComposeResult:
162
67
  yield Header(show_clock=True)
163
- with Container(id="main-container"):
164
- with Vertical():
165
- yield Static("", id="chat-output")
166
- yield Input(placeholder="输入消息或 / 命令...", id="input-area")
68
+ with Vertical():
69
+ # 聊天输出区域 - 使用 RichLog 支持 Markdown
70
+ yield RichLog(id="chat-output", highlight=True, markup=True)
71
+ # 输入区域
72
+ yield Input(
73
+ placeholder="输入消息或按 / 打开命令面板...",
74
+ id="chat-input"
75
+ )
167
76
  yield Footer()
168
77
 
169
78
  def on_mount(self) -> None:
170
- """应用启动时"""
171
- self.chat_output = self.query_one("#chat-output", Static)
172
- self.input_widget = self.query_one("#input-area", Input)
173
- self.input_widget.focus()
174
-
79
+ """屏幕挂载时"""
175
80
  # 显示欢迎信息
176
81
  self._add_welcome_message()
177
82
 
178
83
  def _add_welcome_message(self):
179
84
  """显示欢迎信息"""
85
+ log = self.query_one("#chat-output", RichLog)
180
86
  welcome = """
181
- 欢迎使用 travel-agent 交互式 CLI
182
-
183
- 可用命令:
184
- /run - 运行完整工作流
185
- /flyai - FlyAI 旅行搜索
186
- /analyze - 产品可行性分析
187
- /model - 模型管理
188
- /config - 配置管理
189
- /status - 系统状态
190
- /agents - 查看 Agent 团队
191
-
192
- / 打开命令面板,Ctrl+Q 退出
87
+ # 欢迎使用 travel-agent 交互式 CLI
88
+
89
+ ## 可用命令
90
+ - `/run` - 运行完整工作流
91
+ - `/flyai` - FlyAI 旅行搜索
92
+ - `/analyze` - 产品可行性分析
93
+ - `/model` - 模型管理
94
+ - `/config` - 配置管理
95
+ - `/status` - 系统状态
96
+ - `/agents` - 查看 Agent 团队
97
+
98
+ ## 快捷键
99
+ - `Ctrl+G` - 提交输入
100
+ - `/` - 打开命令面板
101
+ - `Ctrl+L` - 清屏
102
+ - `Ctrl+Q` - 退出
103
+
104
+ 直接输入自然语言查询(如"5 天日本行程规划")将使用 FlyAI 搜索。
193
105
  """
194
- self.messages.append(("system", welcome))
195
- self._update_chat_output()
196
-
197
- def _update_chat_output(self):
198
- """更新聊天输出"""
199
- if not self.chat_output:
200
- return
201
-
202
- lines = []
203
- for role, content in self.messages:
204
- if role == "user":
205
- lines.append(f"[bold blue]> {content}[/bold blue]")
206
- elif role == "system":
207
- lines.append(f"[dim]{content}[/dim]")
208
- else:
209
- lines.append(f"{content}")
210
- lines.append("")
211
-
212
- self.chat_output.update("\n".join(lines))
106
+ log.write(Markdown(welcome))
213
107
 
214
108
  def on_input_submitted(self, event: Input.Submitted) -> None:
215
109
  """处理输入提交"""
@@ -217,27 +111,24 @@ class TravelAgentApp(App):
217
111
  if not user_input:
218
112
  return
219
113
 
220
- # 添加到历史记录
221
- self.command_history.append(user_input)
222
- self.history_index = len(self.command_history)
223
-
224
- # 显示用户消息
225
- self.messages.append(("user", user_input))
226
- self._update_chat_output()
227
-
228
114
  # 清空输入框
229
115
  event.input.value = ""
230
116
 
117
+ # 显示用户消息
118
+ log = self.query_one("#chat-output", RichLog)
119
+ log.write(Text(f"> {user_input}", style="bold blue"))
120
+
231
121
  # 处理命令
232
122
  self._handle_command(user_input)
233
123
 
234
124
  @work(exclusive=True)
235
125
  async def _handle_command(self, command: str):
236
126
  """处理命令(异步工作)"""
127
+ log = self.query_one("#chat-output", RichLog)
128
+
237
129
  try:
238
130
  # 显示处理中状态
239
- self.messages.append(("system", "正在处理..."))
240
- self._update_chat_output()
131
+ log.write(Text("正在处理...", style="dim"))
241
132
 
242
133
  if command.startswith("/"):
243
134
  # 处理斜杠命令
@@ -246,17 +137,15 @@ class TravelAgentApp(App):
246
137
  # 默认使用 flyai 处理自然语言
247
138
  result = await self._handle_natural_language(command)
248
139
 
249
- # 移除处理中状态
250
- self.messages = [(r, c) for r, c in self.messages if c != "正在处理..."]
251
-
252
140
  # 显示结果
253
- self.messages.append(("assistant", result))
254
- self._update_chat_output()
141
+ if result:
142
+ log.write(Panel(result, title="结果", border_style="green"))
255
143
 
256
144
  except Exception as e:
257
- self.messages = [(r, c) for r, c in self.messages if c != "正在处理..."]
258
- self.messages.append(("system", f"[red]错误:{e}[/red]"))
259
- self._update_chat_output()
145
+ log.write(Text(f"错误:{e}", style="red"))
146
+
147
+ # 重新聚焦输入框
148
+ self.query_one("#chat-input", Input).focus()
260
149
 
261
150
  async def _handle_slash_command(self, command: str) -> str:
262
151
  """处理斜杠命令"""
@@ -264,58 +153,82 @@ class TravelAgentApp(App):
264
153
  cmd_name = parts[0]
265
154
  args = parts[1] if len(parts) > 1 else ""
266
155
 
267
- try:
268
- result = await run_command(cmd_name, args)
269
- return result
270
- except Exception as e:
271
- return f"命令执行失败:{e}"
156
+ result = await run_command(cmd_name, args)
157
+ return result
272
158
 
273
159
  async def _handle_natural_language(self, query: str) -> str:
274
160
  """处理自然语言查询(默认使用 flyai)"""
275
- try:
276
- result = await run_command("flyai", query)
277
- return result
278
- except Exception as e:
279
- return f"查询失败:{e}"
161
+ result = await run_command("flyai", query)
162
+ return result
280
163
 
281
- def action_quit(self):
164
+ def action_quit(self) -> None:
282
165
  """退出应用"""
283
166
  self.exit()
284
167
 
285
- def action_command_palette(self):
168
+ def action_submit(self) -> None:
169
+ """提交输入"""
170
+ input_widget = self.query_one("#chat-input", Input)
171
+ input_widget._submit()
172
+
173
+ def action_clear(self) -> None:
174
+ """清屏"""
175
+ log = self.query_one("#chat-output", RichLog)
176
+ log.clear()
177
+ self._add_welcome_message()
178
+
179
+ def action_command_palette(self) -> None:
286
180
  """打开命令面板"""
287
- def handle_result(selected):
288
- if selected:
289
- self.input_widget.value = f"/{selected}"
290
- self.input_widget.focus()
181
+ def handle_result(command: str | None):
182
+ if command:
183
+ # 将选择的命令填入输入框
184
+ input_widget = self.query_one("#chat-input", Input)
185
+ input_widget.value = f"/{command}"
186
+ input_widget.focus()
291
187
 
292
- self.push_screen(CommandPalette(), handle_result)
188
+ self.app.push_screen(CommandPalette(provider=CommandProvider), handle_result)
293
189
 
294
- def action_submit_input(self):
295
- """提交输入"""
296
- if self.input_widget:
297
- self.input_widget._submit()
298
-
299
- def action_toggle_help(self):
300
- """切换帮助显示"""
301
- help_text = """
302
- 快捷键:
303
- Ctrl+G - 提交输入
304
- / - 打开命令面板
305
- Ctrl+Q - 退出应用
306
- ↑/↓ - 历史命令导航(在输入框中)
307
-
308
- 命令:
309
- /run - 运行完整工作流
310
- /flyai - FlyAI 旅行搜索
311
- /analyze - 产品可行性分析
312
- /model - 模型管理
313
- /config - 配置管理
314
- /status - 系统状态
315
- /agents - 查看 Agent 团队
316
- """
317
- self.messages.append(("system", help_text))
318
- self._update_chat_output()
190
+
191
+ class TravelAgentApp(App):
192
+ """travel-agent 交互式应用"""
193
+
194
+ TITLE = "travel-agent"
195
+ SUB_TITLE = "AI 驱动的旅行目的地推荐"
196
+
197
+ CSS = """
198
+ Screen {
199
+ background: $surface;
200
+ }
201
+
202
+ Vertical {
203
+ height: 100%;
204
+ }
205
+
206
+ #chat-output {
207
+ height: 1fr;
208
+ background: $surface;
209
+ padding: 1;
210
+ }
211
+
212
+ Input#chat-input {
213
+ height: 3;
214
+ margin: 1 1 0 1;
215
+ padding: 1;
216
+ dock: bottom;
217
+ }
218
+
219
+ Input#chat-input:focus {
220
+ background: $surface;
221
+ border: solid $primary;
222
+ }
223
+ """
224
+
225
+ BINDINGS = [
226
+ Binding("ctrl+q", "quit", "退出", show=True),
227
+ ]
228
+
229
+ def on_mount(self) -> None:
230
+ """应用启动时"""
231
+ self.push_screen(ChatScreen())
319
232
 
320
233
 
321
234
  def main():