jarvis-ai-assistant 0.3.28__py3-none-any.whl → 0.3.29__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 +60 -25
- jarvis/jarvis_agent/event_bus.py +2 -2
- jarvis/jarvis_agent/events.py +157 -0
- jarvis/jarvis_agent/file_methodology_manager.py +17 -4
- jarvis/jarvis_agent/jarvis.py +3 -3
- jarvis/jarvis_agent/memory_manager.py +4 -3
- jarvis/jarvis_agent/prompts.py +2 -2
- jarvis/jarvis_agent/protocols.py +4 -1
- jarvis/jarvis_agent/run_loop.py +9 -24
- jarvis/jarvis_agent/shell_input_handler.py +6 -1
- jarvis/jarvis_agent/task_analyzer.py +18 -13
- jarvis/jarvis_agent/task_manager.py +6 -4
- jarvis/jarvis_agent/utils.py +50 -0
- jarvis/jarvis_mcp/sse_mcp_client.py +1 -1
- jarvis/jarvis_memory_organizer/memory_organizer.py +4 -4
- jarvis/jarvis_platform/kimi.py +1 -1
- jarvis/jarvis_platform/tongyi.py +1 -1
- jarvis/jarvis_platform/yuanbao.py +1 -1
- jarvis/jarvis_rag/retriever.py +3 -3
- jarvis/jarvis_stats/stats.py +2 -2
- jarvis/jarvis_stats/storage.py +3 -3
- jarvis/jarvis_tools/edit_file.py +3 -3
- jarvis/jarvis_tools/execute_script.py +2 -2
- jarvis/jarvis_tools/sub_agent.py +2 -2
- jarvis/jarvis_tools/sub_code_agent.py +2 -2
- jarvis/jarvis_utils/config.py +3 -3
- jarvis/jarvis_utils/fzf.py +4 -3
- jarvis/jarvis_utils/input.py +5 -5
- jarvis/jarvis_utils/utils.py +96 -9
- {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/METADATA +1 -1
- {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/RECORD +36 -34
- {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/entry_points.txt +0 -0
- {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.28.dist-info → jarvis_ai_assistant-0.3.29.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,8 @@ from jarvis.jarvis_utils.globals import get_interrupt, set_interrupt
|
|
8
8
|
|
9
9
|
from jarvis.jarvis_agent.prompts import TASK_ANALYSIS_PROMPT
|
10
10
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
11
|
+
from jarvis.jarvis_agent.utils import join_prompts
|
12
|
+
from jarvis.jarvis_agent.events import BEFORE_TOOL_CALL, AFTER_TOOL_CALL, BEFORE_SUMMARY, TASK_COMPLETED
|
11
13
|
|
12
14
|
|
13
15
|
class TaskAnalyzer:
|
@@ -55,10 +57,7 @@ class TaskAnalyzer:
|
|
55
57
|
|
56
58
|
def _prepare_analysis_prompt(self, satisfaction_feedback: str) -> str:
|
57
59
|
"""准备分析提示"""
|
58
|
-
|
59
|
-
if satisfaction_feedback:
|
60
|
-
analysis_prompt += satisfaction_feedback
|
61
|
-
return analysis_prompt
|
60
|
+
return join_prompts([TASK_ANALYSIS_PROMPT, satisfaction_feedback])
|
62
61
|
|
63
62
|
def _process_analysis_loop(self):
|
64
63
|
"""处理分析循环"""
|
@@ -74,7 +73,7 @@ class TaskAnalyzer:
|
|
74
73
|
# 执行工具调用(补充事件:before_tool_call/after_tool_call)
|
75
74
|
try:
|
76
75
|
self.agent.event_bus.emit(
|
77
|
-
|
76
|
+
BEFORE_TOOL_CALL,
|
78
77
|
agent=self.agent,
|
79
78
|
current_response=response,
|
80
79
|
)
|
@@ -84,7 +83,7 @@ class TaskAnalyzer:
|
|
84
83
|
self.agent.session.prompt = tool_prompt
|
85
84
|
try:
|
86
85
|
self.agent.event_bus.emit(
|
87
|
-
|
86
|
+
AFTER_TOOL_CALL,
|
88
87
|
agent=self.agent,
|
89
88
|
current_response=response,
|
90
89
|
need_return=need_return,
|
@@ -130,9 +129,15 @@ class TaskAnalyzer:
|
|
130
129
|
def _handle_interrupt_with_tool_calls(self, user_input: str) -> str:
|
131
130
|
"""处理有工具调用时的中断"""
|
132
131
|
if self.agent.user_confirm("检测到有工具调用,是否继续处理工具调用?", True):
|
133
|
-
return
|
132
|
+
return join_prompts([
|
133
|
+
f"被用户中断,用户补充信息为:{user_input}",
|
134
|
+
"用户同意继续工具调用。"
|
135
|
+
])
|
134
136
|
else:
|
135
|
-
return
|
137
|
+
return join_prompts([
|
138
|
+
f"被用户中断,用户补充信息为:{user_input}",
|
139
|
+
"检测到有工具调用,但被用户拒绝执行。请根据用户的补充信息重新考虑下一步操作。"
|
140
|
+
])
|
136
141
|
|
137
142
|
def collect_satisfaction_feedback(self, auto_completed: bool) -> str:
|
138
143
|
"""收集满意度反馈"""
|
@@ -140,18 +145,18 @@ class TaskAnalyzer:
|
|
140
145
|
|
141
146
|
if not auto_completed and self.agent.use_analysis:
|
142
147
|
if self.agent.user_confirm("您对本次任务的完成是否满意?", True):
|
143
|
-
satisfaction_feedback = "
|
148
|
+
satisfaction_feedback = "用户对本次任务的完成表示满意。"
|
144
149
|
else:
|
145
150
|
feedback = self.agent._multiline_input(
|
146
151
|
"请提供您的反馈意见(可留空直接回车):", False
|
147
152
|
)
|
148
153
|
if feedback:
|
149
154
|
satisfaction_feedback = (
|
150
|
-
f"
|
155
|
+
f"用户对本次任务的完成不满意,反馈意见如下:\n{feedback}"
|
151
156
|
)
|
152
157
|
else:
|
153
158
|
satisfaction_feedback = (
|
154
|
-
"
|
159
|
+
"用户对本次任务的完成不满意,未提供具体反馈意见。"
|
155
160
|
)
|
156
161
|
|
157
162
|
return satisfaction_feedback
|
@@ -162,9 +167,9 @@ class TaskAnalyzer:
|
|
162
167
|
def _subscribe_events(self) -> None:
|
163
168
|
bus = self.agent.get_event_bus() # type: ignore[attr-defined]
|
164
169
|
# 在生成总结前触发(保持与原顺序一致)
|
165
|
-
bus.subscribe(
|
170
|
+
bus.subscribe(BEFORE_SUMMARY, self._on_before_summary)
|
166
171
|
# 当无需总结时,作为兜底触发分析
|
167
|
-
bus.subscribe(
|
172
|
+
bus.subscribe(TASK_COMPLETED, self._on_task_completed)
|
168
173
|
|
169
174
|
def _on_before_summary(self, **payload) -> None:
|
170
175
|
if self._analysis_done:
|
@@ -14,6 +14,7 @@ from jarvis.jarvis_agent import (
|
|
14
14
|
get_multiline_input,
|
15
15
|
user_confirm,
|
16
16
|
)
|
17
|
+
from jarvis.jarvis_agent.utils import join_prompts
|
17
18
|
from jarvis.jarvis_utils.config import get_data_dir
|
18
19
|
from jarvis.jarvis_utils.fzf import fzf_select
|
19
20
|
|
@@ -109,7 +110,7 @@ class TaskManager:
|
|
109
110
|
if need_additional:
|
110
111
|
additional_input = get_multiline_input("请输入补充信息:")
|
111
112
|
if additional_input:
|
112
|
-
selected_task = f"
|
113
|
+
selected_task = join_prompts([selected_task, f"补充信息:\n{additional_input}"])
|
113
114
|
return selected_task
|
114
115
|
except Exception:
|
115
116
|
# 如果解析失败,则回退到手动输入
|
@@ -138,9 +139,10 @@ class TaskManager:
|
|
138
139
|
if need_additional:
|
139
140
|
additional_input = get_multiline_input("请输入补充信息:")
|
140
141
|
if additional_input:
|
141
|
-
selected_task = (
|
142
|
-
|
143
|
-
|
142
|
+
selected_task = join_prompts([
|
143
|
+
selected_task,
|
144
|
+
f"补充信息:\n{additional_input}"
|
145
|
+
])
|
144
146
|
return selected_task
|
145
147
|
PrettyOutput.print(
|
146
148
|
"无效的选择。请选择列表中的一个号码。", OutputType.WARNING
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
工具函数(jarvis_agent.utils)
|
4
|
+
|
5
|
+
- join_prompts: 统一的提示拼接策略(仅拼接非空段落,使用双换行)
|
6
|
+
- is_auto_complete: 统一的自动完成标记检测
|
7
|
+
"""
|
8
|
+
from typing import Iterable, List, Any
|
9
|
+
from enum import Enum
|
10
|
+
from jarvis.jarvis_utils.tag import ot
|
11
|
+
|
12
|
+
def join_prompts(parts: Iterable[str]) -> str:
|
13
|
+
"""
|
14
|
+
将多个提示片段按统一规则拼接:
|
15
|
+
- 过滤掉空字符串
|
16
|
+
- 使用两个换行分隔
|
17
|
+
- 不进行额外 strip,保持调用方原样语义
|
18
|
+
"""
|
19
|
+
non_empty: List[str] = [p for p in parts if p]
|
20
|
+
return "\n\n".join(non_empty)
|
21
|
+
|
22
|
+
def is_auto_complete(response: str) -> bool:
|
23
|
+
"""
|
24
|
+
检测是否包含自动完成标记。
|
25
|
+
当前实现:包含 ot('!!!COMPLETE!!!') 即视为自动完成。
|
26
|
+
"""
|
27
|
+
try:
|
28
|
+
return ot("!!!COMPLETE!!!") in response
|
29
|
+
except Exception:
|
30
|
+
# 防御性处理:即使 ot 出现异常,也不阻塞主流程
|
31
|
+
return "!!!COMPLETE!!!" in response
|
32
|
+
|
33
|
+
def normalize_next_action(next_action: Any) -> str:
|
34
|
+
"""
|
35
|
+
规范化下一步动作为字符串:
|
36
|
+
- 如果是 Enum, 返回其 value(若为字符串)
|
37
|
+
- 如果是 str, 原样返回
|
38
|
+
- 其他情况返回空字符串
|
39
|
+
"""
|
40
|
+
try:
|
41
|
+
if isinstance(next_action, Enum):
|
42
|
+
value = getattr(next_action, "value", None)
|
43
|
+
return value if isinstance(value, str) else ""
|
44
|
+
if isinstance(next_action, str):
|
45
|
+
return next_action
|
46
|
+
return ""
|
47
|
+
except Exception:
|
48
|
+
return ""
|
49
|
+
|
50
|
+
__all__ = ["join_prompts", "is_auto_complete", "normalize_next_action"]
|
@@ -63,7 +63,7 @@ class MemoryOrganizer:
|
|
63
63
|
"""加载指定类型的所有记忆"""
|
64
64
|
memories = []
|
65
65
|
memory_files = self._get_memory_files(memory_type)
|
66
|
-
error_lines:
|
66
|
+
error_lines: List[str] = []
|
67
67
|
|
68
68
|
for memory_file in memory_files:
|
69
69
|
try:
|
@@ -388,8 +388,8 @@ tags:
|
|
388
388
|
)
|
389
389
|
|
390
390
|
# 删除原始记忆文件(先汇总日志,最后统一打印)
|
391
|
-
info_lines:
|
392
|
-
warn_lines:
|
391
|
+
info_lines: List[str] = []
|
392
|
+
warn_lines: List[str] = []
|
393
393
|
for orig_memory in original_memories:
|
394
394
|
if "file_path" in orig_memory:
|
395
395
|
try:
|
@@ -428,7 +428,7 @@ tags:
|
|
428
428
|
导出的记忆数量
|
429
429
|
"""
|
430
430
|
all_memories = []
|
431
|
-
progress_lines:
|
431
|
+
progress_lines: List[str] = []
|
432
432
|
|
433
433
|
for memory_type in memory_types:
|
434
434
|
progress_lines.append(f"正在导出 {memory_type} 类型的记忆...")
|
jarvis/jarvis_platform/kimi.py
CHANGED
@@ -194,7 +194,7 @@ class KimiModel(BasePlatform):
|
|
194
194
|
uploaded_files = []
|
195
195
|
for index, file_path in enumerate(file_list, 1):
|
196
196
|
file_name = os.path.basename(file_path)
|
197
|
-
log_lines:
|
197
|
+
log_lines: List[str] = [f"处理文件 [{index}/{len(file_list)}]: {file_name}"]
|
198
198
|
try:
|
199
199
|
mime_type, _ = mimetypes.guess_type(file_path)
|
200
200
|
action = (
|
jarvis/jarvis_platform/tongyi.py
CHANGED
@@ -276,7 +276,7 @@ class TongyiPlatform(BasePlatform):
|
|
276
276
|
|
277
277
|
for file_path in file_list:
|
278
278
|
file_name = os.path.basename(file_path)
|
279
|
-
log_lines:
|
279
|
+
log_lines: List[str] = []
|
280
280
|
log_lines.append(f"上传文件 {file_name}")
|
281
281
|
try:
|
282
282
|
if not os.path.exists(file_path):
|
@@ -133,7 +133,7 @@ class YuanbaoPlatform(BasePlatform):
|
|
133
133
|
|
134
134
|
for file_path in file_list:
|
135
135
|
file_name = os.path.basename(file_path)
|
136
|
-
log_lines:
|
136
|
+
log_lines: List[str] = []
|
137
137
|
log_lines.append(f"上传文件 {file_name}")
|
138
138
|
try:
|
139
139
|
# 1. Prepare the file information
|
jarvis/jarvis_rag/retriever.py
CHANGED
@@ -185,7 +185,7 @@ class ChromaRetriever:
|
|
185
185
|
if not changed and not deleted:
|
186
186
|
return
|
187
187
|
# 为避免在循环中逐条打印,先拼接后统一打印
|
188
|
-
lines:
|
188
|
+
lines: List[str] = []
|
189
189
|
if changed:
|
190
190
|
lines.append(
|
191
191
|
f"检测到 {len(changed)} 个已索引文件发生变化,建议重新索引以保证检索准确性。"
|
@@ -247,7 +247,7 @@ class ChromaRetriever:
|
|
247
247
|
return
|
248
248
|
|
249
249
|
# 先处理删除
|
250
|
-
delete_errors:
|
250
|
+
delete_errors: List[str] = []
|
251
251
|
for src in deleted:
|
252
252
|
try:
|
253
253
|
self.collection.delete(where={"source": src}) # type: ignore[arg-type]
|
@@ -258,7 +258,7 @@ class ChromaRetriever:
|
|
258
258
|
|
259
259
|
# 再处理变更(重建)
|
260
260
|
docs_to_add: List[Document] = []
|
261
|
-
rebuild_errors:
|
261
|
+
rebuild_errors: List[str] = []
|
262
262
|
for src in changed:
|
263
263
|
try:
|
264
264
|
# 删除旧条目
|
jarvis/jarvis_stats/stats.py
CHANGED
@@ -604,8 +604,8 @@ class StatsManager:
|
|
604
604
|
|
605
605
|
@staticmethod
|
606
606
|
def _show_multiple_charts(
|
607
|
-
start_time: datetime,
|
608
|
-
end_time: datetime,
|
607
|
+
start_time: Optional[datetime],
|
608
|
+
end_time: Optional[datetime],
|
609
609
|
aggregation: str,
|
610
610
|
tags: Optional[Dict[str, str]],
|
611
611
|
width: Optional[int] = None,
|
jarvis/jarvis_stats/storage.py
CHANGED
@@ -8,7 +8,7 @@ import json
|
|
8
8
|
import os
|
9
9
|
from datetime import datetime, timedelta
|
10
10
|
from pathlib import Path
|
11
|
-
from typing import Dict, List, Optional, Any
|
11
|
+
from typing import Dict, List, Optional, Any, Set
|
12
12
|
from collections import defaultdict
|
13
13
|
import sys
|
14
14
|
import time
|
@@ -457,7 +457,7 @@ class StatsStorage:
|
|
457
457
|
metrics_from_meta = set(meta.get("metrics", {}).keys())
|
458
458
|
|
459
459
|
# 扫描所有数据文件获取实际存在的指标
|
460
|
-
metrics_from_data:
|
460
|
+
metrics_from_data: Set[str] = set()
|
461
461
|
for data_file in self.data_dir.glob("stats_*.json"):
|
462
462
|
try:
|
463
463
|
data = self._load_json(data_file)
|
@@ -467,7 +467,7 @@ class StatsStorage:
|
|
467
467
|
continue
|
468
468
|
|
469
469
|
# 扫描总量缓存目录中已有的指标文件
|
470
|
-
metrics_from_totals:
|
470
|
+
metrics_from_totals: Set[str] = set()
|
471
471
|
try:
|
472
472
|
for f in self.totals_dir.glob("*"):
|
473
473
|
if f.is_file():
|
jarvis/jarvis_tools/edit_file.py
CHANGED
@@ -14,7 +14,7 @@
|
|
14
14
|
- 完善的错误处理和回滚机制
|
15
15
|
- 严格的格式保持要求
|
16
16
|
"""
|
17
|
-
from typing import Any, Dict
|
17
|
+
from typing import Any, Dict, List
|
18
18
|
|
19
19
|
from jarvis.jarvis_agent.edit_file_handler import EditFileHandler
|
20
20
|
|
@@ -122,7 +122,7 @@ class FileSearchReplaceTool:
|
|
122
122
|
|
123
123
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
124
124
|
|
125
|
-
stdout_messages:
|
125
|
+
stdout_messages: List[str] = []
|
126
126
|
overall_success = False
|
127
127
|
file_results = []
|
128
128
|
|
@@ -167,7 +167,7 @@ class FileSearchReplaceTool:
|
|
167
167
|
)
|
168
168
|
|
169
169
|
# 整合所有错误信息到stderr
|
170
|
-
all_stderr:
|
170
|
+
all_stderr: List[str] = []
|
171
171
|
for file_result in file_results:
|
172
172
|
if not file_result["success"]:
|
173
173
|
all_stderr.append(f"文件 {file_result['file']} 处理失败: {file_result['stderr']}")
|
@@ -2,7 +2,7 @@
|
|
2
2
|
import os
|
3
3
|
import tempfile
|
4
4
|
from pathlib import Path
|
5
|
-
from typing import Any, Dict
|
5
|
+
from typing import Any, Dict, List
|
6
6
|
|
7
7
|
from jarvis.jarvis_utils.output import OutputType, PrettyOutput
|
8
8
|
|
@@ -74,7 +74,7 @@ class ScriptTool:
|
|
74
74
|
stream.feed(data)
|
75
75
|
|
76
76
|
# 清理每行右侧空格,并过滤空行
|
77
|
-
cleaned:
|
77
|
+
cleaned: List[str] = []
|
78
78
|
for y in range(screen.lines):
|
79
79
|
line = screen.buffer[y]
|
80
80
|
stripped = "".join(char.data for char in line.values()).rstrip()
|
jarvis/jarvis_tools/sub_agent.py
CHANGED
@@ -8,7 +8,7 @@ sub_agent 工具
|
|
8
8
|
- 继承父 Agent 的部分配置:model_group、input_handler、execute_tool_confirm、multiline_inputer;其他参数需显式提供
|
9
9
|
- 子Agent必须自动完成(auto_complete=True)且需要summary(need_summary=True)
|
10
10
|
"""
|
11
|
-
from typing import Any, Dict
|
11
|
+
from typing import Any, Dict, List
|
12
12
|
import json
|
13
13
|
|
14
14
|
from jarvis.jarvis_agent import Agent
|
@@ -107,7 +107,7 @@ class SubAgentTool:
|
|
107
107
|
|
108
108
|
# 解析可用工具列表(支持数组或以逗号分隔的字符串)
|
109
109
|
_use_tools = args.get("use_tools", None)
|
110
|
-
use_tools:
|
110
|
+
use_tools: List[str] = []
|
111
111
|
if isinstance(_use_tools, list):
|
112
112
|
use_tools = [str(x).strip() for x in _use_tools if str(x).strip()]
|
113
113
|
elif isinstance(_use_tools, str):
|
@@ -8,7 +8,7 @@ sub_code_agent 工具
|
|
8
8
|
- 不依赖父 Agent,所有配置使用系统默认与全局变量
|
9
9
|
- 子Agent必须自动完成(auto_complete=True)且需要summary(need_summary=True)
|
10
10
|
"""
|
11
|
-
from typing import Any, Dict
|
11
|
+
from typing import Any, Dict, List
|
12
12
|
import json
|
13
13
|
|
14
14
|
from jarvis.jarvis_code_agent.code_agent import CodeAgent
|
@@ -85,7 +85,7 @@ class SubCodeAgentTool:
|
|
85
85
|
except Exception:
|
86
86
|
parent_agent = None
|
87
87
|
model_group = None
|
88
|
-
use_tools:
|
88
|
+
use_tools: List[str] = []
|
89
89
|
try:
|
90
90
|
if parent_agent is not None:
|
91
91
|
if getattr(parent_agent, "model", None):
|
jarvis/jarvis_utils/config.py
CHANGED
@@ -285,7 +285,7 @@ def get_pretty_output() -> bool:
|
|
285
285
|
if platform.system() == "Windows":
|
286
286
|
return False
|
287
287
|
|
288
|
-
return GLOBAL_CONFIG_DATA.get("JARVIS_PRETTY_OUTPUT",
|
288
|
+
return GLOBAL_CONFIG_DATA.get("JARVIS_PRETTY_OUTPUT", True)
|
289
289
|
|
290
290
|
|
291
291
|
def is_use_methodology() -> bool:
|
@@ -423,9 +423,9 @@ def is_force_save_memory() -> bool:
|
|
423
423
|
获取是否强制保存记忆。
|
424
424
|
|
425
425
|
返回:
|
426
|
-
bool: 如果强制保存记忆则返回True,默认为
|
426
|
+
bool: 如果强制保存记忆则返回True,默认为False
|
427
427
|
"""
|
428
|
-
return GLOBAL_CONFIG_DATA.get("JARVIS_FORCE_SAVE_MEMORY",
|
428
|
+
return GLOBAL_CONFIG_DATA.get("JARVIS_FORCE_SAVE_MEMORY", False) is True
|
429
429
|
|
430
430
|
|
431
431
|
def is_enable_static_analysis() -> bool:
|
jarvis/jarvis_utils/fzf.py
CHANGED
@@ -2,10 +2,10 @@
|
|
2
2
|
"""FZF selector utility."""
|
3
3
|
import shutil
|
4
4
|
import subprocess
|
5
|
-
from typing import List, Optional, Union
|
5
|
+
from typing import List, Optional, Union, Dict, Any, cast
|
6
6
|
|
7
7
|
def fzf_select(
|
8
|
-
options: Union[List[str], List[
|
8
|
+
options: Union[List[str], List[Dict[str, Any]]],
|
9
9
|
prompt: str = "SELECT> ",
|
10
10
|
key: Optional[str] = None,
|
11
11
|
) -> Optional[str]:
|
@@ -29,7 +29,8 @@ def fzf_select(
|
|
29
29
|
if isinstance(options[0], dict):
|
30
30
|
if key is None:
|
31
31
|
raise ValueError("A key must be provided for a list of dicts.")
|
32
|
-
|
32
|
+
options_dict = cast(List[Dict[str, Any]], options)
|
33
|
+
input_lines = [str(item.get(key, "")) for item in options_dict]
|
33
34
|
else:
|
34
35
|
input_lines = [str(item) for item in options]
|
35
36
|
|
jarvis/jarvis_utils/input.py
CHANGED
@@ -11,7 +11,7 @@
|
|
11
11
|
import os
|
12
12
|
import sys
|
13
13
|
import base64
|
14
|
-
from typing import Iterable, List
|
14
|
+
from typing import Iterable, List, Optional
|
15
15
|
import wcwidth
|
16
16
|
|
17
17
|
from colorama import Fore
|
@@ -308,7 +308,7 @@ class FileCompleter(Completer):
|
|
308
308
|
import os as _os
|
309
309
|
|
310
310
|
if self._all_files_cache is None:
|
311
|
-
files:
|
311
|
+
files: List[str] = []
|
312
312
|
for root, dirs, fnames in _os.walk(".", followlinks=False):
|
313
313
|
# Exclude .git directory
|
314
314
|
dirs[:] = [d for d in dirs if d != ".git"]
|
@@ -429,7 +429,7 @@ def _show_history_and_copy():
|
|
429
429
|
|
430
430
|
|
431
431
|
def _get_multiline_input_internal(
|
432
|
-
tip: str, preset: str
|
432
|
+
tip: str, preset: Optional[str] = None, preset_cursor: Optional[int] = None
|
433
433
|
) -> str:
|
434
434
|
"""
|
435
435
|
Internal function to get multiline input using prompt_toolkit.
|
@@ -660,8 +660,8 @@ def get_multiline_input(tip: str, print_on_empty: bool = True) -> str:
|
|
660
660
|
tip: 提示文本,将显示在底部工具栏中
|
661
661
|
print_on_empty: 当输入为空字符串时,是否打印“输入已取消”提示。默认打印。
|
662
662
|
"""
|
663
|
-
preset: str
|
664
|
-
preset_cursor: int
|
663
|
+
preset: Optional[str] = None
|
664
|
+
preset_cursor: Optional[int] = None
|
665
665
|
while True:
|
666
666
|
user_input = _get_multiline_input_internal(
|
667
667
|
tip, preset=preset, preset_cursor=preset_cursor
|
jarvis/jarvis_utils/utils.py
CHANGED
@@ -7,7 +7,7 @@ import subprocess
|
|
7
7
|
import sys
|
8
8
|
import time
|
9
9
|
from pathlib import Path
|
10
|
-
from typing import Any, Callable, Dict, List, Optional
|
10
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
11
11
|
from datetime import datetime, date
|
12
12
|
|
13
13
|
import yaml # type: ignore
|
@@ -97,6 +97,98 @@ def is_rag_installed() -> bool:
|
|
97
97
|
return len(get_missing_rag_modules()) == 0
|
98
98
|
|
99
99
|
|
100
|
+
def is_editable_install() -> bool:
|
101
|
+
"""
|
102
|
+
检测当前 Jarvis 是否以可编辑模式安装(pip/uv install -e .)。
|
103
|
+
|
104
|
+
判断顺序:
|
105
|
+
1. 读取 PEP 610 的 direct_url.json(dir_info.editable)
|
106
|
+
2. 兼容旧式 .egg-link 安装
|
107
|
+
3. 启发式回退:源码路径上游存在 .git 且不在 site-packages/dist-packages
|
108
|
+
"""
|
109
|
+
# 优先使用 importlib.metadata 读取 distribution 的 direct_url.json
|
110
|
+
try:
|
111
|
+
import importlib.metadata as metadata # Python 3.8+
|
112
|
+
except Exception:
|
113
|
+
metadata = None # type: ignore
|
114
|
+
|
115
|
+
def _check_direct_url() -> Optional[bool]:
|
116
|
+
if metadata is None:
|
117
|
+
return None
|
118
|
+
candidates = ["jarvis-ai-assistant", "jarvis_ai_assistant"]
|
119
|
+
for name in candidates:
|
120
|
+
try:
|
121
|
+
dist = metadata.distribution(name)
|
122
|
+
except Exception:
|
123
|
+
continue
|
124
|
+
try:
|
125
|
+
files = dist.files or []
|
126
|
+
for f in files:
|
127
|
+
try:
|
128
|
+
if f.name == "direct_url.json":
|
129
|
+
p = Path(str(dist.locate_file(f)))
|
130
|
+
if p.exists():
|
131
|
+
with open(p, "r", encoding="utf-8", errors="ignore") as fp:
|
132
|
+
info = json.load(fp)
|
133
|
+
dir_info = info.get("dir_info") or {}
|
134
|
+
if isinstance(dir_info, dict) and bool(dir_info.get("editable")):
|
135
|
+
return True
|
136
|
+
# 兼容部分工具可能写入顶层 editable 字段
|
137
|
+
if bool(info.get("editable")):
|
138
|
+
return True
|
139
|
+
return False # 找到了 direct_url.json 但未标记 editable
|
140
|
+
except Exception:
|
141
|
+
continue
|
142
|
+
except Exception:
|
143
|
+
continue
|
144
|
+
return None
|
145
|
+
|
146
|
+
res = _check_direct_url()
|
147
|
+
if res is True:
|
148
|
+
return True
|
149
|
+
if res is False:
|
150
|
+
# 明确不是可编辑安装
|
151
|
+
return False
|
152
|
+
|
153
|
+
# 兼容旧式 .egg-link 可编辑安装
|
154
|
+
try:
|
155
|
+
module_path = Path(__file__).resolve()
|
156
|
+
pkg_root = module_path.parent.parent # jarvis 包根目录
|
157
|
+
for entry in sys.path:
|
158
|
+
try:
|
159
|
+
p = Path(entry)
|
160
|
+
if not p.exists() or not p.is_dir():
|
161
|
+
continue
|
162
|
+
for egg in p.glob("*.egg-link"):
|
163
|
+
try:
|
164
|
+
text = egg.read_text(encoding="utf-8", errors="ignore")
|
165
|
+
first_line = (text.strip().splitlines() or [""])[0]
|
166
|
+
if not first_line:
|
167
|
+
continue
|
168
|
+
src_path = Path(first_line).resolve()
|
169
|
+
# 当前包根目录在 egg-link 指向的源码路径下,视为可编辑安装
|
170
|
+
if str(pkg_root).startswith(str(src_path)):
|
171
|
+
return True
|
172
|
+
except Exception:
|
173
|
+
continue
|
174
|
+
except Exception:
|
175
|
+
continue
|
176
|
+
except Exception:
|
177
|
+
pass
|
178
|
+
|
179
|
+
# 启发式回退:源码仓库路径
|
180
|
+
try:
|
181
|
+
parents = list(Path(__file__).resolve().parents)
|
182
|
+
has_git = any((d / ".git").exists() for d in parents)
|
183
|
+
in_site = any(("site-packages" in str(d)) or ("dist-packages" in str(d)) for d in parents)
|
184
|
+
if has_git and not in_site:
|
185
|
+
return True
|
186
|
+
except Exception:
|
187
|
+
pass
|
188
|
+
|
189
|
+
return False
|
190
|
+
|
191
|
+
|
100
192
|
def _setup_signal_handler() -> None:
|
101
193
|
"""设置SIGINT信号处理函数"""
|
102
194
|
original_sigint = signal.getsignal(signal.SIGINT)
|
@@ -124,7 +216,7 @@ def _check_pip_updates() -> bool:
|
|
124
216
|
from packaging import version
|
125
217
|
|
126
218
|
# 检查上次检查日期
|
127
|
-
last_check_file = Path(get_data_dir()) / "last_pip_check"
|
219
|
+
last_check_file = Path(str(get_data_dir())) / "last_pip_check"
|
128
220
|
today_str = date.today().strftime("%Y-%m-%d")
|
129
221
|
|
130
222
|
if last_check_file.exists():
|
@@ -867,7 +959,6 @@ def load_config():
|
|
867
959
|
_load_and_process_config(str(config_file_path.parent), str(config_file_path))
|
868
960
|
|
869
961
|
|
870
|
-
from typing import Tuple
|
871
962
|
|
872
963
|
|
873
964
|
def _load_config_file(config_file: str) -> Tuple[str, dict]:
|
@@ -1235,11 +1326,7 @@ def _collect_optional_config_interactively(
|
|
1235
1326
|
" 适用于您希望绕过检查并自行管理仓库状态的场景。"
|
1236
1327
|
)
|
1237
1328
|
|
1238
|
-
|
1239
|
-
# 查找当前模式在选项中的索引
|
1240
|
-
default_index = choices.index(current_mode)
|
1241
|
-
except ValueError:
|
1242
|
-
default_index = 0 # 默认为第一个选项
|
1329
|
+
|
1243
1330
|
|
1244
1331
|
new_mode = get_choice(
|
1245
1332
|
tip,
|
@@ -1848,7 +1935,7 @@ def daily_check_git_updates(repo_dirs: List[str], repo_type: str):
|
|
1848
1935
|
repo_dirs (List[str]): 需要检查的git仓库目录列表。
|
1849
1936
|
repo_type (str): 仓库的类型名称,例如 "工具" 或 "方法论",用于日志输出。
|
1850
1937
|
"""
|
1851
|
-
data_dir = Path(get_data_dir())
|
1938
|
+
data_dir = Path(str(get_data_dir()))
|
1852
1939
|
last_check_file = data_dir / f"{repo_type}_updates_last_check.txt"
|
1853
1940
|
should_check_for_updates = True
|
1854
1941
|
|