myagent-ai 1.13.2 → 1.13.4
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.
- package/memory/__pycache__/manager.cpython-312.pyc +0 -0
- package/memory/manager.py +127 -19
- package/package.json +1 -1
- package/web/api_server.py +7 -5
- package/web/ui/chat/chat_main.js +11 -10
|
Binary file
|
package/memory/manager.py
CHANGED
|
@@ -633,6 +633,62 @@ class MemoryManager:
|
|
|
633
633
|
# 记忆搜索
|
|
634
634
|
# ==========================================================================
|
|
635
635
|
|
|
636
|
+
# ==========================================================================
|
|
637
|
+
# 时间衰减权重 (遗忘曲线)
|
|
638
|
+
# ==========================================================================
|
|
639
|
+
|
|
640
|
+
@staticmethod
|
|
641
|
+
def _compute_time_weight(created_at: str, half_life_days: float = 30.0) -> float:
|
|
642
|
+
"""
|
|
643
|
+
计算记忆的时间衰减权重(模拟遗忘曲线)。
|
|
644
|
+
|
|
645
|
+
使用指数衰减模型: weight = e^(-ln(2) * age_days / half_life_days)
|
|
646
|
+
- 刚创建的记忆权重为 1.0
|
|
647
|
+
- 经过 half_life_days 后权重降至 0.5
|
|
648
|
+
- 经过 2 * half_life_days 后权重降至 0.25
|
|
649
|
+
- 权重永远不会降到 0(长期记忆仍可被召回)
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
created_at: 记忆创建时间(ISO 8601 格式,如 "2025-01-15T08:30:00+00:00")
|
|
653
|
+
half_life_days: 半衰期天数(默认 30 天)
|
|
654
|
+
- 7 天: 适合短期会话记忆,快速遗忘
|
|
655
|
+
- 30 天: 默认值,平衡近期与长期记忆
|
|
656
|
+
- 90 天: 适合知识型场景,长期保留
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
时间权重值 (0.0 ~ 1.0)
|
|
660
|
+
|
|
661
|
+
Examples:
|
|
662
|
+
>>> _compute_time_weight("2025-04-12T00:00:00+00:00", half_life_days=30)
|
|
663
|
+
1.0 # 今天的记忆
|
|
664
|
+
>>> _compute_time_weight("2025-03-13T00:00:00+00:00", half_life_days=30)
|
|
665
|
+
0.5 # 30天前的记忆
|
|
666
|
+
>>> _compute_time_weight("2025-02-11T00:00:00+00:00", half_life_days=30)
|
|
667
|
+
0.25 # 60天前的记忆
|
|
668
|
+
"""
|
|
669
|
+
if not created_at:
|
|
670
|
+
return 0.5 # 无时间信息的记忆给中等权重
|
|
671
|
+
|
|
672
|
+
try:
|
|
673
|
+
# 解析 ISO 8601 时间戳
|
|
674
|
+
created_dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
|
675
|
+
now_dt = datetime.now(created_dt.tzinfo) if created_dt.tzinfo else datetime.now()
|
|
676
|
+
|
|
677
|
+
age_seconds = (now_dt - created_dt).total_seconds()
|
|
678
|
+
if age_seconds <= 0:
|
|
679
|
+
return 1.0 # 未来的时间戳或零延迟
|
|
680
|
+
|
|
681
|
+
age_days = age_seconds / 86400.0
|
|
682
|
+
|
|
683
|
+
# 指数衰减: e^(-ln(2) * age / half_life)
|
|
684
|
+
decay = math.exp(-0.693147 * age_days / half_life_days)
|
|
685
|
+
|
|
686
|
+
# 限制最低权重为 0.05,确保极老的记忆仍有一丝被召回的可能
|
|
687
|
+
return max(decay, 0.05)
|
|
688
|
+
except (ValueError, TypeError, OSError) as e:
|
|
689
|
+
logger.debug(f"时间衰减计算失败 ({created_at}): {e}")
|
|
690
|
+
return 0.5 # 解析失败时给中等权重
|
|
691
|
+
|
|
636
692
|
# ==========================================================================
|
|
637
693
|
# TF-IDF 语义搜索 (无外部依赖)
|
|
638
694
|
# ==========================================================================
|
|
@@ -753,21 +809,31 @@ class MemoryManager:
|
|
|
753
809
|
category: str = "",
|
|
754
810
|
limit: int = 10,
|
|
755
811
|
mode: str = "hybrid",
|
|
812
|
+
time_decay: bool = True,
|
|
813
|
+
half_life_days: float = 30.0,
|
|
756
814
|
) -> List[MemoryEntry]:
|
|
757
815
|
"""
|
|
758
|
-
|
|
816
|
+
搜索记忆(内置遗忘曲线时间衰减)。
|
|
759
817
|
|
|
760
818
|
支持三种搜索模式:
|
|
761
819
|
- "keyword": 传统 LIKE 关键词匹配(快速)
|
|
762
820
|
- "semantic": TF-IDF 语义搜索(理解语义相似性)
|
|
763
821
|
- "hybrid": 混合搜索(默认)= 0.4 * keyword_score + 0.6 * tfidf_score
|
|
764
822
|
|
|
823
|
+
所有模式均支持时间衰减权重(遗忘曲线),越久远的记忆权重越低。
|
|
824
|
+
最终得分 = 相关性得分 × 时间权重。
|
|
825
|
+
|
|
765
826
|
Args:
|
|
766
827
|
query: 搜索查询
|
|
767
828
|
session_id: 会话 ID(空=所有会话)
|
|
768
829
|
category: 记忆类别(空=所有类别)
|
|
769
830
|
limit: 返回数量
|
|
770
831
|
mode: 搜索模式 "keyword" | "semantic" | "hybrid"
|
|
832
|
+
time_decay: 是否启用时间衰减(默认 True)
|
|
833
|
+
half_life_days: 遗忘曲线半衰期天数(默认 30 天)
|
|
834
|
+
- 7 天: 短期记忆场景,快速遗忘
|
|
835
|
+
- 30 天: 默认,平衡近期与长期
|
|
836
|
+
- 90 天: 知识型场景,长期保留
|
|
771
837
|
"""
|
|
772
838
|
conn = self._get_conn()
|
|
773
839
|
conditions = ["1=1"]
|
|
@@ -787,26 +853,38 @@ class MemoryManager:
|
|
|
787
853
|
where = " AND ".join(conditions)
|
|
788
854
|
|
|
789
855
|
if mode == "keyword":
|
|
790
|
-
return self._search_keyword(conn, query, where, params, limit
|
|
856
|
+
return self._search_keyword(conn, query, where, params, limit,
|
|
857
|
+
time_decay=time_decay, half_life_days=half_life_days)
|
|
791
858
|
elif mode == "semantic":
|
|
792
|
-
return self._search_semantic(conn, query, where, params, limit
|
|
859
|
+
return self._search_semantic(conn, query, where, params, limit,
|
|
860
|
+
time_decay=time_decay, half_life_days=half_life_days)
|
|
793
861
|
else:
|
|
794
|
-
# 混合模式:取两种搜索结果的加权和
|
|
795
|
-
keyword_results = self._search_keyword(conn, query, where, params, limit * 2
|
|
796
|
-
|
|
862
|
+
# 混合模式:取两种搜索结果的加权和 + 时间衰减
|
|
863
|
+
keyword_results = self._search_keyword(conn, query, where, params, limit * 2,
|
|
864
|
+
time_decay=False, half_life_days=half_life_days)
|
|
865
|
+
semantic_results = self._search_semantic(conn, query, where, params, limit * 2,
|
|
866
|
+
time_decay=False, half_life_days=half_life_days)
|
|
797
867
|
|
|
798
|
-
#
|
|
868
|
+
# 合并评分(相关性 + 时间衰减)
|
|
799
869
|
combined: Dict[str, Tuple[MemoryEntry, float]] = {}
|
|
800
870
|
for i, entry in enumerate(keyword_results):
|
|
801
|
-
|
|
802
|
-
|
|
871
|
+
relevance = 1.0 - (i / max(len(keyword_results), 1))
|
|
872
|
+
score = relevance * 0.4
|
|
873
|
+
combined[entry.id] = (entry, score)
|
|
803
874
|
|
|
804
875
|
for i, entry in enumerate(semantic_results):
|
|
805
|
-
|
|
876
|
+
relevance = 1.0 - (i / max(len(semantic_results), 1))
|
|
877
|
+
sem_score = relevance * 0.6
|
|
806
878
|
if entry.id in combined:
|
|
807
|
-
combined[entry.id] = (entry, combined[entry.id][1] +
|
|
879
|
+
combined[entry.id] = (entry, combined[entry.id][1] + sem_score)
|
|
808
880
|
else:
|
|
809
|
-
combined[entry.id] = (entry,
|
|
881
|
+
combined[entry.id] = (entry, sem_score)
|
|
882
|
+
|
|
883
|
+
# 应用时间衰减权重: final_score = relevance_score × time_weight
|
|
884
|
+
if time_decay:
|
|
885
|
+
for mem_id, (entry, relevance_score) in combined.items():
|
|
886
|
+
tw = self._compute_time_weight(entry.created_at, half_life_days)
|
|
887
|
+
combined[mem_id] = (entry, relevance_score * tw)
|
|
810
888
|
|
|
811
889
|
# 按综合得分排序
|
|
812
890
|
sorted_results = sorted(
|
|
@@ -832,28 +910,45 @@ class MemoryManager:
|
|
|
832
910
|
where: str,
|
|
833
911
|
params: list,
|
|
834
912
|
limit: int,
|
|
913
|
+
time_decay: bool = False,
|
|
914
|
+
half_life_days: float = 30.0,
|
|
835
915
|
) -> List[MemoryEntry]:
|
|
836
|
-
"""关键词 LIKE
|
|
916
|
+
"""关键词 LIKE 搜索(支持时间衰减重排序)"""
|
|
837
917
|
like_pattern = f"%{query}%"
|
|
838
918
|
conditions = f"{where} AND (content LIKE ? OR summary LIKE ? OR key LIKE ?)"
|
|
839
919
|
search_params = params + [like_pattern, like_pattern, like_pattern]
|
|
840
920
|
|
|
921
|
+
# 取更多候选(后续按时间衰减重排序)
|
|
922
|
+
fetch_limit = limit * 3 if time_decay else limit
|
|
841
923
|
sql = f"""
|
|
842
924
|
SELECT * FROM memories WHERE {conditions}
|
|
843
925
|
ORDER BY importance DESC, access_count DESC
|
|
844
926
|
LIMIT ?
|
|
845
927
|
"""
|
|
846
|
-
search_params.append(
|
|
928
|
+
search_params.append(fetch_limit)
|
|
847
929
|
rows = conn.execute(sql, search_params).fetchall()
|
|
848
930
|
|
|
849
|
-
for row in rows
|
|
931
|
+
entries = [MemoryEntry.from_row(row) for row in rows]
|
|
932
|
+
|
|
933
|
+
# 应用时间衰减重排序
|
|
934
|
+
if time_decay and entries:
|
|
935
|
+
scored = []
|
|
936
|
+
for entry in entries:
|
|
937
|
+
base_score = entry.importance + entry.access_count * 0.01
|
|
938
|
+
tw = self._compute_time_weight(entry.created_at, half_life_days)
|
|
939
|
+
scored.append((entry, base_score * tw))
|
|
940
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
941
|
+
entries = [e for e, _ in scored[:limit]]
|
|
942
|
+
|
|
943
|
+
# 更新访问计数
|
|
944
|
+
for entry in entries:
|
|
850
945
|
conn.execute(
|
|
851
946
|
"UPDATE memories SET access_count = access_count + 1 WHERE id = ?",
|
|
852
|
-
(
|
|
947
|
+
(entry.id,),
|
|
853
948
|
)
|
|
854
949
|
conn.commit()
|
|
855
950
|
|
|
856
|
-
return
|
|
951
|
+
return entries
|
|
857
952
|
|
|
858
953
|
def _search_semantic(
|
|
859
954
|
self,
|
|
@@ -862,8 +957,10 @@ class MemoryManager:
|
|
|
862
957
|
where: str,
|
|
863
958
|
params: list,
|
|
864
959
|
limit: int,
|
|
960
|
+
time_decay: bool = False,
|
|
961
|
+
half_life_days: float = 30.0,
|
|
865
962
|
) -> List[MemoryEntry]:
|
|
866
|
-
"""TF-IDF
|
|
963
|
+
"""TF-IDF 语义搜索(支持时间衰减重排序)"""
|
|
867
964
|
# 先取一批候选文档
|
|
868
965
|
candidate_sql = f"SELECT * FROM memories WHERE {where} ORDER BY created_at DESC LIMIT 200"
|
|
869
966
|
rows = conn.execute(candidate_sql, params).fetchall()
|
|
@@ -883,6 +980,14 @@ class MemoryManager:
|
|
|
883
980
|
# 计算 TF-IDF 得分
|
|
884
981
|
scores = self._compute_tfidf(query, documents)
|
|
885
982
|
|
|
983
|
+
# 应用时间衰减重排序
|
|
984
|
+
if time_decay:
|
|
985
|
+
for doc_id, tfidf_score in scores.items():
|
|
986
|
+
row = row_map.get(doc_id)
|
|
987
|
+
if row:
|
|
988
|
+
tw = self._compute_time_weight(row["created_at"], half_life_days)
|
|
989
|
+
scores[doc_id] = tfidf_score * tw
|
|
990
|
+
|
|
886
991
|
# 按得分排序,取 top N
|
|
887
992
|
top_ids = list(scores.keys())[:limit]
|
|
888
993
|
result = [MemoryEntry.from_row(row_map[doc_id]) for doc_id in top_ids if doc_id in row_map]
|
|
@@ -903,9 +1008,12 @@ class MemoryManager:
|
|
|
903
1008
|
category: str = "",
|
|
904
1009
|
limit: int = 20,
|
|
905
1010
|
mode: str = "hybrid",
|
|
1011
|
+
time_decay: bool = True,
|
|
1012
|
+
half_life_days: float = 30.0,
|
|
906
1013
|
) -> List[MemoryEntry]:
|
|
907
1014
|
"""跨会话搜索"""
|
|
908
|
-
return self.search(query, session_id="", category=category, limit=limit, mode=mode
|
|
1015
|
+
return self.search(query, session_id="", category=category, limit=limit, mode=mode,
|
|
1016
|
+
time_decay=time_decay, half_life_days=half_life_days)
|
|
909
1017
|
|
|
910
1018
|
# ==========================================================================
|
|
911
1019
|
# 记忆总结与维护
|
package/package.json
CHANGED
package/web/api_server.py
CHANGED
|
@@ -1034,23 +1034,25 @@ class ApiServer:
|
|
|
1034
1034
|
base_instruction = (
|
|
1035
1035
|
"你当前处于【执行模式】(Execution Mode)。\n\n"
|
|
1036
1036
|
"## 核心规则\n"
|
|
1037
|
-
"1.
|
|
1037
|
+
"1. **任务列表(按复杂度决定)**:\n"
|
|
1038
|
+
" - 如果用户的需求是简单任务(预计操作步骤不超过5步,如:单次查询、简单计算、问答题、格式转换、文件读取等),【不要】使用 ```tasklist```,直接用纯文本回复并执行即可。\n"
|
|
1039
|
+
" - 只有当任务较复杂(预计需要超过5步操作,如:多文件修改、需要调研+实现+测试、涉及多个模块联动等),才使用 ```tasklist``` 代码块来跟踪进度。\n"
|
|
1038
1040
|
" - 格式:```tasklist\\n[{\"text\": \"步骤描述\", \"status\": \"pending\"}]\\n```\n"
|
|
1039
1041
|
" - status 可选值:pending(待执行)、running(进行中)、done(已完成)、blocked(受阻)\n"
|
|
1040
|
-
" -
|
|
1042
|
+
" - 首次收到复杂任务时,拆分为多个步骤,全部标记为 pending\n"
|
|
1041
1043
|
" - 每次执行完一个步骤后,更新对应步骤状态为 done,下一个为 running\n"
|
|
1042
1044
|
"2. **单步执行(强制)**:每次回复【只能执行一个操作】(一个工具调用、一个代码块或一个技能调用)。\n"
|
|
1043
1045
|
" - 执行完一个操作后停下来,等待结果反馈后再决定下一步\n"
|
|
1044
1046
|
" - 不要一次性执行多个操作\n"
|
|
1045
|
-
"3. **回复格式**:先写纯文本分析/总结 →
|
|
1046
|
-
"4.
|
|
1047
|
+
"3. **回复格式**:先写纯文本分析/总结 → 如有任务列表则用 ```tasklist``` 更新进度 → 最后用 ```action``` 执行操作(如有)\n"
|
|
1048
|
+
"4. **任务完成**:当使用任务列表且所有步骤都标记为 done 时,用 ```action``` 输出 {\"type\": \"final_answer\", \"content\": \"...\"} 结束任务。简单任务直接回复即可。\n"
|
|
1047
1049
|
)
|
|
1048
1050
|
|
|
1049
1051
|
# 从内存读取当前任务列表(按 session 隔离)
|
|
1050
1052
|
store_key = session_id or agent_path
|
|
1051
1053
|
tasks = self._task_list_store.get(store_key, [])
|
|
1052
1054
|
if not tasks:
|
|
1053
|
-
return base_instruction + "\n## 当前状态\n
|
|
1055
|
+
return base_instruction + "\n## 当前状态\n暂无任务计划。如果是简单任务(不超过5步),直接执行即可,无需创建任务列表。如果是复杂任务(超过5步),请先分析用户需求,拆分为具体步骤,然后用 ```tasklist``` 输出计划。"
|
|
1054
1056
|
|
|
1055
1057
|
pending = [f" - ⏳ {t['text']}" for t in tasks if t.get("status") in ("pending", "running", "blocked")]
|
|
1056
1058
|
done = [f" - ✅ {t['text']}" for t in tasks if t.get("status") == "done"]
|
package/web/ui/chat/chat_main.js
CHANGED
|
@@ -345,15 +345,10 @@ function initChat() {
|
|
|
345
345
|
document.getElementById('sendBtn').disabled = !this.value.trim();
|
|
346
346
|
saveDraft();
|
|
347
347
|
});
|
|
348
|
-
// Load task plan if in exec mode (
|
|
348
|
+
// Load task plan if in exec mode (panel stays hidden until tasks exist)
|
|
349
349
|
if (state.chatMode === 'exec') {
|
|
350
|
-
|
|
351
|
-
//
|
|
352
|
-
var taskBody = document.getElementById('taskBody');
|
|
353
|
-
var taskToggle = document.getElementById('taskToggle');
|
|
354
|
-
if (taskBody) taskBody.classList.remove('expanded');
|
|
355
|
-
if (taskToggle) taskToggle.classList.remove('expanded');
|
|
356
|
-
state.taskPanelExpanded = false;
|
|
350
|
+
// Don't show task panel by default - it will appear when the LLM creates a task list
|
|
351
|
+
// document.getElementById('taskPanel').classList.remove('hidden');
|
|
357
352
|
loadTaskPlan();
|
|
358
353
|
}
|
|
359
354
|
|
|
@@ -536,7 +531,10 @@ function setMode(mode) {
|
|
|
536
531
|
} else {
|
|
537
532
|
chatBtn.className = 'mode-btn';
|
|
538
533
|
execBtn.className = 'mode-btn active-exec';
|
|
539
|
-
|
|
534
|
+
// Only show task panel if tasks already exist (don't show empty panel)
|
|
535
|
+
if (state.taskItems.length > 0) {
|
|
536
|
+
taskPanel.classList.remove('hidden');
|
|
537
|
+
}
|
|
540
538
|
// Default collapsed when switching to exec mode
|
|
541
539
|
var tb = document.getElementById('taskBody');
|
|
542
540
|
var tt = document.getElementById('taskToggle');
|
|
@@ -642,7 +640,7 @@ async function loadTaskPlan() {
|
|
|
642
640
|
var data = await api(url);
|
|
643
641
|
state.taskItems = data.tasks || [];
|
|
644
642
|
renderTaskList();
|
|
645
|
-
//
|
|
643
|
+
// Show/hide task panel based on whether tasks exist
|
|
646
644
|
const panel = document.getElementById('taskPanel');
|
|
647
645
|
if (panel && state.taskItems.length > 0) {
|
|
648
646
|
panel.classList.remove('hidden');
|
|
@@ -651,6 +649,9 @@ async function loadTaskPlan() {
|
|
|
651
649
|
triggerTaskAutoFade();
|
|
652
650
|
}
|
|
653
651
|
// Don't auto-expand the body - user can click to expand manually
|
|
652
|
+
} else if (panel) {
|
|
653
|
+
// Hide panel when no tasks exist
|
|
654
|
+
panel.classList.add('hidden');
|
|
654
655
|
}
|
|
655
656
|
} catch (e) {
|
|
656
657
|
state.taskItems = [];
|