myagent-ai 1.13.3 → 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/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
- semantic_results = self._search_semantic(conn, query, where, params, limit * 2)
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
- score = 1.0 - (i / max(len(keyword_results), 1))
802
- combined[entry.id] = (entry, score * 0.4)
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
- score = 1.0 - (i / max(len(semantic_results), 1))
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] + score * 0.6)
879
+ combined[entry.id] = (entry, combined[entry.id][1] + sem_score)
808
880
  else:
809
- combined[entry.id] = (entry, score * 0.6)
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(limit)
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
- (row["id"],),
947
+ (entry.id,),
853
948
  )
854
949
  conn.commit()
855
950
 
856
- return [MemoryEntry.from_row(row) for row in rows]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagent-ai",
3
- "version": "1.13.3",
3
+ "version": "1.13.4",
4
4
  "description": "本地桌面端执行型AI助手 - Open Interpreter 风格 | Local Desktop Execution-Oriented AI Assistant",
5
5
  "main": "main.py",
6
6
  "bin": {