vox-code 2.0.0__py3-none-any.whl
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.
- vox_code-2.0.0.dist-info/METADATA +258 -0
- vox_code-2.0.0.dist-info/RECORD +88 -0
- vox_code-2.0.0.dist-info/WHEEL +4 -0
- vox_code-2.0.0.dist-info/entry_points.txt +3 -0
- voxcli/__init__.py +3 -0
- voxcli/__main__.py +5 -0
- voxcli/agent/__init__.py +12 -0
- voxcli/agent/agent.py +449 -0
- voxcli/agent/agent_budget.py +133 -0
- voxcli/agent/agent_orchestrator.py +414 -0
- voxcli/agent/plan_execute_agent.py +514 -0
- voxcli/agent/roles.py +80 -0
- voxcli/agent/sub_agent.py +351 -0
- voxcli/catalog.py +477 -0
- voxcli/chat.py +91 -0
- voxcli/cli/__init__.py +4 -0
- voxcli/cli/main.py +452 -0
- voxcli/cli/parser.py +71 -0
- voxcli/config.py +518 -0
- voxcli/gui/__main__.py +3 -0
- voxcli/gui/main.py +22 -0
- voxcli/gui/pet/__init__.py +5 -0
- voxcli/gui/pet/base.py +62 -0
- voxcli/gui/pet/coordinator.py +888 -0
- voxcli/gui/pet/data.py +430 -0
- voxcli/gui/pet/widgets.py +683 -0
- voxcli/gui/pet/windows.py +2298 -0
- voxcli/gui/pet/workers.py +54 -0
- voxcli/gui/pet_app.py +7 -0
- voxcli/hitl/__init__.py +11 -0
- voxcli/hitl/handler.py +11 -0
- voxcli/hitl/policy.py +32 -0
- voxcli/hitl/request.py +13 -0
- voxcli/hitl/result.py +11 -0
- voxcli/hitl/terminal_handler.py +64 -0
- voxcli/hitl/tool_registry.py +64 -0
- voxcli/llm/base.py +93 -0
- voxcli/llm/factory.py +178 -0
- voxcli/llm/ollama_client.py +137 -0
- voxcli/llm/openai_compatible.py +249 -0
- voxcli/memory/base.py +16 -0
- voxcli/memory/budget.py +53 -0
- voxcli/memory/compressor.py +198 -0
- voxcli/memory/entry.py +36 -0
- voxcli/memory/long_term.py +126 -0
- voxcli/memory/manager.py +101 -0
- voxcli/memory/retriever.py +72 -0
- voxcli/memory/short_term.py +84 -0
- voxcli/memory/tokenizer.py +21 -0
- voxcli/plan/__init__.py +5 -0
- voxcli/plan/execution_plan.py +225 -0
- voxcli/plan/planner.py +198 -0
- voxcli/plan/task.py +123 -0
- voxcli/policy/audit_log.py +111 -0
- voxcli/policy/command_guard.py +34 -0
- voxcli/policy/exception.py +5 -0
- voxcli/policy/path_guard.py +32 -0
- voxcli/prompting/__init__.py +7 -0
- voxcli/prompting/presenter.py +154 -0
- voxcli/rag/__init__.py +16 -0
- voxcli/rag/analyzer.py +89 -0
- voxcli/rag/chunk.py +17 -0
- voxcli/rag/chunker.py +137 -0
- voxcli/rag/embedding.py +75 -0
- voxcli/rag/formatter.py +40 -0
- voxcli/rag/index.py +96 -0
- voxcli/rag/relation.py +14 -0
- voxcli/rag/retriever.py +58 -0
- voxcli/rag/store.py +155 -0
- voxcli/rag/tokenizer.py +26 -0
- voxcli/runtime/__init__.py +6 -0
- voxcli/runtime/session_controller.py +386 -0
- voxcli/tool/__init__.py +3 -0
- voxcli/tool/tool_registry.py +433 -0
- voxcli/util/animation.py +219 -0
- voxcli/util/ansi.py +82 -0
- voxcli/util/markdown.py +98 -0
- voxcli/web/__init__.py +17 -0
- voxcli/web/base.py +20 -0
- voxcli/web/extractor.py +77 -0
- voxcli/web/factory.py +38 -0
- voxcli/web/fetch_result.py +27 -0
- voxcli/web/fetcher.py +42 -0
- voxcli/web/network_policy.py +49 -0
- voxcli/web/result.py +23 -0
- voxcli/web/searxng.py +55 -0
- voxcli/web/serpapi.py +53 -0
- voxcli/web/zhipu.py +55 -0
voxcli/rag/tokenizer.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""RAG 查询分词器 - 简单中文/英文分词"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RagQueryTokenizer:
|
|
8
|
+
@staticmethod
|
|
9
|
+
def tokenize(text: str) -> List[str]:
|
|
10
|
+
text = text.lower().strip()
|
|
11
|
+
if not text:
|
|
12
|
+
return []
|
|
13
|
+
tokens = re.findall(r"[\w]+", text)
|
|
14
|
+
return tokens
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def tokenize_chinese(text: str) -> List[str]:
|
|
18
|
+
tokens = RagQueryTokenizer.tokenize(text)
|
|
19
|
+
chars = []
|
|
20
|
+
for token in tokens:
|
|
21
|
+
if re.match(r"^[a-zA-Z0-9_]+$", token):
|
|
22
|
+
chars.append(token)
|
|
23
|
+
else:
|
|
24
|
+
for ch in token:
|
|
25
|
+
chars.append(ch)
|
|
26
|
+
return chars
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""Reusable session controller for CLI and desktop shells."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import redirect_stdout
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from io import StringIO
|
|
8
|
+
import os
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from ..agent import Agent, AgentOrchestrator, PlanExecuteAgent
|
|
12
|
+
from ..chat import GuiChatSubmission
|
|
13
|
+
from ..cli.parser import CliCommandParser, ParsedCommand
|
|
14
|
+
from ..config import pai_config
|
|
15
|
+
from ..llm.factory import create, create_from_config
|
|
16
|
+
from ..memory.manager import MemoryManager
|
|
17
|
+
from ..prompting import PresentationMode, ResponsePresenter
|
|
18
|
+
from ..tool import ToolRegistry
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class SessionReply:
|
|
23
|
+
text: str
|
|
24
|
+
kind: str = "assistant"
|
|
25
|
+
raw_text: str = ""
|
|
26
|
+
mode: str = "single"
|
|
27
|
+
presentation_mode: str = PresentationMode.WORK.value
|
|
28
|
+
should_quit: bool = False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SessionController:
|
|
32
|
+
"""Stateful facade for chat-style shells."""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
llm_client=None,
|
|
37
|
+
tool_registry: Optional[ToolRegistry] = None,
|
|
38
|
+
agent: Optional[Agent] = None,
|
|
39
|
+
plan_agent: Optional[PlanExecuteAgent] = None,
|
|
40
|
+
orchestrator: Optional[AgentOrchestrator] = None,
|
|
41
|
+
presenter: Optional[ResponsePresenter] = None,
|
|
42
|
+
allow_model_switch_commands: bool = True,
|
|
43
|
+
):
|
|
44
|
+
self._llm = llm_client or create_from_config()
|
|
45
|
+
if self._llm is None:
|
|
46
|
+
raise RuntimeError(
|
|
47
|
+
"无法创建 LLM 客户端。请至少配置一组模型环境变量,例如 "
|
|
48
|
+
"GLM_API_KEY/GLM_MODEL、DEEPSEEK_API_KEY/DEEPSEEK_MODEL、"
|
|
49
|
+
"QWEN_API_KEY/QWEN_MODEL 或 "
|
|
50
|
+
"OLLAMA_MODEL/OLLAMA_BASE_URL。"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self._tool_registry = tool_registry or ToolRegistry()
|
|
54
|
+
self._shared_memory_manager = MemoryManager(self._llm)
|
|
55
|
+
self._agent = agent or Agent(self._llm, self._tool_registry)
|
|
56
|
+
self._plan_agent = plan_agent or PlanExecuteAgent(
|
|
57
|
+
self._llm, self._tool_registry, None, self._shared_memory_manager, None
|
|
58
|
+
)
|
|
59
|
+
self._orchestrator = orchestrator or AgentOrchestrator(
|
|
60
|
+
self._llm, self._tool_registry, self._shared_memory_manager
|
|
61
|
+
)
|
|
62
|
+
self._presenter = presenter or ResponsePresenter(self._llm)
|
|
63
|
+
self._parser = CliCommandParser()
|
|
64
|
+
self._allow_model_switch_commands = allow_model_switch_commands
|
|
65
|
+
self._mode = "single"
|
|
66
|
+
self._presentation_mode = self._default_presentation_mode().value
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def mode(self) -> str:
|
|
70
|
+
return self._mode
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def presentation_mode(self) -> str:
|
|
74
|
+
return self._presentation_mode
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def provider_name(self) -> str:
|
|
78
|
+
return self._llm.provider_name
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def model_name(self) -> str:
|
|
82
|
+
return self._llm.model_name
|
|
83
|
+
|
|
84
|
+
def set_mode(self, mode: str) -> SessionReply:
|
|
85
|
+
normalized = mode.strip().lower()
|
|
86
|
+
if normalized not in {"single", "plan", "team"}:
|
|
87
|
+
return self._system_reply(f"无效运行模式: {mode}", kind="error")
|
|
88
|
+
self._mode = normalized
|
|
89
|
+
return self._system_reply(f"运行模式已切换为: {self._render_mode_label(normalized)}")
|
|
90
|
+
|
|
91
|
+
def cycle_mode(self) -> SessionReply:
|
|
92
|
+
modes = ["single", "plan", "team"]
|
|
93
|
+
idx = modes.index(self._mode) if self._mode in modes else 0
|
|
94
|
+
self._mode = modes[(idx + 1) % len(modes)]
|
|
95
|
+
return self._system_reply(f"运行模式已切换为: {self._render_mode_label(self._mode)}")
|
|
96
|
+
|
|
97
|
+
def set_presentation_mode(self, mode: str) -> SessionReply:
|
|
98
|
+
if not PresentationMode.is_valid(mode):
|
|
99
|
+
return self._system_reply("无效展示模式,可选: work, pet", kind="error")
|
|
100
|
+
self._presentation_mode = PresentationMode.normalize(mode).value
|
|
101
|
+
return self._system_reply(f"展示模式已设置为: {self._presentation_mode}")
|
|
102
|
+
|
|
103
|
+
def set_llm_client(self, llm_client) -> None:
|
|
104
|
+
self._llm = llm_client
|
|
105
|
+
self._agent.set_llm_client(llm_client)
|
|
106
|
+
if not getattr(llm_client, "supports_image_inputs", False) and hasattr(
|
|
107
|
+
self._agent, "clear_attachment_context"
|
|
108
|
+
):
|
|
109
|
+
self._agent.clear_attachment_context()
|
|
110
|
+
self._plan_agent._llm = llm_client
|
|
111
|
+
if hasattr(self._plan_agent, "_planner") and self._plan_agent._planner is not None:
|
|
112
|
+
self._plan_agent._planner._llm = llm_client
|
|
113
|
+
self._plan_agent.memory_manager.set_llm_client(llm_client)
|
|
114
|
+
self._orchestrator._llm = llm_client
|
|
115
|
+
if hasattr(self._orchestrator, "_planner") and self._orchestrator._planner is not None:
|
|
116
|
+
self._orchestrator._planner._llm = llm_client
|
|
117
|
+
if hasattr(self._orchestrator, "_reviewer") and self._orchestrator._reviewer is not None:
|
|
118
|
+
self._orchestrator._reviewer._llm = llm_client
|
|
119
|
+
if hasattr(self._orchestrator, "_workers"):
|
|
120
|
+
for worker in self._orchestrator._workers:
|
|
121
|
+
worker._llm = llm_client
|
|
122
|
+
self._orchestrator.memory_manager.set_llm_client(llm_client)
|
|
123
|
+
self._presenter._llm = llm_client
|
|
124
|
+
|
|
125
|
+
def reload_default_model(self) -> SessionReply:
|
|
126
|
+
new_client = create_from_config()
|
|
127
|
+
if new_client is None:
|
|
128
|
+
return self._system_reply("切换模型失败: 无法创建当前全局配置的客户端", kind="error")
|
|
129
|
+
self.set_llm_client(new_client)
|
|
130
|
+
return self._system_reply(
|
|
131
|
+
f"已切换到全局模型: {new_client.provider_name} ({new_client.model_name})"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def set_model(self, provider: str, model_name: Optional[str] = None) -> SessionReply:
|
|
135
|
+
try:
|
|
136
|
+
new_client = create(provider, model_name)
|
|
137
|
+
if new_client is None:
|
|
138
|
+
return self._system_reply(
|
|
139
|
+
f"切换模型失败: 无法创建 provider={provider} 的客户端",
|
|
140
|
+
kind="error",
|
|
141
|
+
)
|
|
142
|
+
self.set_llm_client(new_client)
|
|
143
|
+
selected_model = getattr(new_client, "model_name", model_name or "")
|
|
144
|
+
pai_config.persist_model_selection(provider, selected_model)
|
|
145
|
+
return self._system_reply(
|
|
146
|
+
"已切换到模型: " + provider + (f" ({selected_model})" if selected_model else "")
|
|
147
|
+
)
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
return self._system_reply(f"切换模型失败: {exc}", kind="error")
|
|
150
|
+
|
|
151
|
+
def set_model_preset(self, preset_id: str) -> SessionReply:
|
|
152
|
+
preset = pai_config.get_model_preset(preset_id)
|
|
153
|
+
if preset is None:
|
|
154
|
+
return self._system_reply(f"未知模型预设: {preset_id}", kind="error")
|
|
155
|
+
return self.set_model(preset.provider, preset.model)
|
|
156
|
+
|
|
157
|
+
def submit(self, line: str | GuiChatSubmission) -> SessionReply:
|
|
158
|
+
if isinstance(line, GuiChatSubmission):
|
|
159
|
+
submission = line.normalized()
|
|
160
|
+
if submission.is_empty:
|
|
161
|
+
return self._system_reply("", kind="system")
|
|
162
|
+
|
|
163
|
+
stripped = submission.text
|
|
164
|
+
parsed = self._parser.parse(stripped) if stripped else None
|
|
165
|
+
if parsed:
|
|
166
|
+
if submission.has_attachments:
|
|
167
|
+
return self._system_reply("命令消息暂不支持携带图片,请移除附件后重试。", kind="error")
|
|
168
|
+
return self._handle_command(parsed)
|
|
169
|
+
|
|
170
|
+
if submission.has_attachments:
|
|
171
|
+
if self._mode != "single":
|
|
172
|
+
return self._system_reply("图片附件目前仅支持单聊模式,请先切换回 single。", kind="error")
|
|
173
|
+
if not getattr(self._llm, "supports_image_inputs", False):
|
|
174
|
+
return self._system_reply(
|
|
175
|
+
"当前模型不支持图片输入。请切换到支持视觉的 OpenAI Compatible 模型。",
|
|
176
|
+
kind="error",
|
|
177
|
+
)
|
|
178
|
+
return self._run_user_request(submission)
|
|
179
|
+
|
|
180
|
+
if not stripped:
|
|
181
|
+
return self._system_reply("", kind="system")
|
|
182
|
+
return self._run_user_request(stripped)
|
|
183
|
+
|
|
184
|
+
stripped = line.strip()
|
|
185
|
+
if not stripped:
|
|
186
|
+
return self._system_reply("", kind="system")
|
|
187
|
+
|
|
188
|
+
parsed = self._parser.parse(stripped)
|
|
189
|
+
if parsed:
|
|
190
|
+
return self._handle_command(parsed)
|
|
191
|
+
return self._run_user_request(stripped)
|
|
192
|
+
|
|
193
|
+
def help_text(self) -> str:
|
|
194
|
+
lines = ["可用命令:"]
|
|
195
|
+
for command, desc in sorted(CliCommandParser.COMMANDS.items()):
|
|
196
|
+
lines.append(f" {command:<10} {desc}")
|
|
197
|
+
lines.append("")
|
|
198
|
+
lines.append("桌宠窗口里也支持直接输入普通问题或任务。")
|
|
199
|
+
return "\n".join(lines)
|
|
200
|
+
|
|
201
|
+
def _handle_command(self, parsed: ParsedCommand) -> SessionReply:
|
|
202
|
+
cmd = parsed.command
|
|
203
|
+
|
|
204
|
+
if cmd == "/help":
|
|
205
|
+
return self._system_reply(self.help_text())
|
|
206
|
+
|
|
207
|
+
if cmd == "/exit":
|
|
208
|
+
return SessionReply(
|
|
209
|
+
text="再见!",
|
|
210
|
+
kind="system",
|
|
211
|
+
mode=self._mode,
|
|
212
|
+
presentation_mode=self._presentation_mode,
|
|
213
|
+
should_quit=True,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if cmd == "/team":
|
|
217
|
+
return self.cycle_mode()
|
|
218
|
+
|
|
219
|
+
if cmd == "/init":
|
|
220
|
+
return self._system_reply(
|
|
221
|
+
"请在终端里运行 vox-code init 来完成首次配置。",
|
|
222
|
+
kind="error",
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if cmd == "/style":
|
|
226
|
+
if not parsed.args:
|
|
227
|
+
return self._system_reply(f"当前展示模式: {self._presentation_mode}")
|
|
228
|
+
return self.set_presentation_mode(parsed.args[0])
|
|
229
|
+
|
|
230
|
+
if cmd == "/model":
|
|
231
|
+
if not self._allow_model_switch_commands:
|
|
232
|
+
return self._system_reply(
|
|
233
|
+
"桌宠 GUI 请在“模型设置”窗口中修改模型。",
|
|
234
|
+
kind="error",
|
|
235
|
+
)
|
|
236
|
+
if not parsed.args:
|
|
237
|
+
return self._system_reply("用法: /model <preset-id|provider[:model]>", kind="error")
|
|
238
|
+
provider, model_name, preset = pai_config.resolve_model_selection(parsed.args[0])
|
|
239
|
+
if preset is not None:
|
|
240
|
+
return self.set_model_preset(preset.id)
|
|
241
|
+
return self.set_model(provider, model_name)
|
|
242
|
+
|
|
243
|
+
if cmd == "/plan":
|
|
244
|
+
return self._system_reply(
|
|
245
|
+
"Plan-and-Execute 模式会先生成计划再执行。"
|
|
246
|
+
f"\n当前运行模式: {self._render_mode_label(self._mode)}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if cmd == "/memory":
|
|
250
|
+
return self._system_reply(self._active_memory_manager().status_summary())
|
|
251
|
+
|
|
252
|
+
if cmd == "/clear":
|
|
253
|
+
self._agent.clear_history()
|
|
254
|
+
self._shared_memory_manager.clear_short_term()
|
|
255
|
+
return self._system_reply("对话历史已清空")
|
|
256
|
+
|
|
257
|
+
if cmd == "/context":
|
|
258
|
+
if self._mode == "single":
|
|
259
|
+
return self._system_reply(self._agent.get_context_status())
|
|
260
|
+
return self._system_reply(
|
|
261
|
+
f"当前运行模式: {self._render_mode_label(self._mode)}\n"
|
|
262
|
+
f"{self._active_memory_manager().status_summary()}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if cmd == "/policy":
|
|
266
|
+
return self._system_reply(
|
|
267
|
+
"安全策略\n"
|
|
268
|
+
f"- 项目根目录: {self._tool_registry.project_path}\n"
|
|
269
|
+
"- PathGuard: 限制到项目根目录\n"
|
|
270
|
+
"- CommandGuard: 禁止危险命令\n"
|
|
271
|
+
"- 审计日志: ~/.vox-code/audit/"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if cmd == "/audit":
|
|
275
|
+
entries = self._tool_registry.audit_log.recent(10)
|
|
276
|
+
if not entries:
|
|
277
|
+
return self._system_reply("暂无审计记录")
|
|
278
|
+
lines = ["最近审计记录:"]
|
|
279
|
+
for entry in entries:
|
|
280
|
+
lines.append(
|
|
281
|
+
f"- {entry.tool} [{entry.outcome}] "
|
|
282
|
+
f"{entry.reason or '无原因'}"
|
|
283
|
+
)
|
|
284
|
+
return self._system_reply("\n".join(lines))
|
|
285
|
+
|
|
286
|
+
if cmd == "/index":
|
|
287
|
+
try:
|
|
288
|
+
from ..rag.index import CodeIndex
|
|
289
|
+
|
|
290
|
+
indexer = CodeIndex(self._tool_registry.project_path)
|
|
291
|
+
count = indexer.index_project()
|
|
292
|
+
return self._system_reply(f"索引完成,共 {count} 个代码块")
|
|
293
|
+
except ImportError:
|
|
294
|
+
return self._system_reply("索引功能不可用(缺少 RAG 模块依赖)", kind="error")
|
|
295
|
+
except Exception as exc:
|
|
296
|
+
return self._system_reply(f"索引失败: {exc}", kind="error")
|
|
297
|
+
|
|
298
|
+
if cmd == "/search":
|
|
299
|
+
query = " ".join(parsed.args).strip()
|
|
300
|
+
if not query:
|
|
301
|
+
return self._system_reply("用法: /search <query>", kind="error")
|
|
302
|
+
try:
|
|
303
|
+
return self._system_reply(self._tool_registry._search_code(query, 5))
|
|
304
|
+
except Exception as exc:
|
|
305
|
+
return self._system_reply(f"搜索失败: {exc}", kind="error")
|
|
306
|
+
|
|
307
|
+
if cmd == "/save":
|
|
308
|
+
filename = " ".join(parsed.args).strip() or "conversation.md"
|
|
309
|
+
try:
|
|
310
|
+
history = self._agent.conversation_history
|
|
311
|
+
lines = []
|
|
312
|
+
for message in history:
|
|
313
|
+
parts = [message.content or ""]
|
|
314
|
+
if message.attachments:
|
|
315
|
+
parts.extend(
|
|
316
|
+
f"[image] {attachment.display_name} ({attachment.file_path})"
|
|
317
|
+
for attachment in message.attachments
|
|
318
|
+
)
|
|
319
|
+
lines.append(f"## {message.role.upper()}\n" + "\n".join(part for part in parts if part) + "\n")
|
|
320
|
+
with open(filename, "w", encoding="utf-8") as handle:
|
|
321
|
+
handle.write("\n".join(lines))
|
|
322
|
+
return self._system_reply(f"对话已保存到: {filename}")
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
return self._system_reply(f"保存失败: {exc}", kind="error")
|
|
325
|
+
|
|
326
|
+
if cmd == "/hitl":
|
|
327
|
+
return self._system_reply("桌宠版 MVP 暂未接入 HITL 面板,请继续使用 CLI 处理审批。")
|
|
328
|
+
|
|
329
|
+
return self._system_reply(f"未知命令: {cmd}", kind="error")
|
|
330
|
+
|
|
331
|
+
def _run_user_request(self, user_input: str | GuiChatSubmission) -> SessionReply:
|
|
332
|
+
executor = self._active_executor()
|
|
333
|
+
stdout_buffer = StringIO()
|
|
334
|
+
source_input = user_input.summary_text if isinstance(user_input, GuiChatSubmission) else user_input
|
|
335
|
+
try:
|
|
336
|
+
with redirect_stdout(stdout_buffer):
|
|
337
|
+
raw_result = executor.run(user_input)
|
|
338
|
+
except Exception as exc:
|
|
339
|
+
return self._system_reply(f"执行失败: {exc}", kind="error")
|
|
340
|
+
|
|
341
|
+
captured = stdout_buffer.getvalue().strip()
|
|
342
|
+
source = (raw_result or "").strip() or captured
|
|
343
|
+
if not source:
|
|
344
|
+
source = "任务执行完成,但模型没有返回可显示内容。"
|
|
345
|
+
|
|
346
|
+
presented = self._presenter.present(source_input, source, self._presentation_mode)
|
|
347
|
+
return SessionReply(
|
|
348
|
+
text=presented.display_response or source,
|
|
349
|
+
raw_text=source,
|
|
350
|
+
kind="assistant",
|
|
351
|
+
mode=self._mode,
|
|
352
|
+
presentation_mode=self._presentation_mode,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
def _active_executor(self):
|
|
356
|
+
if self._mode == "plan":
|
|
357
|
+
return self._plan_agent
|
|
358
|
+
if self._mode == "team":
|
|
359
|
+
return self._orchestrator
|
|
360
|
+
return self._agent
|
|
361
|
+
|
|
362
|
+
def _active_memory_manager(self) -> MemoryManager:
|
|
363
|
+
if self._mode == "single":
|
|
364
|
+
return self._agent.memory_manager
|
|
365
|
+
return self._shared_memory_manager
|
|
366
|
+
|
|
367
|
+
def _system_reply(self, text: str, kind: str = "system") -> SessionReply:
|
|
368
|
+
return SessionReply(
|
|
369
|
+
text=text,
|
|
370
|
+
kind=kind,
|
|
371
|
+
raw_text=text,
|
|
372
|
+
mode=self._mode,
|
|
373
|
+
presentation_mode=self._presentation_mode,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def _render_mode_label(mode: str) -> str:
|
|
378
|
+
if mode == "plan":
|
|
379
|
+
return "Plan-and-Execute"
|
|
380
|
+
if mode == "team":
|
|
381
|
+
return "多 Agent 团队"
|
|
382
|
+
return "单 Agent"
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def _default_presentation_mode() -> PresentationMode:
|
|
386
|
+
return PresentationMode.normalize(os.environ.get("VOX_CODE_PRESENTATION_MODE", "work"))
|
voxcli/tool/__init__.py
ADDED