jarvis-ai-assistant 0.1.222__py3-none-any.whl → 0.7.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 (162) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +1143 -245
  3. jarvis/jarvis_agent/agent_manager.py +97 -0
  4. jarvis/jarvis_agent/builtin_input_handler.py +12 -10
  5. jarvis/jarvis_agent/config_editor.py +57 -0
  6. jarvis/jarvis_agent/edit_file_handler.py +392 -99
  7. jarvis/jarvis_agent/event_bus.py +48 -0
  8. jarvis/jarvis_agent/events.py +157 -0
  9. jarvis/jarvis_agent/file_context_handler.py +79 -0
  10. jarvis/jarvis_agent/file_methodology_manager.py +117 -0
  11. jarvis/jarvis_agent/jarvis.py +1117 -147
  12. jarvis/jarvis_agent/main.py +78 -34
  13. jarvis/jarvis_agent/memory_manager.py +195 -0
  14. jarvis/jarvis_agent/methodology_share_manager.py +174 -0
  15. jarvis/jarvis_agent/prompt_manager.py +82 -0
  16. jarvis/jarvis_agent/prompts.py +46 -9
  17. jarvis/jarvis_agent/protocols.py +4 -1
  18. jarvis/jarvis_agent/rewrite_file_handler.py +141 -0
  19. jarvis/jarvis_agent/run_loop.py +146 -0
  20. jarvis/jarvis_agent/session_manager.py +9 -9
  21. jarvis/jarvis_agent/share_manager.py +228 -0
  22. jarvis/jarvis_agent/shell_input_handler.py +23 -3
  23. jarvis/jarvis_agent/stdio_redirect.py +295 -0
  24. jarvis/jarvis_agent/task_analyzer.py +212 -0
  25. jarvis/jarvis_agent/task_manager.py +154 -0
  26. jarvis/jarvis_agent/task_planner.py +496 -0
  27. jarvis/jarvis_agent/tool_executor.py +8 -4
  28. jarvis/jarvis_agent/tool_share_manager.py +139 -0
  29. jarvis/jarvis_agent/user_interaction.py +42 -0
  30. jarvis/jarvis_agent/utils.py +54 -0
  31. jarvis/jarvis_agent/web_bridge.py +189 -0
  32. jarvis/jarvis_agent/web_output_sink.py +53 -0
  33. jarvis/jarvis_agent/web_server.py +751 -0
  34. jarvis/jarvis_c2rust/__init__.py +26 -0
  35. jarvis/jarvis_c2rust/cli.py +613 -0
  36. jarvis/jarvis_c2rust/collector.py +258 -0
  37. jarvis/jarvis_c2rust/library_replacer.py +1122 -0
  38. jarvis/jarvis_c2rust/llm_module_agent.py +1300 -0
  39. jarvis/jarvis_c2rust/optimizer.py +960 -0
  40. jarvis/jarvis_c2rust/scanner.py +1681 -0
  41. jarvis/jarvis_c2rust/transpiler.py +2325 -0
  42. jarvis/jarvis_code_agent/build_validation_config.py +133 -0
  43. jarvis/jarvis_code_agent/code_agent.py +1605 -178
  44. jarvis/jarvis_code_agent/code_analyzer/__init__.py +62 -0
  45. jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
  46. jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
  47. jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +102 -0
  48. jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +59 -0
  49. jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
  50. jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +69 -0
  51. jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +38 -0
  52. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +44 -0
  53. jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +38 -0
  54. jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +50 -0
  55. jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +93 -0
  56. jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +129 -0
  57. jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +54 -0
  58. jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +154 -0
  59. jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
  60. jarvis/jarvis_code_agent/code_analyzer/context_manager.py +363 -0
  61. jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
  62. jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
  63. jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
  64. jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
  65. jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
  66. jarvis/jarvis_code_agent/code_analyzer/language_support.py +89 -0
  67. jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +31 -0
  68. jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +231 -0
  69. jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +183 -0
  70. jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +219 -0
  71. jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +209 -0
  72. jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +451 -0
  73. jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +77 -0
  74. jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +48 -0
  75. jarvis/jarvis_code_agent/lint.py +275 -13
  76. jarvis/jarvis_code_agent/utils.py +142 -0
  77. jarvis/jarvis_code_analysis/checklists/loader.py +20 -6
  78. jarvis/jarvis_code_analysis/code_review.py +583 -548
  79. jarvis/jarvis_data/config_schema.json +339 -28
  80. jarvis/jarvis_git_squash/main.py +22 -13
  81. jarvis/jarvis_git_utils/git_commiter.py +171 -55
  82. jarvis/jarvis_mcp/sse_mcp_client.py +22 -15
  83. jarvis/jarvis_mcp/stdio_mcp_client.py +4 -4
  84. jarvis/jarvis_mcp/streamable_mcp_client.py +36 -16
  85. jarvis/jarvis_memory_organizer/memory_organizer.py +753 -0
  86. jarvis/jarvis_methodology/main.py +48 -63
  87. jarvis/jarvis_multi_agent/__init__.py +302 -43
  88. jarvis/jarvis_multi_agent/main.py +70 -24
  89. jarvis/jarvis_platform/ai8.py +40 -23
  90. jarvis/jarvis_platform/base.py +210 -49
  91. jarvis/jarvis_platform/human.py +11 -1
  92. jarvis/jarvis_platform/kimi.py +82 -76
  93. jarvis/jarvis_platform/openai.py +73 -1
  94. jarvis/jarvis_platform/registry.py +8 -15
  95. jarvis/jarvis_platform/tongyi.py +115 -101
  96. jarvis/jarvis_platform/yuanbao.py +89 -63
  97. jarvis/jarvis_platform_manager/main.py +194 -132
  98. jarvis/jarvis_platform_manager/service.py +122 -86
  99. jarvis/jarvis_rag/cli.py +156 -53
  100. jarvis/jarvis_rag/embedding_manager.py +155 -12
  101. jarvis/jarvis_rag/llm_interface.py +10 -13
  102. jarvis/jarvis_rag/query_rewriter.py +63 -12
  103. jarvis/jarvis_rag/rag_pipeline.py +222 -40
  104. jarvis/jarvis_rag/reranker.py +26 -3
  105. jarvis/jarvis_rag/retriever.py +270 -14
  106. jarvis/jarvis_sec/__init__.py +3605 -0
  107. jarvis/jarvis_sec/checkers/__init__.py +32 -0
  108. jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
  109. jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
  110. jarvis/jarvis_sec/cli.py +116 -0
  111. jarvis/jarvis_sec/report.py +257 -0
  112. jarvis/jarvis_sec/status.py +264 -0
  113. jarvis/jarvis_sec/types.py +20 -0
  114. jarvis/jarvis_sec/workflow.py +219 -0
  115. jarvis/jarvis_smart_shell/main.py +405 -137
  116. jarvis/jarvis_stats/__init__.py +13 -0
  117. jarvis/jarvis_stats/cli.py +387 -0
  118. jarvis/jarvis_stats/stats.py +711 -0
  119. jarvis/jarvis_stats/storage.py +612 -0
  120. jarvis/jarvis_stats/visualizer.py +282 -0
  121. jarvis/jarvis_tools/ask_user.py +1 -0
  122. jarvis/jarvis_tools/base.py +18 -2
  123. jarvis/jarvis_tools/clear_memory.py +239 -0
  124. jarvis/jarvis_tools/cli/main.py +220 -144
  125. jarvis/jarvis_tools/execute_script.py +52 -12
  126. jarvis/jarvis_tools/file_analyzer.py +17 -12
  127. jarvis/jarvis_tools/generate_new_tool.py +46 -24
  128. jarvis/jarvis_tools/read_code.py +277 -18
  129. jarvis/jarvis_tools/read_symbols.py +141 -0
  130. jarvis/jarvis_tools/read_webpage.py +86 -13
  131. jarvis/jarvis_tools/registry.py +294 -90
  132. jarvis/jarvis_tools/retrieve_memory.py +227 -0
  133. jarvis/jarvis_tools/save_memory.py +194 -0
  134. jarvis/jarvis_tools/search_web.py +62 -28
  135. jarvis/jarvis_tools/sub_agent.py +205 -0
  136. jarvis/jarvis_tools/sub_code_agent.py +217 -0
  137. jarvis/jarvis_tools/virtual_tty.py +330 -62
  138. jarvis/jarvis_utils/builtin_replace_map.py +4 -5
  139. jarvis/jarvis_utils/clipboard.py +90 -0
  140. jarvis/jarvis_utils/config.py +607 -50
  141. jarvis/jarvis_utils/embedding.py +3 -0
  142. jarvis/jarvis_utils/fzf.py +57 -0
  143. jarvis/jarvis_utils/git_utils.py +251 -29
  144. jarvis/jarvis_utils/globals.py +174 -17
  145. jarvis/jarvis_utils/http.py +58 -79
  146. jarvis/jarvis_utils/input.py +899 -153
  147. jarvis/jarvis_utils/methodology.py +210 -83
  148. jarvis/jarvis_utils/output.py +220 -137
  149. jarvis/jarvis_utils/utils.py +1906 -135
  150. jarvis_ai_assistant-0.7.0.dist-info/METADATA +465 -0
  151. jarvis_ai_assistant-0.7.0.dist-info/RECORD +192 -0
  152. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/entry_points.txt +8 -2
  153. jarvis/jarvis_git_details/main.py +0 -265
  154. jarvis/jarvis_platform/oyi.py +0 -357
  155. jarvis/jarvis_tools/edit_file.py +0 -255
  156. jarvis/jarvis_tools/rewrite_file.py +0 -195
  157. jarvis_ai_assistant-0.1.222.dist-info/METADATA +0 -767
  158. jarvis_ai_assistant-0.1.222.dist-info/RECORD +0 -110
  159. /jarvis/{jarvis_git_details → jarvis_memory_organizer}/__init__.py +0 -0
  160. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/WHEEL +0 -0
  161. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/licenses/LICENSE +0 -0
  162. {jarvis_ai_assistant-0.1.222.dist-info → jarvis_ai_assistant-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,753 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 记忆整理工具 - 用于合并具有相似标签的记忆
5
+
6
+ 该工具会查找具有高度重叠标签的记忆,并使用大模型将它们合并成一个新的记忆。
7
+ """
8
+
9
+ import json
10
+ from collections import defaultdict
11
+ from pathlib import Path
12
+ from typing import Dict, List, Set, Any, Optional
13
+
14
+ import typer
15
+
16
+ from jarvis.jarvis_utils.config import (
17
+ get_data_dir,
18
+ get_normal_platform_name,
19
+ get_normal_model_name,
20
+ )
21
+ from jarvis.jarvis_utils.output import OutputType, PrettyOutput
22
+ from jarvis.jarvis_platform.registry import PlatformRegistry
23
+ from jarvis.jarvis_utils.utils import init_env
24
+
25
+
26
+ class MemoryOrganizer:
27
+ """记忆整理器,用于合并具有相似标签的记忆"""
28
+
29
+ def __init__(self, llm_group: Optional[str] = None):
30
+ """初始化记忆整理器"""
31
+ self.project_memory_dir = Path(".jarvis/memory")
32
+ self.global_memory_dir = Path(get_data_dir()) / "memory"
33
+
34
+ # 统一使用 normal 平台与模型
35
+ platform_name_func = get_normal_platform_name
36
+ model_name_func = get_normal_model_name
37
+
38
+ # 确定平台和模型
39
+ platform_name = platform_name_func(model_group_override=llm_group)
40
+ model_name = model_name_func(model_group_override=llm_group)
41
+
42
+ # 获取当前配置的平台实例
43
+ registry = PlatformRegistry.get_global_platform_registry()
44
+ self.platform = registry.create_platform(platform_name)
45
+ if self.platform and model_name:
46
+ self.platform.set_model_name(model_name)
47
+
48
+ def _get_memory_files(self, memory_type: str) -> List[Path]:
49
+ """获取指定类型的所有记忆文件"""
50
+ if memory_type == "project_long_term":
51
+ memory_dir = self.project_memory_dir
52
+ elif memory_type == "global_long_term":
53
+ memory_dir = self.global_memory_dir / memory_type
54
+ else:
55
+ raise ValueError(f"不支持的记忆类型: {memory_type}")
56
+
57
+ if not memory_dir.exists():
58
+ return []
59
+
60
+ return list(memory_dir.glob("*.json"))
61
+
62
+ def _load_memories(self, memory_type: str) -> List[Dict[str, Any]]:
63
+ """加载指定类型的所有记忆"""
64
+ memories = []
65
+ memory_files = self._get_memory_files(memory_type)
66
+ error_lines: List[str] = []
67
+
68
+ for memory_file in memory_files:
69
+ try:
70
+ with open(memory_file, "r", encoding="utf-8") as f:
71
+ memory_data = json.load(f)
72
+ memory_data["file_path"] = str(memory_file)
73
+ memories.append(memory_data)
74
+ except Exception as e:
75
+ error_lines.append(f"读取记忆文件 {memory_file} 失败: {str(e)}")
76
+
77
+ if error_lines:
78
+ PrettyOutput.print("\n".join(error_lines), OutputType.WARNING)
79
+
80
+ return memories
81
+
82
+ def _find_overlapping_memories(
83
+ self, memories: List[Dict[str, Any]], min_overlap: int
84
+ ) -> Dict[int, List[Set[int]]]:
85
+ """
86
+ 查找具有重叠标签的记忆组
87
+
88
+ 返回:{重叠数量: [记忆索引集合列表]}
89
+ """
90
+ # 构建标签到记忆索引的映射
91
+ tag_to_memories = defaultdict(set)
92
+ for i, memory in enumerate(memories):
93
+ for tag in memory.get("tags", []):
94
+ tag_to_memories[tag].add(i)
95
+
96
+ # 查找具有共同标签的记忆对
97
+ overlap_groups = defaultdict(list)
98
+ processed_groups = set()
99
+
100
+ # 对每对记忆计算标签重叠数
101
+ for i in range(len(memories)):
102
+ for j in range(i + 1, len(memories)):
103
+ tags_i = set(memories[i].get("tags", []))
104
+ tags_j = set(memories[j].get("tags", []))
105
+ overlap_count = len(tags_i & tags_j)
106
+
107
+ if overlap_count >= min_overlap:
108
+ # 查找包含这两个记忆的最大组
109
+ group = {i, j}
110
+
111
+ # 扩展组,包含所有与组内记忆有足够重叠的记忆
112
+ changed = True
113
+ while changed:
114
+ changed = False
115
+ for k in range(len(memories)):
116
+ if k not in group:
117
+ # 检查与组内所有记忆的最小重叠数
118
+ min_overlap_with_group = min(
119
+ len(
120
+ set(memories[k].get("tags", []))
121
+ & set(memories[m].get("tags", []))
122
+ )
123
+ for m in group
124
+ )
125
+ if min_overlap_with_group >= min_overlap:
126
+ group.add(k)
127
+ changed = True
128
+
129
+ # 将组转换为有序元组以便去重
130
+ group_tuple = tuple(sorted(group))
131
+ if group_tuple not in processed_groups:
132
+ processed_groups.add(group_tuple)
133
+ overlap_groups[min_overlap].append(set(group_tuple))
134
+
135
+ return overlap_groups
136
+
137
+ def _merge_memories_with_llm(
138
+ self, memories: List[Dict[str, Any]]
139
+ ) -> Optional[Dict[str, Any]]:
140
+ """使用大模型合并多个记忆"""
141
+ # 准备合并提示
142
+ memory_contents = []
143
+ all_tags = set()
144
+
145
+ # 按创建时间排序,最新的在前
146
+ sorted_memories = sorted(
147
+ memories, key=lambda m: m.get("created_at", ""), reverse=True
148
+ )
149
+
150
+ for memory in sorted_memories:
151
+ memory_contents.append(
152
+ f"记忆ID: {memory.get('id', '未知')}\n"
153
+ f"创建时间: {memory.get('created_at', '未知')}\n"
154
+ f"标签: {', '.join(memory.get('tags', []))}\n"
155
+ f"内容:\n{memory.get('content', '')}"
156
+ )
157
+ all_tags.update(memory.get("tags", []))
158
+
159
+ memory_contents_str = (("=" * 50) + "\n").join(memory_contents)
160
+
161
+ prompt = f"""请将以下{len(memories)}个相关记忆合并成一个综合性的记忆。
162
+
163
+ 原始记忆(按时间从新到旧排序):
164
+ {"="*50}
165
+ {memory_contents_str}
166
+ {"="*50}
167
+
168
+ 原始标签集合:{', '.join(sorted(all_tags))}
169
+
170
+ 请完成以下任务:
171
+ 1. 分析这些记忆的共同主题和关键信息
172
+ 2. 将它们合并成一个连贯、完整的记忆
173
+ 3. 生成新的标签列表(保留重要标签,去除冗余,可以添加新的概括性标签)
174
+ 4. 确保合并后的记忆保留了所有重要信息
175
+ 5. **重要**:越近期的记忆权重越高,优先保留最新记忆中的信息
176
+
177
+ 请将合并结果放在 <merged_memory> 标签内,使用YAML格式:
178
+
179
+ <merged_memory>
180
+ content: |
181
+ 合并后的记忆内容
182
+ 可以是多行文本
183
+ tags:
184
+ - 标签1
185
+ - 标签2
186
+ - 标签3
187
+ </merged_memory>
188
+
189
+ 注意:
190
+ - 内容要全面但简洁
191
+ - 标签要准确反映内容主题
192
+ - 保持专业和客观的语气
193
+ - 最近的记忆信息优先级更高
194
+ - 只输出 <merged_memory> 标签内的内容,不要有其他说明
195
+ """
196
+
197
+ try:
198
+ # 调用大模型 - 收集完整响应
199
+ response_parts = []
200
+ for chunk in self.platform.chat(prompt): # type: ignore
201
+ response_parts.append(chunk)
202
+ response = "".join(response_parts)
203
+
204
+ # 解析响应
205
+ import re
206
+ import yaml # type: ignore[import-untyped]
207
+
208
+ # 提取 <merged_memory> 标签内的内容
209
+ yaml_match = re.search(
210
+ r"<merged_memory>(.*?)</merged_memory>",
211
+ response,
212
+ re.DOTALL | re.IGNORECASE,
213
+ )
214
+
215
+ if yaml_match:
216
+ yaml_content = yaml_match.group(1).strip()
217
+ try:
218
+ result = yaml.safe_load(yaml_content)
219
+ return {
220
+ "content": result.get("content", ""),
221
+ "tags": result.get("tags", []),
222
+ "type": memories[0].get("type", "unknown"),
223
+ "merged_from": [m.get("id", "") for m in memories],
224
+ }
225
+ except yaml.YAMLError as e:
226
+ raise ValueError(f"无法解析YAML内容: {str(e)}")
227
+ else:
228
+ raise ValueError("无法从模型响应中提取 <merged_memory> 标签内容")
229
+
230
+ except Exception as e:
231
+ PrettyOutput.print(f"调用大模型合并记忆失败: {str(e)}", OutputType.WARNING)
232
+ # 返回 None 表示合并失败,跳过这组记忆
233
+ return None
234
+
235
+ def organize_memories(
236
+ self,
237
+ memory_type: str,
238
+ min_overlap: int = 2,
239
+ dry_run: bool = False,
240
+ ) -> Dict[str, Any]:
241
+ """
242
+ 整理指定类型的记忆
243
+
244
+ 参数:
245
+ memory_type: 记忆类型
246
+ min_overlap: 最小标签重叠数
247
+ dry_run: 是否只进行模拟运行
248
+
249
+ 返回:
250
+ 整理结果统计
251
+ """
252
+ PrettyOutput.print(
253
+ f"开始整理{memory_type}类型的记忆,最小重叠标签数: {min_overlap}",
254
+ OutputType.INFO,
255
+ )
256
+
257
+ # 加载记忆
258
+ memories = self._load_memories(memory_type)
259
+ if not memories:
260
+ PrettyOutput.print("没有找到需要整理的记忆", OutputType.INFO)
261
+ return {"processed": 0, "merged": 0}
262
+
263
+ PrettyOutput.print(f"加载了 {len(memories)} 个记忆", OutputType.INFO)
264
+
265
+ # 统计信息
266
+ stats = {
267
+ "total_memories": len(memories),
268
+ "processed_groups": 0,
269
+ "merged_memories": 0,
270
+ "created_memories": 0,
271
+ }
272
+
273
+ # 从高重叠度开始处理
274
+ max_tags = max(len(m.get("tags", [])) for m in memories)
275
+
276
+ # 创建一个标记已删除记忆的集合
277
+ deleted_indices = set()
278
+
279
+ for overlap_count in range(min(max_tags, 5), min_overlap - 1, -1):
280
+ # 过滤掉已删除的记忆
281
+ active_memories = [
282
+ (i, mem) for i, mem in enumerate(memories) if i not in deleted_indices
283
+ ]
284
+ if not active_memories:
285
+ break
286
+
287
+ # 创建索引映射:原始索引 -> 活跃索引
288
+ active_memory_list = [mem for _, mem in active_memories]
289
+
290
+ overlap_groups = self._find_overlapping_memories(
291
+ active_memory_list, overlap_count
292
+ )
293
+
294
+ if overlap_count in overlap_groups:
295
+ groups = overlap_groups[overlap_count]
296
+ PrettyOutput.print(
297
+ f"\n发现 {len(groups)} 个具有 {overlap_count} 个重叠标签的记忆组",
298
+ OutputType.INFO,
299
+ )
300
+
301
+ for group in groups:
302
+ # 将活跃索引转换回原始索引
303
+ original_indices = set()
304
+ for active_idx in group:
305
+ original_idx = active_memories[active_idx][0]
306
+ original_indices.add(original_idx)
307
+
308
+ group_memories = [memories[i] for i in original_indices]
309
+
310
+ # 显示将要合并的记忆(先拼接后统一打印,避免循环逐条输出)
311
+ lines = ["", f"准备合并 {len(group_memories)} 个记忆:"]
312
+ for mem in group_memories:
313
+ lines.append(
314
+ f" - ID: {mem.get('id', '未知')}, "
315
+ f"标签: {', '.join(mem.get('tags', []))[:50]}..."
316
+ )
317
+ PrettyOutput.print("\n".join(lines), OutputType.INFO)
318
+
319
+ if not dry_run:
320
+ # 合并记忆
321
+ merged_memory = self._merge_memories_with_llm(group_memories)
322
+
323
+ # 如果合并失败,跳过这组
324
+ if merged_memory is None:
325
+ PrettyOutput.print(
326
+ " 跳过这组记忆的合并", OutputType.WARNING
327
+ )
328
+ continue
329
+
330
+ # 保存新记忆
331
+ self._save_merged_memory(
332
+ merged_memory,
333
+ memory_type,
334
+ [memories[i] for i in original_indices],
335
+ )
336
+
337
+ stats["processed_groups"] += 1
338
+ stats["merged_memories"] += len(original_indices)
339
+ stats["created_memories"] += 1
340
+
341
+ # 标记这些记忆已被删除
342
+ deleted_indices.update(original_indices)
343
+ else:
344
+ PrettyOutput.print(" [模拟运行] 跳过实际合并", OutputType.INFO)
345
+
346
+ # 显示统计信息
347
+ PrettyOutput.print("\n整理完成!", OutputType.SUCCESS)
348
+ PrettyOutput.print(f"总记忆数: {stats['total_memories']}", OutputType.INFO)
349
+ PrettyOutput.print(f"处理的组数: {stats['processed_groups']}", OutputType.INFO)
350
+ PrettyOutput.print(f"合并的记忆数: {stats['merged_memories']}", OutputType.INFO)
351
+ PrettyOutput.print(
352
+ f"创建的新记忆数: {stats['created_memories']}", OutputType.INFO
353
+ )
354
+
355
+ return stats
356
+
357
+ def _save_merged_memory(
358
+ self,
359
+ memory: Dict[str, Any],
360
+ memory_type: str,
361
+ original_memories: List[Dict[str, Any]],
362
+ ):
363
+ """保存合并后的记忆并删除原始记忆"""
364
+ import uuid
365
+ from datetime import datetime
366
+
367
+ # 生成新的记忆ID
368
+ memory["id"] = f"merged_{uuid.uuid4().hex[:8]}"
369
+ memory["created_at"] = datetime.now().isoformat()
370
+ memory["type"] = memory_type
371
+
372
+ # 确定保存路径
373
+ if memory_type == "project_long_term":
374
+ memory_dir = self.project_memory_dir
375
+ else:
376
+ memory_dir = self.global_memory_dir / memory_type
377
+
378
+ memory_dir.mkdir(parents=True, exist_ok=True)
379
+
380
+ # 保存新记忆
381
+ new_file = memory_dir / f"{memory['id']}.json"
382
+ with open(new_file, "w", encoding="utf-8") as f:
383
+ json.dump(memory, f, ensure_ascii=False, indent=2)
384
+
385
+ PrettyOutput.print(
386
+ f"创建新记忆: {memory['id']} (标签: {', '.join(memory['tags'][:3])}...)",
387
+ OutputType.SUCCESS,
388
+ )
389
+
390
+ # 删除原始记忆文件(先汇总日志,最后统一打印)
391
+ info_lines: List[str] = []
392
+ warn_lines: List[str] = []
393
+ for orig_memory in original_memories:
394
+ if "file_path" in orig_memory:
395
+ try:
396
+ file_path = Path(orig_memory["file_path"])
397
+ if file_path.exists():
398
+ file_path.unlink()
399
+ info_lines.append(f"删除原始记忆: {orig_memory.get('id', '未知')}")
400
+ else:
401
+ info_lines.append(
402
+ f"原始记忆文件已不存在,跳过删除: {orig_memory.get('id', '未知')}"
403
+ )
404
+ except Exception as e:
405
+ warn_lines.append(
406
+ f"删除记忆文件失败 {orig_memory.get('file_path', '')}: {str(e)}"
407
+ )
408
+ if info_lines:
409
+ PrettyOutput.print("\n".join(info_lines), OutputType.INFO)
410
+ if warn_lines:
411
+ PrettyOutput.print("\n".join(warn_lines), OutputType.WARNING)
412
+
413
+ def export_memories(
414
+ self,
415
+ memory_types: List[str],
416
+ output_file: Path,
417
+ tags: Optional[List[str]] = None,
418
+ ) -> int:
419
+ """
420
+ 导出指定类型的记忆到文件
421
+
422
+ 参数:
423
+ memory_types: 要导出的记忆类型列表
424
+ output_file: 输出文件路径
425
+ tags: 可选的标签过滤器
426
+
427
+ 返回:
428
+ 导出的记忆数量
429
+ """
430
+ all_memories = []
431
+ progress_lines: List[str] = []
432
+
433
+ for memory_type in memory_types:
434
+ progress_lines.append(f"正在导出 {memory_type} 类型的记忆...")
435
+ memories = self._load_memories(memory_type)
436
+
437
+ # 如果指定了标签,进行过滤
438
+ if tags:
439
+ filtered_memories = []
440
+ for memory in memories:
441
+ memory_tags = set(memory.get("tags", []))
442
+ if any(tag in memory_tags for tag in tags):
443
+ filtered_memories.append(memory)
444
+ memories = filtered_memories
445
+
446
+ # 添加记忆类型信息并移除文件路径
447
+ for memory in memories:
448
+ memory["memory_type"] = memory_type
449
+ memory.pop("file_path", None)
450
+
451
+ all_memories.extend(memories)
452
+ progress_lines.append(f"从 {memory_type} 导出了 {len(memories)} 个记忆")
453
+
454
+ # 统一展示导出进度日志
455
+ if progress_lines:
456
+ PrettyOutput.print("\n".join(progress_lines), OutputType.INFO)
457
+
458
+ # 保存到文件
459
+ output_file.parent.mkdir(parents=True, exist_ok=True)
460
+ with open(output_file, "w", encoding="utf-8") as f:
461
+ json.dump(all_memories, f, ensure_ascii=False, indent=2)
462
+
463
+ PrettyOutput.print(
464
+ f"成功导出 {len(all_memories)} 个记忆到 {output_file}", OutputType.SUCCESS
465
+ )
466
+
467
+ return len(all_memories)
468
+
469
+ def import_memories(
470
+ self,
471
+ input_file: Path,
472
+ overwrite: bool = False,
473
+ ) -> Dict[str, int]:
474
+ """
475
+ 从文件导入记忆
476
+
477
+ 参数:
478
+ input_file: 输入文件路径
479
+ overwrite: 是否覆盖已存在的记忆
480
+
481
+ 返回:
482
+ 导入统计 {memory_type: count}
483
+ """
484
+ # 读取记忆文件
485
+ if not input_file.exists():
486
+ raise FileNotFoundError(f"导入文件不存在: {input_file}")
487
+
488
+ with open(input_file, "r", encoding="utf-8") as f:
489
+ memories = json.load(f)
490
+
491
+ if not isinstance(memories, list):
492
+ raise ValueError("导入文件格式错误,应为记忆列表")
493
+
494
+ PrettyOutput.print(f"准备导入 {len(memories)} 个记忆", OutputType.INFO)
495
+
496
+ # 统计导入结果
497
+ import_stats: Dict[str, int] = defaultdict(int)
498
+ skipped_count = 0
499
+
500
+ for memory in memories:
501
+ memory_type = memory.get("memory_type", memory.get("type"))
502
+ if not memory_type:
503
+ skipped_count += 1
504
+ continue
505
+
506
+ # 确定保存路径
507
+ if memory_type == "project_long_term":
508
+ memory_dir = self.project_memory_dir
509
+ elif memory_type == "global_long_term":
510
+ memory_dir = self.global_memory_dir / memory_type
511
+ else:
512
+ PrettyOutput.print(
513
+ f"跳过不支持的记忆类型: {memory_type}", OutputType.WARNING
514
+ )
515
+ skipped_count += 1
516
+ continue
517
+
518
+ memory_dir.mkdir(parents=True, exist_ok=True)
519
+
520
+ # 检查是否已存在
521
+ memory_id = memory.get("id")
522
+ if not memory_id:
523
+ import uuid
524
+
525
+ memory_id = f"imported_{uuid.uuid4().hex[:8]}"
526
+ memory["id"] = memory_id
527
+
528
+ memory_file = memory_dir / f"{memory_id}.json"
529
+
530
+ if memory_file.exists() and not overwrite:
531
+ PrettyOutput.print(f"跳过已存在的记忆: {memory_id}", OutputType.INFO)
532
+ skipped_count += 1
533
+ continue
534
+
535
+ # 保存记忆
536
+ with open(memory_file, "w", encoding="utf-8") as f:
537
+ # 清理记忆数据
538
+ clean_memory = {
539
+ "id": memory["id"],
540
+ "type": memory_type,
541
+ "tags": memory.get("tags", []),
542
+ "content": memory.get("content", ""),
543
+ "created_at": memory.get("created_at", ""),
544
+ }
545
+ if "merged_from" in memory:
546
+ clean_memory["merged_from"] = memory["merged_from"]
547
+
548
+ json.dump(clean_memory, f, ensure_ascii=False, indent=2)
549
+
550
+ import_stats[memory_type] += 1
551
+
552
+ # 显示导入结果
553
+ PrettyOutput.print("\n导入完成!", OutputType.SUCCESS)
554
+ if import_stats:
555
+ lines = [f"{memory_type}: 导入了 {count} 个记忆" for memory_type, count in import_stats.items()]
556
+ PrettyOutput.print("\n".join(lines), OutputType.INFO)
557
+
558
+ if skipped_count > 0:
559
+ PrettyOutput.print(f"跳过了 {skipped_count} 个记忆", OutputType.WARNING)
560
+
561
+ return dict(import_stats)
562
+
563
+
564
+ app = typer.Typer(help="记忆整理工具 - 合并具有相似标签的记忆")
565
+
566
+
567
+ @app.command("organize")
568
+ def organize(
569
+ memory_type: str = typer.Option(
570
+ "project_long_term",
571
+ "--type",
572
+ help="要整理的记忆类型(project_long_term 或 global_long_term)",
573
+ ),
574
+ min_overlap: int = typer.Option(
575
+ 2,
576
+ "--min-overlap",
577
+ help="最小标签重叠数,必须大于等于2",
578
+ ),
579
+ dry_run: bool = typer.Option(
580
+ False,
581
+ "--dry-run",
582
+ help="模拟运行,只显示将要进行的操作但不实际执行",
583
+ ),
584
+ llm_group: Optional[str] = typer.Option(
585
+ None, "-g", "--llm-group", help="使用的模型组,覆盖配置文件中的设置"
586
+ ),
587
+
588
+ ):
589
+ """
590
+ 整理和合并具有相似标签的记忆。
591
+
592
+ 示例:
593
+
594
+ # 整理项目长期记忆,最小重叠标签数为3
595
+ jarvis-memory-organizer organize --type project_long_term --min-overlap 3
596
+
597
+ # 整理全局长期记忆,模拟运行
598
+ jarvis-memory-organizer organize --type global_long_term --dry-run
599
+
600
+ # 使用默认设置(最小重叠数2)整理项目记忆
601
+ jarvis-memory-organizer organize
602
+ """
603
+ # 验证参数
604
+ if memory_type not in ["project_long_term", "global_long_term"]:
605
+ PrettyOutput.print(
606
+ f"错误:不支持的记忆类型 '{memory_type}',请选择 'project_long_term' 或 'global_long_term'",
607
+ OutputType.ERROR,
608
+ )
609
+ raise typer.Exit(1)
610
+
611
+ if min_overlap < 2:
612
+ PrettyOutput.print("错误:最小重叠数必须大于等于2", OutputType.ERROR)
613
+ raise typer.Exit(1)
614
+
615
+ # 创建整理器并执行
616
+ try:
617
+ organizer = MemoryOrganizer(llm_group=llm_group)
618
+ stats = organizer.organize_memories(
619
+ memory_type=memory_type, min_overlap=min_overlap, dry_run=dry_run
620
+ )
621
+
622
+ # 根据结果返回适当的退出码
623
+ if stats.get("processed_groups", 0) > 0 or dry_run:
624
+ raise typer.Exit(0)
625
+ else:
626
+ raise typer.Exit(0) # 即使没有处理也是正常退出
627
+
628
+ except typer.Exit:
629
+ # typer.Exit 是正常的退出方式,直接传播
630
+ raise
631
+ except Exception as e:
632
+ PrettyOutput.print(f"记忆整理失败: {str(e)}", OutputType.ERROR)
633
+ raise typer.Exit(1)
634
+
635
+
636
+ @app.command("export")
637
+ def export(
638
+ output: Path = typer.Argument(
639
+ ...,
640
+ help="导出文件路径(JSON格式)",
641
+ ),
642
+ memory_types: List[str] = typer.Option(
643
+ ["project_long_term", "global_long_term"],
644
+ "--type",
645
+ "-t",
646
+ help="要导出的记忆类型(可多次指定)",
647
+ ),
648
+ tags: Optional[List[str]] = typer.Option(
649
+ None,
650
+ "--tag",
651
+ help="按标签过滤(可多次指定)",
652
+ ),
653
+ ):
654
+ """
655
+ 导出记忆到文件。
656
+
657
+ 示例:
658
+
659
+ # 导出所有记忆到文件
660
+ jarvis-memory-organizer export memories.json
661
+
662
+ # 只导出项目长期记忆
663
+ jarvis-memory-organizer export project_memories.json -t project_long_term
664
+
665
+ # 导出带特定标签的记忆
666
+ jarvis-memory-organizer export tagged_memories.json --tag Python --tag API
667
+ """
668
+ try:
669
+ organizer = MemoryOrganizer()
670
+
671
+ # 验证记忆类型(先收集无效类型,统一打印一次)
672
+ valid_types = ["project_long_term", "global_long_term"]
673
+ invalid_types = [mt for mt in memory_types if mt not in valid_types]
674
+ if invalid_types:
675
+ PrettyOutput.print(
676
+ "错误:不支持的记忆类型: " + ", ".join(f"'{mt}'" for mt in invalid_types),
677
+ OutputType.ERROR,
678
+ )
679
+ raise typer.Exit(1)
680
+
681
+ count = organizer.export_memories(
682
+ memory_types=memory_types,
683
+ output_file=output,
684
+ tags=tags,
685
+ )
686
+
687
+ if count > 0:
688
+ raise typer.Exit(0)
689
+ else:
690
+ PrettyOutput.print("没有找到要导出的记忆", OutputType.WARNING)
691
+ raise typer.Exit(0)
692
+
693
+ except Exception as e:
694
+ PrettyOutput.print(f"导出失败: {str(e)}", OutputType.ERROR)
695
+ raise typer.Exit(1)
696
+
697
+
698
+ @app.command("import")
699
+ def import_memories(
700
+ input: Path = typer.Argument(
701
+ ...,
702
+ help="导入文件路径(JSON格式)",
703
+ ),
704
+ overwrite: bool = typer.Option(
705
+ False,
706
+ "--overwrite",
707
+ "-o",
708
+ help="覆盖已存在的记忆",
709
+ ),
710
+ ):
711
+ """
712
+ 从文件导入记忆。
713
+
714
+ 示例:
715
+
716
+ # 导入记忆文件
717
+ jarvis-memory-organizer import memories.json
718
+
719
+ # 导入并覆盖已存在的记忆
720
+ jarvis-memory-organizer import memories.json --overwrite
721
+ """
722
+ try:
723
+ organizer = MemoryOrganizer()
724
+
725
+ stats = organizer.import_memories(
726
+ input_file=input,
727
+ overwrite=overwrite,
728
+ )
729
+
730
+ total_imported = sum(stats.values())
731
+ if total_imported > 0:
732
+ raise typer.Exit(0)
733
+ else:
734
+ PrettyOutput.print("没有导入任何记忆", OutputType.WARNING)
735
+ raise typer.Exit(0)
736
+
737
+ except FileNotFoundError as e:
738
+ PrettyOutput.print(str(e), OutputType.ERROR)
739
+ raise typer.Exit(1)
740
+ except Exception as e:
741
+ PrettyOutput.print(f"导入失败: {str(e)}", OutputType.ERROR)
742
+ raise typer.Exit(1)
743
+
744
+
745
+ def main():
746
+ """Application entry point"""
747
+ # 统一初始化环境
748
+ init_env("欢迎使用记忆整理工具!")
749
+ app()
750
+
751
+
752
+ if __name__ == "__main__":
753
+ main()