travel-agent-cli 0.3.2 → 0.3.4
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/package.json +1 -1
- package/python/interactive.py +196 -238
package/package.json
CHANGED
package/python/interactive.py
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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("
|
|
57
|
-
Binding("
|
|
58
|
-
Binding("
|
|
59
|
-
Binding("
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
141
|
+
if result:
|
|
142
|
+
log.write(Panel(result, title="结果", border_style="green"))
|
|
255
143
|
|
|
256
144
|
except Exception as e:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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,127 @@ class TravelAgentApp(App):
|
|
|
264
153
|
cmd_name = parts[0]
|
|
265
154
|
args = parts[1] if len(parts) > 1 else ""
|
|
266
155
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
276
|
-
|
|
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
|
|
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(
|
|
288
|
-
if
|
|
289
|
-
|
|
290
|
-
self.
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
190
|
+
|
|
191
|
+
class TravelAgentApp(App):
|
|
192
|
+
"""travel-agent 交互式应用"""
|
|
193
|
+
|
|
194
|
+
TITLE = "travel-agent"
|
|
195
|
+
SUB_TITLE = "AI 驱动的旅行目的地推荐"
|
|
196
|
+
|
|
197
|
+
CSS = """
|
|
198
|
+
Screen {
|
|
199
|
+
background: #1a1a2e;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
Vertical {
|
|
203
|
+
height: 100%;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
#chat-output {
|
|
207
|
+
height: 1fr;
|
|
208
|
+
background: #1a1a2e;
|
|
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
|
+
background: #16213e;
|
|
218
|
+
border: solid #0f3460;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Input#chat-input:focus {
|
|
222
|
+
background: #16213e;
|
|
223
|
+
border: solid #e94560;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Command Palette Styles */
|
|
227
|
+
CommandPalette {
|
|
228
|
+
background: rgba(26, 26, 46, 0.95);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
CommandPalette > Vertical {
|
|
232
|
+
background: #1a1a2e;
|
|
233
|
+
border: solid #0f3460;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
CommandPalette Input {
|
|
237
|
+
background: #16213e;
|
|
238
|
+
color: #eaeaea;
|
|
239
|
+
border: solid #0f3460;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
CommandPalette Input:focus {
|
|
243
|
+
background: #16213e;
|
|
244
|
+
border: solid #e94560;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
CommandPalette .command-palette--help-text {
|
|
248
|
+
color: #6c6c8a;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
CommandPalette .command-palette--highlight {
|
|
252
|
+
text-style: bold;
|
|
253
|
+
color: #e94560;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
CommandPalette Static {
|
|
257
|
+
color: #eaeaea;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
CommandPalette .option-list__option {
|
|
261
|
+
color: #eaeaea;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
CommandPalette .option-list__option--highlighted {
|
|
265
|
+
background: #0f3460;
|
|
266
|
+
color: #ffffff;
|
|
267
|
+
}
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
BINDINGS = [
|
|
271
|
+
Binding("ctrl+q", "quit", "退出", show=True),
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
def on_mount(self) -> None:
|
|
275
|
+
"""应用启动时"""
|
|
276
|
+
self.push_screen(ChatScreen())
|
|
319
277
|
|
|
320
278
|
|
|
321
279
|
def main():
|