jarvis-ai-assistant 0.2.8__py3-none-any.whl → 0.3.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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +267 -240
- jarvis/jarvis_agent/agent_manager.py +85 -0
- jarvis/jarvis_agent/config_editor.py +53 -0
- jarvis/jarvis_agent/file_methodology_manager.py +105 -0
- jarvis/jarvis_agent/jarvis.py +30 -619
- jarvis/jarvis_agent/memory_manager.py +133 -0
- jarvis/jarvis_agent/methodology_share_manager.py +174 -0
- jarvis/jarvis_agent/prompts.py +18 -3
- jarvis/jarvis_agent/share_manager.py +176 -0
- jarvis/jarvis_agent/task_analyzer.py +126 -0
- jarvis/jarvis_agent/task_manager.py +111 -0
- jarvis/jarvis_agent/tool_share_manager.py +139 -0
- jarvis/jarvis_code_agent/code_agent.py +26 -20
- jarvis/jarvis_data/config_schema.json +32 -0
- jarvis/jarvis_platform/ai8.py +13 -1
- jarvis/jarvis_platform/base.py +20 -5
- jarvis/jarvis_platform/human.py +11 -1
- jarvis/jarvis_platform/kimi.py +10 -0
- jarvis/jarvis_platform/openai.py +20 -0
- jarvis/jarvis_platform/tongyi.py +14 -9
- jarvis/jarvis_platform/yuanbao.py +10 -0
- jarvis/jarvis_platform_manager/main.py +12 -12
- jarvis/jarvis_tools/registry.py +32 -0
- jarvis/jarvis_tools/retrieve_memory.py +36 -8
- jarvis/jarvis_utils/clipboard.py +90 -0
- jarvis/jarvis_utils/config.py +54 -0
- jarvis/jarvis_utils/git_utils.py +17 -7
- jarvis/jarvis_utils/globals.py +18 -12
- jarvis/jarvis_utils/input.py +118 -16
- jarvis/jarvis_utils/methodology.py +48 -5
- jarvis/jarvis_utils/utils.py +169 -105
- {jarvis_ai_assistant-0.2.8.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/METADATA +1 -1
- {jarvis_ai_assistant-0.2.8.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/RECORD +38 -28
- {jarvis_ai_assistant-0.2.8.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.2.8.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.2.8.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.2.8.dist-info → jarvis_ai_assistant-0.3.0.dist-info}/top_level.txt +0 -0
jarvis/jarvis_agent/jarvis.py
CHANGED
@@ -1,624 +1,19 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
|
-
|
3
|
-
import
|
4
|
-
import subprocess
|
5
|
-
import sys
|
6
|
-
from pathlib import Path
|
7
|
-
from typing import Dict, Optional, List
|
2
|
+
"""Jarvis AI 助手主入口模块"""
|
3
|
+
from typing import Optional
|
8
4
|
|
9
5
|
import typer
|
10
|
-
import yaml # type: ignore
|
11
|
-
from prompt_toolkit import prompt # type: ignore
|
12
6
|
|
13
|
-
from jarvis.jarvis_agent import
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
origin_agent_system_prompt,
|
19
|
-
user_confirm,
|
20
|
-
)
|
21
|
-
from jarvis.jarvis_agent.builtin_input_handler import builtin_input_handler
|
22
|
-
from jarvis.jarvis_agent.shell_input_handler import shell_input_handler
|
23
|
-
from jarvis.jarvis_tools.registry import ToolRegistry
|
24
|
-
from jarvis.jarvis_utils.config import get_data_dir
|
7
|
+
from jarvis.jarvis_agent import OutputType, PrettyOutput
|
8
|
+
from jarvis.jarvis_agent.agent_manager import AgentManager
|
9
|
+
from jarvis.jarvis_agent.config_editor import ConfigEditor
|
10
|
+
from jarvis.jarvis_agent.methodology_share_manager import MethodologyShareManager
|
11
|
+
from jarvis.jarvis_agent.tool_share_manager import ToolShareManager
|
25
12
|
from jarvis.jarvis_utils.utils import init_env
|
26
13
|
|
27
14
|
app = typer.Typer(help="Jarvis AI 助手")
|
28
15
|
|
29
16
|
|
30
|
-
def _load_tasks() -> Dict[str, str]:
|
31
|
-
"""Load tasks from .jarvis files in user home and current directory."""
|
32
|
-
tasks: Dict[str, str] = {}
|
33
|
-
|
34
|
-
# Check pre-command in data directory
|
35
|
-
data_dir = get_data_dir()
|
36
|
-
pre_command_path = os.path.join(data_dir, "pre-command")
|
37
|
-
if os.path.exists(pre_command_path):
|
38
|
-
print(f"🔍 从{pre_command_path}加载预定义任务...")
|
39
|
-
try:
|
40
|
-
with open(pre_command_path, "r", encoding="utf-8", errors="ignore") as f:
|
41
|
-
user_tasks = yaml.safe_load(f)
|
42
|
-
if isinstance(user_tasks, dict):
|
43
|
-
for name, desc in user_tasks.items():
|
44
|
-
if desc:
|
45
|
-
tasks[str(name)] = str(desc)
|
46
|
-
print(f"✅ 预定义任务加载完成 {pre_command_path}")
|
47
|
-
except (yaml.YAMLError, OSError):
|
48
|
-
print(f"❌ 预定义任务加载失败 {pre_command_path}")
|
49
|
-
|
50
|
-
# Check .jarvis/pre-command in current directory
|
51
|
-
pre_command_path = ".jarvis/pre-command"
|
52
|
-
if os.path.exists(pre_command_path):
|
53
|
-
abs_path = os.path.abspath(pre_command_path)
|
54
|
-
print(f"🔍 从{abs_path}加载预定义任务...")
|
55
|
-
try:
|
56
|
-
with open(pre_command_path, "r", encoding="utf-8", errors="ignore") as f:
|
57
|
-
local_tasks = yaml.safe_load(f)
|
58
|
-
if isinstance(local_tasks, dict):
|
59
|
-
for name, desc in local_tasks.items():
|
60
|
-
if desc:
|
61
|
-
tasks[str(name)] = str(desc)
|
62
|
-
print(f"✅ 预定义任务加载完成 {pre_command_path}")
|
63
|
-
except (yaml.YAMLError, OSError):
|
64
|
-
print(f"❌ 预定义任务加载失败 {pre_command_path}")
|
65
|
-
|
66
|
-
return tasks
|
67
|
-
|
68
|
-
|
69
|
-
def _select_task(tasks: Dict[str, str]) -> str:
|
70
|
-
"""Let user select a task from the list or skip. Returns task description if selected."""
|
71
|
-
if not tasks:
|
72
|
-
return ""
|
73
|
-
|
74
|
-
task_names = list(tasks.keys())
|
75
|
-
task_list = ["可用任务:"]
|
76
|
-
for i, name in enumerate(task_names, 1):
|
77
|
-
task_list.append(f"[{i}] {name}")
|
78
|
-
task_list.append("[0] 跳过预定义任务")
|
79
|
-
PrettyOutput.print("\n".join(task_list), OutputType.INFO)
|
80
|
-
|
81
|
-
while True:
|
82
|
-
try:
|
83
|
-
choice_str = prompt("\n请选择一个任务编号(0 跳过预定义任务):").strip()
|
84
|
-
if not choice_str:
|
85
|
-
return ""
|
86
|
-
|
87
|
-
choice = int(choice_str)
|
88
|
-
if choice == 0:
|
89
|
-
return ""
|
90
|
-
if 1 <= choice <= len(task_names):
|
91
|
-
selected_task = tasks[task_names[choice - 1]]
|
92
|
-
PrettyOutput.print(f"将要执行任务:\n {selected_task}", OutputType.INFO)
|
93
|
-
# 询问是否需要补充信息
|
94
|
-
need_additional = user_confirm(
|
95
|
-
"需要为此任务添加补充信息吗?", default=False
|
96
|
-
)
|
97
|
-
if need_additional:
|
98
|
-
additional_input = get_multiline_input("请输入补充信息:")
|
99
|
-
if additional_input:
|
100
|
-
selected_task = (
|
101
|
-
f"{selected_task}\n\n补充信息:\n{additional_input}"
|
102
|
-
)
|
103
|
-
return selected_task
|
104
|
-
PrettyOutput.print(
|
105
|
-
"无效的选择。请选择列表中的一个号码。", OutputType.WARNING
|
106
|
-
)
|
107
|
-
|
108
|
-
except (KeyboardInterrupt, EOFError):
|
109
|
-
return ""
|
110
|
-
except ValueError as val_err:
|
111
|
-
PrettyOutput.print(f"选择任务失败: {str(val_err)}", OutputType.ERROR)
|
112
|
-
|
113
|
-
|
114
|
-
def _handle_edit_mode(edit: bool, config_file: Optional[str]) -> None:
|
115
|
-
"""If edit flag is set, open config file in editor and exit."""
|
116
|
-
if not edit:
|
117
|
-
return
|
118
|
-
|
119
|
-
config_file_path = (
|
120
|
-
Path(config_file)
|
121
|
-
if config_file
|
122
|
-
else Path(os.path.expanduser("~/.jarvis/config.yaml"))
|
123
|
-
)
|
124
|
-
# 根据操作系统选择合适的编辑器
|
125
|
-
import platform
|
126
|
-
|
127
|
-
if platform.system() == "Windows":
|
128
|
-
# 优先级:终端工具 -> 代码编辑器 -> 通用文本编辑器
|
129
|
-
editors = ["nvim", "vim", "nano", "code", "notepad++", "notepad"]
|
130
|
-
else:
|
131
|
-
# 优先级:终端工具 -> 代码编辑器 -> 通用文本编辑器
|
132
|
-
editors = ["nvim", "vim", "vi", "nano", "emacs", "code", "gedit", "kate"]
|
133
|
-
|
134
|
-
editor = next((e for e in editors if shutil.which(e)), None)
|
135
|
-
|
136
|
-
if editor:
|
137
|
-
try:
|
138
|
-
subprocess.run([editor, str(config_file_path)], check=True)
|
139
|
-
raise typer.Exit(code=0)
|
140
|
-
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
141
|
-
PrettyOutput.print(f"Failed to open editor: {e}", OutputType.ERROR)
|
142
|
-
raise typer.Exit(code=1)
|
143
|
-
else:
|
144
|
-
PrettyOutput.print(
|
145
|
-
f"No suitable editor found. Tried: {', '.join(editors)}", OutputType.ERROR
|
146
|
-
)
|
147
|
-
raise typer.Exit(code=1)
|
148
|
-
|
149
|
-
|
150
|
-
def _initialize_agent(
|
151
|
-
llm_type: str, model_group: Optional[str], restore_session: bool
|
152
|
-
) -> Agent:
|
153
|
-
"""Initialize the agent and restore session if requested."""
|
154
|
-
agent = Agent(
|
155
|
-
system_prompt=origin_agent_system_prompt,
|
156
|
-
llm_type=llm_type,
|
157
|
-
model_group=model_group,
|
158
|
-
input_handler=[shell_input_handler, builtin_input_handler],
|
159
|
-
output_handler=[ToolRegistry()], # type: ignore
|
160
|
-
need_summary=False,
|
161
|
-
)
|
162
|
-
|
163
|
-
# 尝试恢复会话
|
164
|
-
if restore_session:
|
165
|
-
if agent.restore_session():
|
166
|
-
PrettyOutput.print("会话已成功恢复。", OutputType.SUCCESS)
|
167
|
-
else:
|
168
|
-
PrettyOutput.print("无法恢复会话。", OutputType.WARNING)
|
169
|
-
return agent
|
170
|
-
|
171
|
-
|
172
|
-
def _get_and_run_task(agent: Agent, task_content: Optional[str] = None) -> None:
|
173
|
-
"""Get task from various sources and run it."""
|
174
|
-
# 优先处理命令行直接传入的任务
|
175
|
-
if task_content:
|
176
|
-
agent.run(task_content)
|
177
|
-
raise typer.Exit(code=0)
|
178
|
-
|
179
|
-
if agent.first:
|
180
|
-
tasks = _load_tasks()
|
181
|
-
if tasks and (selected_task := _select_task(tasks)):
|
182
|
-
PrettyOutput.print(f"开始执行任务: \n{selected_task}", OutputType.INFO)
|
183
|
-
agent.run(selected_task)
|
184
|
-
raise typer.Exit(code=0)
|
185
|
-
|
186
|
-
user_input = get_multiline_input("请输入你的任务(输入空行退出):")
|
187
|
-
if user_input:
|
188
|
-
agent.run(user_input)
|
189
|
-
raise typer.Exit(code=0)
|
190
|
-
|
191
|
-
|
192
|
-
def _parse_selection(selection_str: str, max_value: int) -> List[int]:
|
193
|
-
"""解析用户输入的选择字符串,支持逗号分隔和范围选择
|
194
|
-
|
195
|
-
例如: "1,2,3,4-9,20" -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 20]
|
196
|
-
"""
|
197
|
-
selected: set[int] = set()
|
198
|
-
parts = selection_str.split(",")
|
199
|
-
|
200
|
-
for part in parts:
|
201
|
-
part = part.strip()
|
202
|
-
if "-" in part:
|
203
|
-
# 处理范围选择
|
204
|
-
try:
|
205
|
-
start_str, end_str = part.split("-")
|
206
|
-
start_num = int(start_str.strip())
|
207
|
-
end_num = int(end_str.strip())
|
208
|
-
if 1 <= start_num <= max_value and 1 <= end_num <= max_value:
|
209
|
-
selected.update(range(start_num, end_num + 1))
|
210
|
-
except ValueError:
|
211
|
-
continue
|
212
|
-
else:
|
213
|
-
# 处理单个数字
|
214
|
-
try:
|
215
|
-
num = int(part)
|
216
|
-
if 1 <= num <= max_value:
|
217
|
-
selected.add(num)
|
218
|
-
except ValueError:
|
219
|
-
continue
|
220
|
-
|
221
|
-
return sorted(list(selected))
|
222
|
-
|
223
|
-
|
224
|
-
def _handle_share_tool(config_file: Optional[str] = None) -> None:
|
225
|
-
"""处理工具分享功能"""
|
226
|
-
from jarvis.jarvis_utils.config import (
|
227
|
-
get_central_tool_repo,
|
228
|
-
get_data_dir,
|
229
|
-
)
|
230
|
-
import glob
|
231
|
-
import shutil
|
232
|
-
|
233
|
-
# 获取中心工具仓库配置
|
234
|
-
central_repo = get_central_tool_repo()
|
235
|
-
if not central_repo:
|
236
|
-
PrettyOutput.print(
|
237
|
-
"错误:未配置中心工具仓库(JARVIS_CENTRAL_TOOL_REPO)",
|
238
|
-
OutputType.ERROR,
|
239
|
-
)
|
240
|
-
PrettyOutput.print("请在配置文件中设置中心工具仓库的Git地址", OutputType.INFO)
|
241
|
-
raise typer.Exit(code=1)
|
242
|
-
|
243
|
-
# 克隆或更新中心工具仓库
|
244
|
-
central_repo_path = os.path.join(get_data_dir(), "central_tool_repo")
|
245
|
-
if not os.path.exists(central_repo_path):
|
246
|
-
PrettyOutput.print(f"正在克隆中心工具仓库...", OutputType.INFO)
|
247
|
-
subprocess.run(["git", "clone", central_repo, central_repo_path], check=True)
|
248
|
-
else:
|
249
|
-
PrettyOutput.print(f"正在更新中心工具仓库...", OutputType.INFO)
|
250
|
-
# 检查是否是空仓库
|
251
|
-
try:
|
252
|
-
# 先尝试获取远程分支信息
|
253
|
-
result = subprocess.run(
|
254
|
-
["git", "ls-remote", "--heads", "origin"],
|
255
|
-
cwd=central_repo_path,
|
256
|
-
capture_output=True,
|
257
|
-
text=True,
|
258
|
-
check=True,
|
259
|
-
)
|
260
|
-
# 如果有远程分支,执行pull
|
261
|
-
if result.stdout.strip():
|
262
|
-
subprocess.run(["git", "pull"], cwd=central_repo_path, check=True)
|
263
|
-
else:
|
264
|
-
PrettyOutput.print(
|
265
|
-
"中心工具仓库是空的,将初始化为新仓库", OutputType.INFO
|
266
|
-
)
|
267
|
-
except subprocess.CalledProcessError:
|
268
|
-
# 如果命令失败,可能是网络问题或其他错误
|
269
|
-
PrettyOutput.print("无法连接到远程仓库,将跳过更新", OutputType.WARNING)
|
270
|
-
|
271
|
-
# 获取中心仓库中已有的工具文件名
|
272
|
-
existing_tools = set()
|
273
|
-
for filepath in glob.glob(os.path.join(central_repo_path, "*.py")):
|
274
|
-
existing_tools.add(os.path.basename(filepath))
|
275
|
-
|
276
|
-
# 只从数据目录的tools目录获取工具
|
277
|
-
local_tools_dir = os.path.join(get_data_dir(), "tools")
|
278
|
-
if not os.path.exists(local_tools_dir):
|
279
|
-
PrettyOutput.print(
|
280
|
-
f"本地工具目录不存在: {local_tools_dir}",
|
281
|
-
OutputType.WARNING,
|
282
|
-
)
|
283
|
-
raise typer.Exit(code=0)
|
284
|
-
|
285
|
-
# 收集本地工具文件(排除已存在的)
|
286
|
-
tool_files = []
|
287
|
-
for filepath in glob.glob(os.path.join(local_tools_dir, "*.py")):
|
288
|
-
filename = os.path.basename(filepath)
|
289
|
-
# 跳过__init__.py和已存在的文件
|
290
|
-
if filename == "__init__.py" or filename in existing_tools:
|
291
|
-
continue
|
292
|
-
|
293
|
-
# 尝试获取工具名称(通过简单解析)
|
294
|
-
tool_name = filename[:-3] # 移除.py后缀
|
295
|
-
tool_files.append(
|
296
|
-
{
|
297
|
-
"path": filepath,
|
298
|
-
"filename": filename,
|
299
|
-
"tool_name": tool_name,
|
300
|
-
}
|
301
|
-
)
|
302
|
-
|
303
|
-
if not tool_files:
|
304
|
-
PrettyOutput.print(
|
305
|
-
"没有找到新的工具文件(所有工具可能已存在于中心仓库)",
|
306
|
-
OutputType.WARNING,
|
307
|
-
)
|
308
|
-
raise typer.Exit(code=0)
|
309
|
-
|
310
|
-
# 显示可选的工具
|
311
|
-
tool_list = ["\n可分享的工具(已排除中心仓库中已有的):"]
|
312
|
-
for i, tool in enumerate(tool_files, 1):
|
313
|
-
tool_list.append(f"[{i}] {tool['tool_name']} ({tool['filename']})")
|
314
|
-
|
315
|
-
# 一次性打印所有工具
|
316
|
-
PrettyOutput.print("\n".join(tool_list), OutputType.INFO)
|
317
|
-
|
318
|
-
# 让用户选择要分享的工具
|
319
|
-
while True:
|
320
|
-
try:
|
321
|
-
choice_str = prompt(
|
322
|
-
"\n请选择要分享的工具编号(支持格式: 1,2,3,4-9,20 或 all):"
|
323
|
-
).strip()
|
324
|
-
if choice_str == "0":
|
325
|
-
raise typer.Exit(code=0)
|
326
|
-
|
327
|
-
selected_tools = []
|
328
|
-
if choice_str.lower() == "all":
|
329
|
-
selected_tools = tool_files
|
330
|
-
else:
|
331
|
-
selected_indices = _parse_selection(choice_str, len(tool_files))
|
332
|
-
if not selected_indices:
|
333
|
-
PrettyOutput.print("无效的选择", OutputType.WARNING)
|
334
|
-
continue
|
335
|
-
selected_tools = [tool_files[i - 1] for i in selected_indices]
|
336
|
-
|
337
|
-
# 确认操作
|
338
|
-
share_list = [
|
339
|
-
"\n将要分享以下工具到中心仓库(注意:文件将被移动而非复制):"
|
340
|
-
]
|
341
|
-
for tool in selected_tools:
|
342
|
-
share_list.append(f"- {tool['tool_name']} ({tool['filename']})")
|
343
|
-
PrettyOutput.print("\n".join(share_list), OutputType.WARNING)
|
344
|
-
|
345
|
-
if not user_confirm("确认移动这些工具到中心仓库吗?(原文件将被删除)"):
|
346
|
-
continue
|
347
|
-
|
348
|
-
# 移动选中的工具到中心仓库
|
349
|
-
moved_list = []
|
350
|
-
for tool in selected_tools:
|
351
|
-
src_file = tool["path"]
|
352
|
-
dst_file = os.path.join(central_repo_path, tool["filename"])
|
353
|
-
shutil.move(src_file, dst_file) # 使用move而不是copy
|
354
|
-
moved_list.append(f"已移动: {tool['tool_name']}")
|
355
|
-
|
356
|
-
# 一次性显示所有移动结果
|
357
|
-
if moved_list:
|
358
|
-
PrettyOutput.print("\n".join(moved_list), OutputType.SUCCESS)
|
359
|
-
|
360
|
-
# 提交并推送更改
|
361
|
-
PrettyOutput.print("\n正在提交更改...", OutputType.INFO)
|
362
|
-
subprocess.run(["git", "add", "."], cwd=central_repo_path, check=True)
|
363
|
-
|
364
|
-
commit_msg = f"Add {len(selected_tools)} tool(s) from local collection"
|
365
|
-
subprocess.run(
|
366
|
-
["git", "commit", "-m", commit_msg], cwd=central_repo_path, check=True
|
367
|
-
)
|
368
|
-
|
369
|
-
PrettyOutput.print("正在推送到远程仓库...", OutputType.INFO)
|
370
|
-
# 检查是否需要设置上游分支(空仓库的情况)
|
371
|
-
try:
|
372
|
-
# 先尝试普通推送
|
373
|
-
subprocess.run(["git", "push"], cwd=central_repo_path, check=True)
|
374
|
-
except subprocess.CalledProcessError:
|
375
|
-
# 如果失败,可能是空仓库,尝试设置上游分支
|
376
|
-
try:
|
377
|
-
subprocess.run(
|
378
|
-
["git", "push", "-u", "origin", "main"],
|
379
|
-
cwd=central_repo_path,
|
380
|
-
check=True,
|
381
|
-
)
|
382
|
-
except subprocess.CalledProcessError:
|
383
|
-
# 如果main分支不存在,尝试master分支
|
384
|
-
subprocess.run(
|
385
|
-
["git", "push", "-u", "origin", "master"],
|
386
|
-
cwd=central_repo_path,
|
387
|
-
check=True,
|
388
|
-
)
|
389
|
-
|
390
|
-
PrettyOutput.print("\n工具已成功分享到中心仓库!", OutputType.SUCCESS)
|
391
|
-
PrettyOutput.print(
|
392
|
-
f"原文件已从 {local_tools_dir} 移动到中心仓库", OutputType.INFO
|
393
|
-
)
|
394
|
-
break
|
395
|
-
|
396
|
-
except ValueError:
|
397
|
-
PrettyOutput.print("请输入有效的数字", OutputType.WARNING)
|
398
|
-
except subprocess.CalledProcessError as e:
|
399
|
-
PrettyOutput.print(f"Git操作失败: {str(e)}", OutputType.ERROR)
|
400
|
-
raise typer.Exit(code=1)
|
401
|
-
except Exception as e:
|
402
|
-
PrettyOutput.print(f"分享工具时出错: {str(e)}", OutputType.ERROR)
|
403
|
-
raise typer.Exit(code=1)
|
404
|
-
|
405
|
-
|
406
|
-
def _handle_share_methodology(config_file: Optional[str] = None) -> None:
|
407
|
-
"""处理方法论分享功能"""
|
408
|
-
from jarvis.jarvis_utils.config import (
|
409
|
-
get_central_methodology_repo,
|
410
|
-
get_methodology_dirs,
|
411
|
-
get_data_dir,
|
412
|
-
)
|
413
|
-
import glob
|
414
|
-
import json
|
415
|
-
import shutil
|
416
|
-
|
417
|
-
# 获取中心方法论仓库配置
|
418
|
-
central_repo = get_central_methodology_repo()
|
419
|
-
if not central_repo:
|
420
|
-
PrettyOutput.print(
|
421
|
-
"错误:未配置中心方法论仓库(JARVIS_CENTRAL_METHODOLOGY_REPO)",
|
422
|
-
OutputType.ERROR,
|
423
|
-
)
|
424
|
-
PrettyOutput.print("请在配置文件中设置中心方法论仓库的Git地址", OutputType.INFO)
|
425
|
-
raise typer.Exit(code=1)
|
426
|
-
|
427
|
-
# 克隆或更新中心方法论仓库
|
428
|
-
central_repo_path = os.path.join(get_data_dir(), "central_methodology_repo")
|
429
|
-
if not os.path.exists(central_repo_path):
|
430
|
-
PrettyOutput.print(f"正在克隆中心方法论仓库...", OutputType.INFO)
|
431
|
-
subprocess.run(["git", "clone", central_repo, central_repo_path], check=True)
|
432
|
-
else:
|
433
|
-
PrettyOutput.print(f"正在更新中心方法论仓库...", OutputType.INFO)
|
434
|
-
# 检查是否是空仓库
|
435
|
-
try:
|
436
|
-
# 先尝试获取远程分支信息
|
437
|
-
result = subprocess.run(
|
438
|
-
["git", "ls-remote", "--heads", "origin"],
|
439
|
-
cwd=central_repo_path,
|
440
|
-
capture_output=True,
|
441
|
-
text=True,
|
442
|
-
check=True,
|
443
|
-
)
|
444
|
-
# 如果有远程分支,执行pull
|
445
|
-
if result.stdout.strip():
|
446
|
-
subprocess.run(["git", "pull"], cwd=central_repo_path, check=True)
|
447
|
-
else:
|
448
|
-
PrettyOutput.print(
|
449
|
-
"中心方法论仓库是空的,将初始化为新仓库", OutputType.INFO
|
450
|
-
)
|
451
|
-
except subprocess.CalledProcessError:
|
452
|
-
# 如果命令失败,可能是网络问题或其他错误
|
453
|
-
PrettyOutput.print("无法连接到远程仓库,将跳过更新", OutputType.WARNING)
|
454
|
-
|
455
|
-
# 获取中心仓库中已有的方法论
|
456
|
-
existing_methodologies = {} # 改为字典,存储 problem_type -> content 的映射
|
457
|
-
for filepath in glob.glob(os.path.join(central_repo_path, "*.json")):
|
458
|
-
try:
|
459
|
-
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
460
|
-
methodology = json.load(f)
|
461
|
-
problem_type = methodology.get("problem_type", "")
|
462
|
-
content = methodology.get("content", "")
|
463
|
-
if problem_type and content:
|
464
|
-
existing_methodologies[problem_type] = content
|
465
|
-
except Exception:
|
466
|
-
pass
|
467
|
-
|
468
|
-
# 获取所有方法论目录
|
469
|
-
from jarvis.jarvis_utils.methodology import _get_methodology_directory
|
470
|
-
|
471
|
-
methodology_dirs = [_get_methodology_directory()] + get_methodology_dirs()
|
472
|
-
|
473
|
-
# 收集所有方法论文件(排除中心仓库目录和已存在的方法论)
|
474
|
-
all_methodologies = {}
|
475
|
-
methodology_files = []
|
476
|
-
seen_problem_types = set() # 用于去重
|
477
|
-
|
478
|
-
for directory in set(methodology_dirs):
|
479
|
-
# 跳过中心仓库目录
|
480
|
-
if os.path.abspath(directory) == os.path.abspath(central_repo_path):
|
481
|
-
continue
|
482
|
-
|
483
|
-
if not os.path.isdir(directory):
|
484
|
-
continue
|
485
|
-
|
486
|
-
for filepath in glob.glob(os.path.join(directory, "*.json")):
|
487
|
-
try:
|
488
|
-
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
489
|
-
methodology = json.load(f)
|
490
|
-
problem_type = methodology.get("problem_type", "")
|
491
|
-
content = methodology.get("content", "")
|
492
|
-
# 基于内容判断是否已存在于中心仓库
|
493
|
-
is_duplicate = False
|
494
|
-
if problem_type in existing_methodologies:
|
495
|
-
# 如果problem_type相同,比较内容
|
496
|
-
if (
|
497
|
-
content.strip()
|
498
|
-
== existing_methodologies[problem_type].strip()
|
499
|
-
):
|
500
|
-
is_duplicate = True
|
501
|
-
|
502
|
-
# 排除已存在于中心仓库的方法论(基于内容),以及本地重复的方法论
|
503
|
-
if (
|
504
|
-
problem_type
|
505
|
-
and content
|
506
|
-
and not is_duplicate
|
507
|
-
and problem_type not in seen_problem_types
|
508
|
-
):
|
509
|
-
methodology_files.append(
|
510
|
-
{
|
511
|
-
"path": filepath,
|
512
|
-
"problem_type": problem_type,
|
513
|
-
"directory": directory,
|
514
|
-
}
|
515
|
-
)
|
516
|
-
all_methodologies[problem_type] = methodology
|
517
|
-
seen_problem_types.add(problem_type)
|
518
|
-
except Exception:
|
519
|
-
pass
|
520
|
-
|
521
|
-
if not methodology_files:
|
522
|
-
PrettyOutput.print(
|
523
|
-
"没有找到新的方法论文件(所有方法论可能已存在于中心仓库)",
|
524
|
-
OutputType.WARNING,
|
525
|
-
)
|
526
|
-
raise typer.Exit(code=0)
|
527
|
-
|
528
|
-
# 显示可选的方法论
|
529
|
-
methodology_list = ["\n可分享的方法论(已排除中心仓库中已有的):"]
|
530
|
-
for i, meth in enumerate(methodology_files, 1):
|
531
|
-
dir_name = os.path.basename(meth["directory"])
|
532
|
-
methodology_list.append(f"[{i}] {meth['problem_type']} (来自: {dir_name})")
|
533
|
-
|
534
|
-
# 一次性打印所有方法论
|
535
|
-
PrettyOutput.print("\n".join(methodology_list), OutputType.INFO)
|
536
|
-
|
537
|
-
# 让用户选择要分享的方法论
|
538
|
-
while True:
|
539
|
-
try:
|
540
|
-
choice_str = prompt(
|
541
|
-
"\n请选择要分享的方法论编号(支持格式: 1,2,3,4-9,20 或 all):"
|
542
|
-
).strip()
|
543
|
-
if choice_str == "0":
|
544
|
-
raise typer.Exit(code=0)
|
545
|
-
|
546
|
-
selected_methodologies = []
|
547
|
-
if choice_str.lower() == "all":
|
548
|
-
selected_methodologies = methodology_files
|
549
|
-
else:
|
550
|
-
selected_indices = _parse_selection(choice_str, len(methodology_files))
|
551
|
-
if not selected_indices:
|
552
|
-
PrettyOutput.print("无效的选择", OutputType.WARNING)
|
553
|
-
continue
|
554
|
-
selected_methodologies = [
|
555
|
-
methodology_files[i - 1] for i in selected_indices
|
556
|
-
]
|
557
|
-
|
558
|
-
# 确认操作
|
559
|
-
share_list = ["\n将要分享以下方法论到中心仓库:"]
|
560
|
-
for meth in selected_methodologies:
|
561
|
-
share_list.append(f"- {meth['problem_type']}")
|
562
|
-
PrettyOutput.print("\n".join(share_list), OutputType.INFO)
|
563
|
-
|
564
|
-
if not user_confirm("确认分享这些方法论吗?"):
|
565
|
-
continue
|
566
|
-
|
567
|
-
# 复制选中的方法论到中心仓库
|
568
|
-
copied_list = []
|
569
|
-
for meth in selected_methodologies:
|
570
|
-
src_file = meth["path"]
|
571
|
-
dst_file = os.path.join(central_repo_path, os.path.basename(src_file))
|
572
|
-
shutil.copy2(src_file, dst_file)
|
573
|
-
copied_list.append(f"已复制: {meth['problem_type']}")
|
574
|
-
|
575
|
-
# 一次性显示所有复制结果
|
576
|
-
if copied_list:
|
577
|
-
PrettyOutput.print("\n".join(copied_list), OutputType.SUCCESS)
|
578
|
-
|
579
|
-
# 提交并推送更改
|
580
|
-
PrettyOutput.print("\n正在提交更改...", OutputType.INFO)
|
581
|
-
subprocess.run(["git", "add", "."], cwd=central_repo_path, check=True)
|
582
|
-
|
583
|
-
commit_msg = f"Add {len(selected_methodologies)} methodology(ies) from local collection"
|
584
|
-
subprocess.run(
|
585
|
-
["git", "commit", "-m", commit_msg], cwd=central_repo_path, check=True
|
586
|
-
)
|
587
|
-
|
588
|
-
PrettyOutput.print("正在推送到远程仓库...", OutputType.INFO)
|
589
|
-
# 检查是否需要设置上游分支(空仓库的情况)
|
590
|
-
try:
|
591
|
-
# 先尝试普通推送
|
592
|
-
subprocess.run(["git", "push"], cwd=central_repo_path, check=True)
|
593
|
-
except subprocess.CalledProcessError:
|
594
|
-
# 如果失败,可能是空仓库,尝试设置上游分支
|
595
|
-
try:
|
596
|
-
subprocess.run(
|
597
|
-
["git", "push", "-u", "origin", "main"],
|
598
|
-
cwd=central_repo_path,
|
599
|
-
check=True,
|
600
|
-
)
|
601
|
-
except subprocess.CalledProcessError:
|
602
|
-
# 如果main分支不存在,尝试master分支
|
603
|
-
subprocess.run(
|
604
|
-
["git", "push", "-u", "origin", "master"],
|
605
|
-
cwd=central_repo_path,
|
606
|
-
check=True,
|
607
|
-
)
|
608
|
-
|
609
|
-
PrettyOutput.print("\n方法论已成功分享到中心仓库!", OutputType.SUCCESS)
|
610
|
-
break
|
611
|
-
|
612
|
-
except ValueError:
|
613
|
-
PrettyOutput.print("请输入有效的数字", OutputType.WARNING)
|
614
|
-
except subprocess.CalledProcessError as e:
|
615
|
-
PrettyOutput.print(f"Git操作失败: {str(e)}", OutputType.ERROR)
|
616
|
-
raise typer.Exit(code=1)
|
617
|
-
except Exception as e:
|
618
|
-
PrettyOutput.print(f"分享方法论时出错: {str(e)}", OutputType.ERROR)
|
619
|
-
raise typer.Exit(code=1)
|
620
|
-
|
621
|
-
|
622
17
|
@app.callback(invoke_without_command=True)
|
623
18
|
def run_cli(
|
624
19
|
ctx: typer.Context,
|
@@ -633,6 +28,9 @@ def run_cli(
|
|
633
28
|
model_group: Optional[str] = typer.Option(
|
634
29
|
None, "--llm_group", help="使用的模型组,覆盖配置文件中的设置"
|
635
30
|
),
|
31
|
+
tool_group: Optional[str] = typer.Option(
|
32
|
+
None, "--tool_group", help="使用的工具组,覆盖配置文件中的设置"
|
33
|
+
),
|
636
34
|
config_file: Optional[str] = typer.Option(
|
637
35
|
None, "-f", "--config", help="自定义配置文件路径"
|
638
36
|
),
|
@@ -653,27 +51,40 @@ def run_cli(
|
|
653
51
|
if ctx.invoked_subcommand is not None:
|
654
52
|
return
|
655
53
|
|
656
|
-
|
54
|
+
# 处理配置文件编辑
|
55
|
+
if edit:
|
56
|
+
ConfigEditor.edit_config(config_file)
|
57
|
+
return
|
657
58
|
|
658
59
|
# 处理方法论分享
|
659
60
|
if share_methodology:
|
660
61
|
init_env("", config_file=config_file) # 初始化配置但不显示欢迎信息
|
661
|
-
|
662
|
-
|
62
|
+
manager = MethodologyShareManager()
|
63
|
+
manager.run()
|
64
|
+
return
|
663
65
|
|
664
66
|
# 处理工具分享
|
665
67
|
if share_tool:
|
666
68
|
init_env("", config_file=config_file) # 初始化配置但不显示欢迎信息
|
667
|
-
|
668
|
-
|
69
|
+
manager = ToolShareManager()
|
70
|
+
manager.run()
|
71
|
+
return
|
669
72
|
|
73
|
+
# 初始化环境
|
670
74
|
init_env(
|
671
75
|
"欢迎使用 Jarvis AI 助手,您的智能助理已准备就绪!", config_file=config_file
|
672
76
|
)
|
673
77
|
|
78
|
+
# 运行主流程
|
674
79
|
try:
|
675
|
-
|
676
|
-
|
80
|
+
agent_manager = AgentManager(
|
81
|
+
llm_type=llm_type,
|
82
|
+
model_group=model_group,
|
83
|
+
tool_group=tool_group,
|
84
|
+
restore_session=restore_session,
|
85
|
+
)
|
86
|
+
agent_manager.initialize()
|
87
|
+
agent_manager.run_task(task)
|
677
88
|
except typer.Exit:
|
678
89
|
raise
|
679
90
|
except Exception as err: # pylint: disable=broad-except
|