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