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.
Files changed (88) hide show
  1. vox_code-2.0.0.dist-info/METADATA +258 -0
  2. vox_code-2.0.0.dist-info/RECORD +88 -0
  3. vox_code-2.0.0.dist-info/WHEEL +4 -0
  4. vox_code-2.0.0.dist-info/entry_points.txt +3 -0
  5. voxcli/__init__.py +3 -0
  6. voxcli/__main__.py +5 -0
  7. voxcli/agent/__init__.py +12 -0
  8. voxcli/agent/agent.py +449 -0
  9. voxcli/agent/agent_budget.py +133 -0
  10. voxcli/agent/agent_orchestrator.py +414 -0
  11. voxcli/agent/plan_execute_agent.py +514 -0
  12. voxcli/agent/roles.py +80 -0
  13. voxcli/agent/sub_agent.py +351 -0
  14. voxcli/catalog.py +477 -0
  15. voxcli/chat.py +91 -0
  16. voxcli/cli/__init__.py +4 -0
  17. voxcli/cli/main.py +452 -0
  18. voxcli/cli/parser.py +71 -0
  19. voxcli/config.py +518 -0
  20. voxcli/gui/__main__.py +3 -0
  21. voxcli/gui/main.py +22 -0
  22. voxcli/gui/pet/__init__.py +5 -0
  23. voxcli/gui/pet/base.py +62 -0
  24. voxcli/gui/pet/coordinator.py +888 -0
  25. voxcli/gui/pet/data.py +430 -0
  26. voxcli/gui/pet/widgets.py +683 -0
  27. voxcli/gui/pet/windows.py +2298 -0
  28. voxcli/gui/pet/workers.py +54 -0
  29. voxcli/gui/pet_app.py +7 -0
  30. voxcli/hitl/__init__.py +11 -0
  31. voxcli/hitl/handler.py +11 -0
  32. voxcli/hitl/policy.py +32 -0
  33. voxcli/hitl/request.py +13 -0
  34. voxcli/hitl/result.py +11 -0
  35. voxcli/hitl/terminal_handler.py +64 -0
  36. voxcli/hitl/tool_registry.py +64 -0
  37. voxcli/llm/base.py +93 -0
  38. voxcli/llm/factory.py +178 -0
  39. voxcli/llm/ollama_client.py +137 -0
  40. voxcli/llm/openai_compatible.py +249 -0
  41. voxcli/memory/base.py +16 -0
  42. voxcli/memory/budget.py +53 -0
  43. voxcli/memory/compressor.py +198 -0
  44. voxcli/memory/entry.py +36 -0
  45. voxcli/memory/long_term.py +126 -0
  46. voxcli/memory/manager.py +101 -0
  47. voxcli/memory/retriever.py +72 -0
  48. voxcli/memory/short_term.py +84 -0
  49. voxcli/memory/tokenizer.py +21 -0
  50. voxcli/plan/__init__.py +5 -0
  51. voxcli/plan/execution_plan.py +225 -0
  52. voxcli/plan/planner.py +198 -0
  53. voxcli/plan/task.py +123 -0
  54. voxcli/policy/audit_log.py +111 -0
  55. voxcli/policy/command_guard.py +34 -0
  56. voxcli/policy/exception.py +5 -0
  57. voxcli/policy/path_guard.py +32 -0
  58. voxcli/prompting/__init__.py +7 -0
  59. voxcli/prompting/presenter.py +154 -0
  60. voxcli/rag/__init__.py +16 -0
  61. voxcli/rag/analyzer.py +89 -0
  62. voxcli/rag/chunk.py +17 -0
  63. voxcli/rag/chunker.py +137 -0
  64. voxcli/rag/embedding.py +75 -0
  65. voxcli/rag/formatter.py +40 -0
  66. voxcli/rag/index.py +96 -0
  67. voxcli/rag/relation.py +14 -0
  68. voxcli/rag/retriever.py +58 -0
  69. voxcli/rag/store.py +155 -0
  70. voxcli/rag/tokenizer.py +26 -0
  71. voxcli/runtime/__init__.py +6 -0
  72. voxcli/runtime/session_controller.py +386 -0
  73. voxcli/tool/__init__.py +3 -0
  74. voxcli/tool/tool_registry.py +433 -0
  75. voxcli/util/animation.py +219 -0
  76. voxcli/util/ansi.py +82 -0
  77. voxcli/util/markdown.py +98 -0
  78. voxcli/web/__init__.py +17 -0
  79. voxcli/web/base.py +20 -0
  80. voxcli/web/extractor.py +77 -0
  81. voxcli/web/factory.py +38 -0
  82. voxcli/web/fetch_result.py +27 -0
  83. voxcli/web/fetcher.py +42 -0
  84. voxcli/web/network_policy.py +49 -0
  85. voxcli/web/result.py +23 -0
  86. voxcli/web/searxng.py +55 -0
  87. voxcli/web/serpapi.py +53 -0
  88. voxcli/web/zhipu.py +55 -0
@@ -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,6 @@
1
+ from .session_controller import SessionController, SessionReply
2
+
3
+ __all__ = [
4
+ "SessionController",
5
+ "SessionReply",
6
+ ]
@@ -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"))
@@ -0,0 +1,3 @@
1
+ from .tool_registry import ToolRegistry, ToolDef, ToolInvocation, ToolExecutionResult
2
+
3
+ __all__ = ["ToolRegistry", "ToolDef", "ToolInvocation", "ToolExecutionResult"]