@elizaos/python 2.0.0-alpha.10

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 (197) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +239 -0
  3. package/elizaos/__init__.py +280 -0
  4. package/elizaos/action_docs.py +149 -0
  5. package/elizaos/advanced_capabilities/__init__.py +85 -0
  6. package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
  7. package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
  8. package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
  9. package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
  10. package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
  11. package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
  12. package/elizaos/advanced_capabilities/actions/roles.py +207 -0
  13. package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
  14. package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
  15. package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
  16. package/elizaos/advanced_capabilities/actions/settings.py +151 -0
  17. package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
  18. package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
  19. package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
  20. package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
  21. package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
  22. package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
  23. package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
  24. package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
  25. package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
  26. package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
  27. package/elizaos/advanced_capabilities/providers/facts.py +82 -0
  28. package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
  29. package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
  30. package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
  31. package/elizaos/advanced_capabilities/providers/roles.py +97 -0
  32. package/elizaos/advanced_capabilities/providers/settings.py +51 -0
  33. package/elizaos/advanced_capabilities/services/__init__.py +18 -0
  34. package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
  35. package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
  36. package/elizaos/advanced_memory/__init__.py +3 -0
  37. package/elizaos/advanced_memory/evaluators.py +97 -0
  38. package/elizaos/advanced_memory/memory_service.py +556 -0
  39. package/elizaos/advanced_memory/plugin.py +30 -0
  40. package/elizaos/advanced_memory/prompts.py +12 -0
  41. package/elizaos/advanced_memory/providers.py +90 -0
  42. package/elizaos/advanced_memory/types.py +65 -0
  43. package/elizaos/advanced_planning/__init__.py +10 -0
  44. package/elizaos/advanced_planning/actions.py +145 -0
  45. package/elizaos/advanced_planning/message_classifier.py +127 -0
  46. package/elizaos/advanced_planning/planning_service.py +712 -0
  47. package/elizaos/advanced_planning/plugin.py +40 -0
  48. package/elizaos/advanced_planning/prompts.py +4 -0
  49. package/elizaos/basic_capabilities/__init__.py +66 -0
  50. package/elizaos/basic_capabilities/actions/__init__.py +24 -0
  51. package/elizaos/basic_capabilities/actions/choice.py +140 -0
  52. package/elizaos/basic_capabilities/actions/ignore.py +66 -0
  53. package/elizaos/basic_capabilities/actions/none.py +56 -0
  54. package/elizaos/basic_capabilities/actions/reply.py +120 -0
  55. package/elizaos/basic_capabilities/providers/__init__.py +54 -0
  56. package/elizaos/basic_capabilities/providers/action_state.py +113 -0
  57. package/elizaos/basic_capabilities/providers/actions.py +263 -0
  58. package/elizaos/basic_capabilities/providers/attachments.py +76 -0
  59. package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
  60. package/elizaos/basic_capabilities/providers/character.py +113 -0
  61. package/elizaos/basic_capabilities/providers/choice.py +73 -0
  62. package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
  63. package/elizaos/basic_capabilities/providers/current_time.py +58 -0
  64. package/elizaos/basic_capabilities/providers/entities.py +99 -0
  65. package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
  66. package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
  67. package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
  68. package/elizaos/basic_capabilities/providers/time.py +45 -0
  69. package/elizaos/basic_capabilities/providers/world.py +93 -0
  70. package/elizaos/basic_capabilities/services/__init__.py +18 -0
  71. package/elizaos/basic_capabilities/services/embedding.py +122 -0
  72. package/elizaos/basic_capabilities/services/task.py +178 -0
  73. package/elizaos/bootstrap/__init__.py +12 -0
  74. package/elizaos/bootstrap/actions/__init__.py +68 -0
  75. package/elizaos/bootstrap/actions/add_contact.py +149 -0
  76. package/elizaos/bootstrap/actions/choice.py +147 -0
  77. package/elizaos/bootstrap/actions/follow_room.py +151 -0
  78. package/elizaos/bootstrap/actions/ignore.py +80 -0
  79. package/elizaos/bootstrap/actions/image_generation.py +135 -0
  80. package/elizaos/bootstrap/actions/mute_room.py +151 -0
  81. package/elizaos/bootstrap/actions/none.py +71 -0
  82. package/elizaos/bootstrap/actions/remove_contact.py +159 -0
  83. package/elizaos/bootstrap/actions/reply.py +140 -0
  84. package/elizaos/bootstrap/actions/roles.py +193 -0
  85. package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
  86. package/elizaos/bootstrap/actions/search_contacts.py +159 -0
  87. package/elizaos/bootstrap/actions/send_message.py +173 -0
  88. package/elizaos/bootstrap/actions/settings.py +165 -0
  89. package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
  90. package/elizaos/bootstrap/actions/unmute_room.py +151 -0
  91. package/elizaos/bootstrap/actions/update_contact.py +178 -0
  92. package/elizaos/bootstrap/actions/update_entity.py +175 -0
  93. package/elizaos/bootstrap/autonomy/__init__.py +18 -0
  94. package/elizaos/bootstrap/autonomy/action.py +197 -0
  95. package/elizaos/bootstrap/autonomy/providers.py +165 -0
  96. package/elizaos/bootstrap/autonomy/routes.py +171 -0
  97. package/elizaos/bootstrap/autonomy/service.py +562 -0
  98. package/elizaos/bootstrap/autonomy/types.py +18 -0
  99. package/elizaos/bootstrap/evaluators/__init__.py +19 -0
  100. package/elizaos/bootstrap/evaluators/reflection.py +118 -0
  101. package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
  102. package/elizaos/bootstrap/plugin.py +140 -0
  103. package/elizaos/bootstrap/providers/__init__.py +80 -0
  104. package/elizaos/bootstrap/providers/action_state.py +71 -0
  105. package/elizaos/bootstrap/providers/actions.py +256 -0
  106. package/elizaos/bootstrap/providers/agent_settings.py +63 -0
  107. package/elizaos/bootstrap/providers/attachments.py +76 -0
  108. package/elizaos/bootstrap/providers/capabilities.py +66 -0
  109. package/elizaos/bootstrap/providers/character.py +128 -0
  110. package/elizaos/bootstrap/providers/choice.py +77 -0
  111. package/elizaos/bootstrap/providers/contacts.py +78 -0
  112. package/elizaos/bootstrap/providers/context_bench.py +49 -0
  113. package/elizaos/bootstrap/providers/current_time.py +56 -0
  114. package/elizaos/bootstrap/providers/entities.py +99 -0
  115. package/elizaos/bootstrap/providers/evaluators.py +58 -0
  116. package/elizaos/bootstrap/providers/facts.py +86 -0
  117. package/elizaos/bootstrap/providers/follow_ups.py +116 -0
  118. package/elizaos/bootstrap/providers/knowledge.py +73 -0
  119. package/elizaos/bootstrap/providers/providers_list.py +59 -0
  120. package/elizaos/bootstrap/providers/recent_messages.py +85 -0
  121. package/elizaos/bootstrap/providers/relationships.py +106 -0
  122. package/elizaos/bootstrap/providers/roles.py +95 -0
  123. package/elizaos/bootstrap/providers/settings.py +55 -0
  124. package/elizaos/bootstrap/providers/time.py +45 -0
  125. package/elizaos/bootstrap/providers/world.py +97 -0
  126. package/elizaos/bootstrap/services/__init__.py +26 -0
  127. package/elizaos/bootstrap/services/embedding.py +122 -0
  128. package/elizaos/bootstrap/services/follow_up.py +138 -0
  129. package/elizaos/bootstrap/services/rolodex.py +244 -0
  130. package/elizaos/bootstrap/services/task.py +585 -0
  131. package/elizaos/bootstrap/types.py +54 -0
  132. package/elizaos/bootstrap/utils/__init__.py +7 -0
  133. package/elizaos/bootstrap/utils/xml.py +69 -0
  134. package/elizaos/character.py +149 -0
  135. package/elizaos/logger.py +179 -0
  136. package/elizaos/media/__init__.py +45 -0
  137. package/elizaos/media/mime.py +315 -0
  138. package/elizaos/media/search.py +161 -0
  139. package/elizaos/media/tests/__init__.py +1 -0
  140. package/elizaos/media/tests/test_mime.py +117 -0
  141. package/elizaos/media/tests/test_search.py +156 -0
  142. package/elizaos/plugin.py +191 -0
  143. package/elizaos/prompts.py +1071 -0
  144. package/elizaos/py.typed +0 -0
  145. package/elizaos/runtime.py +2572 -0
  146. package/elizaos/services/__init__.py +49 -0
  147. package/elizaos/services/hook_service.py +511 -0
  148. package/elizaos/services/message_service.py +1248 -0
  149. package/elizaos/settings.py +182 -0
  150. package/elizaos/streaming_context.py +159 -0
  151. package/elizaos/trajectory_context.py +18 -0
  152. package/elizaos/types/__init__.py +512 -0
  153. package/elizaos/types/agent.py +31 -0
  154. package/elizaos/types/components.py +208 -0
  155. package/elizaos/types/database.py +64 -0
  156. package/elizaos/types/environment.py +46 -0
  157. package/elizaos/types/events.py +47 -0
  158. package/elizaos/types/memory.py +45 -0
  159. package/elizaos/types/model.py +393 -0
  160. package/elizaos/types/plugin.py +188 -0
  161. package/elizaos/types/primitives.py +100 -0
  162. package/elizaos/types/runtime.py +460 -0
  163. package/elizaos/types/service.py +113 -0
  164. package/elizaos/types/service_interfaces.py +244 -0
  165. package/elizaos/types/state.py +188 -0
  166. package/elizaos/types/task.py +29 -0
  167. package/elizaos/utils/__init__.py +108 -0
  168. package/elizaos/utils/spec_examples.py +48 -0
  169. package/elizaos/utils/streaming.py +426 -0
  170. package/elizaos_atropos_shared/__init__.py +1 -0
  171. package/elizaos_atropos_shared/canonical_eliza.py +282 -0
  172. package/package.json +19 -0
  173. package/pyproject.toml +143 -0
  174. package/requirements-dev.in +11 -0
  175. package/requirements-dev.lock +134 -0
  176. package/requirements.in +9 -0
  177. package/requirements.lock +64 -0
  178. package/tests/__init__.py +0 -0
  179. package/tests/test_action_parameters.py +154 -0
  180. package/tests/test_actions_provider_examples.py +39 -0
  181. package/tests/test_advanced_memory_behavior.py +96 -0
  182. package/tests/test_advanced_memory_flag.py +30 -0
  183. package/tests/test_advanced_planning_behavior.py +225 -0
  184. package/tests/test_advanced_planning_flag.py +26 -0
  185. package/tests/test_autonomy.py +445 -0
  186. package/tests/test_bootstrap_initialize.py +37 -0
  187. package/tests/test_character.py +163 -0
  188. package/tests/test_character_provider.py +231 -0
  189. package/tests/test_dynamic_prompt_exec.py +561 -0
  190. package/tests/test_logger_redaction.py +43 -0
  191. package/tests/test_plugin.py +117 -0
  192. package/tests/test_runtime.py +422 -0
  193. package/tests/test_salt_production_enforcement.py +22 -0
  194. package/tests/test_settings_crypto.py +118 -0
  195. package/tests/test_streaming.py +295 -0
  196. package/tests/test_types.py +221 -0
  197. package/tests/test_uuid_parity.py +46 -0
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ from elizaos.types import Evaluator, HandlerOptions
4
+
5
+
6
+ async def _summarization_validate(runtime, message, _state=None) -> bool:
7
+ if not message.content or not message.content.text:
8
+ return False
9
+ svc = runtime.get_service("memory")
10
+ if svc is None:
11
+ return False
12
+
13
+ # Best-effort message count (works with adapter; otherwise stays 0)
14
+ count = await runtime.count_memories(message.room_id, unique=False, table_name="messages")
15
+ existing = await svc.get_current_session_summary(message.room_id)
16
+ cfg = svc.get_config()
17
+
18
+ if existing is None:
19
+ return count >= cfg.short_term_summarization_threshold
20
+ return (count - existing.last_message_offset) >= cfg.short_term_summarization_interval
21
+
22
+
23
+ async def _summarization_handler(
24
+ runtime, message, _state=None, _options: HandlerOptions | None = None, *_args
25
+ ):
26
+ svc = runtime.get_service("memory")
27
+ if svc is None:
28
+ return None
29
+ # Pull a window of messages; adapters may vary, so be defensive.
30
+ messages = await runtime.get_memories(
31
+ {"tableName": "messages", "roomId": message.room_id, "count": 200, "unique": False}
32
+ )
33
+ await svc.summarize_from_messages(
34
+ room_id=message.room_id,
35
+ agent_id=runtime.agent_id,
36
+ agent_name=runtime.character.name,
37
+ messages=messages,
38
+ )
39
+ return None
40
+
41
+
42
+ async def _long_term_validate(runtime, message, _state=None) -> bool:
43
+ if not message.content or not message.content.text:
44
+ return False
45
+ if message.entity_id == runtime.agent_id:
46
+ return False
47
+ svc = runtime.get_service("memory")
48
+ if svc is None:
49
+ return False
50
+ cfg = svc.get_config()
51
+ if not cfg.long_term_extraction_enabled:
52
+ return False
53
+ count = await runtime.count_memories(message.room_id, unique=False, table_name="messages")
54
+ return await svc.should_run_extraction(message.entity_id, message.room_id, count)
55
+
56
+
57
+ async def _long_term_handler(
58
+ runtime, message, _state=None, _options: HandlerOptions | None = None, *_args
59
+ ):
60
+ svc = runtime.get_service("memory")
61
+ if svc is None:
62
+ return None
63
+ messages = await runtime.get_memories(
64
+ {"tableName": "messages", "roomId": message.room_id, "count": 50, "unique": False}
65
+ )
66
+ await svc.extract_long_term_from_messages(
67
+ entity_id=message.entity_id,
68
+ room_id=message.room_id,
69
+ agent_id=runtime.agent_id,
70
+ agent_name=runtime.character.name,
71
+ messages=messages,
72
+ )
73
+ # Update checkpoint
74
+ count = await runtime.count_memories(message.room_id, unique=False, table_name="messages")
75
+ await svc.set_last_extraction_checkpoint(message.entity_id, message.room_id, count)
76
+ return None
77
+
78
+
79
+ summarization_evaluator = Evaluator(
80
+ name="MEMORY_SUMMARIZATION",
81
+ description="Automatically summarizes conversations to optimize context usage",
82
+ always_run=True,
83
+ similes=["CONVERSATION_SUMMARY", "CONTEXT_COMPRESSION", "MEMORY_OPTIMIZATION"],
84
+ examples=[],
85
+ validate=_summarization_validate,
86
+ handler=_summarization_handler,
87
+ )
88
+
89
+ long_term_extraction_evaluator = Evaluator(
90
+ name="LONG_TERM_MEMORY_EXTRACTION",
91
+ description="Extracts long-term facts about users from conversations",
92
+ always_run=True,
93
+ similes=["MEMORY_EXTRACTION", "FACT_LEARNING", "USER_PROFILING"],
94
+ examples=[],
95
+ validate=_long_term_validate,
96
+ handler=_long_term_handler,
97
+ )
@@ -0,0 +1,556 @@
1
+ from __future__ import annotations
2
+
3
+ import heapq
4
+ import re
5
+ import time
6
+ from typing import cast
7
+ from uuid import UUID, uuid4
8
+
9
+ from elizaos.types.model import ModelType
10
+ from elizaos.types.primitives import string_to_uuid
11
+ from elizaos.types.service import Service
12
+
13
+ from .prompts import (
14
+ INITIAL_SUMMARIZATION_TEMPLATE,
15
+ LONG_TERM_EXTRACTION_TEMPLATE,
16
+ UPDATE_SUMMARIZATION_TEMPLATE,
17
+ )
18
+ from .types import (
19
+ LongTermMemory,
20
+ LongTermMemoryCategory,
21
+ MemoryConfig,
22
+ MemoryExtraction,
23
+ SessionSummary,
24
+ SummaryResult,
25
+ )
26
+
27
+ _TABLE_SESSION_SUMMARY = "session_summary"
28
+ _TABLE_LONG_TERM_MEMORY = "long_term_memory"
29
+ _GLOBAL_LONG_TERM_ROOM_ID = string_to_uuid("advanced-memory:long-term")
30
+
31
+
32
+ def _parse_summary_xml(xml: str) -> SummaryResult:
33
+ summary_match = re.search(r"<text>([\s\S]*?)</text>", xml)
34
+ topics_match = re.search(r"<topics>([\s\S]*?)</topics>", xml)
35
+ key_points_matches = re.findall(r"<point>([\s\S]*?)</point>", xml)
36
+
37
+ summary = summary_match.group(1).strip() if summary_match else "Summary not available"
38
+ topics = (
39
+ [t.strip() for t in topics_match.group(1).split(",") if t.strip()] if topics_match else []
40
+ )
41
+ key_points = [p.strip() for p in key_points_matches]
42
+ return SummaryResult(summary=summary, topics=topics, key_points=key_points)
43
+
44
+
45
+ def _parse_memory_extraction_xml(xml: str) -> list[MemoryExtraction]:
46
+ pattern = (
47
+ r"<memory>[\s\S]*?"
48
+ r"<category>(.*?)</category>[\s\S]*?"
49
+ r"<content>(.*?)</content>[\s\S]*?"
50
+ r"<confidence>(.*?)</confidence>[\s\S]*?"
51
+ r"</memory>"
52
+ )
53
+ out: list[MemoryExtraction] = []
54
+ for match in re.finditer(pattern, xml):
55
+ category_str = match.group(1).strip()
56
+ content = match.group(2).strip()
57
+ confidence_str = match.group(3).strip()
58
+
59
+ try:
60
+ category = LongTermMemoryCategory(category_str)
61
+ except Exception:
62
+ continue
63
+ try:
64
+ confidence = float(confidence_str)
65
+ except Exception:
66
+ continue
67
+ if content:
68
+ out.append(MemoryExtraction(category=category, content=content, confidence=confidence))
69
+ return out
70
+
71
+
72
+ def _top_k_by_confidence(items: list[LongTermMemory], limit: int) -> list[LongTermMemory]:
73
+ if limit <= 0 or not items:
74
+ return []
75
+ if len(items) <= limit:
76
+ return sorted(items, key=lambda mm: mm.confidence, reverse=True)
77
+ return heapq.nlargest(limit, items, key=lambda mm: mm.confidence)
78
+
79
+
80
+ class MemoryService(Service):
81
+ service_type = "memory"
82
+
83
+ def __init__(self, runtime=None) -> None:
84
+ super().__init__(runtime=runtime)
85
+ self._config: MemoryConfig = MemoryConfig()
86
+ # Fallback storage for runtimes without a DB adapter (tests/benchmarks).
87
+ self._session_summaries: dict[str, SessionSummary] = {}
88
+ self._long_term: dict[str, list[LongTermMemory]] = {}
89
+ self._extraction_checkpoints: dict[str, int] = {}
90
+
91
+ @property
92
+ def capability_description(self) -> str:
93
+ return "Memory management with short-term summarization and long-term persistent facts"
94
+
95
+ @classmethod
96
+ async def start(cls, runtime):
97
+ svc = cls(runtime=runtime)
98
+ # read settings
99
+ settings = runtime.character.settings or {}
100
+ if (v := settings.get("MEMORY_SUMMARIZATION_THRESHOLD")) is not None and isinstance(
101
+ v, (int, float, str)
102
+ ):
103
+ svc._config.short_term_summarization_threshold = int(v)
104
+ if (v := settings.get("MEMORY_RETAIN_RECENT")) is not None and isinstance(
105
+ v, (int, float, str)
106
+ ):
107
+ svc._config.short_term_retain_recent = int(v)
108
+ if (v := settings.get("MEMORY_SUMMARIZATION_INTERVAL")) is not None and isinstance(
109
+ v, (int, float, str)
110
+ ):
111
+ svc._config.short_term_summarization_interval = int(v)
112
+ if (v := settings.get("MEMORY_MAX_NEW_MESSAGES")) is not None and isinstance(
113
+ v, (int, float, str)
114
+ ):
115
+ svc._config.summary_max_new_messages = int(v)
116
+ if (v := settings.get("MEMORY_LONG_TERM_ENABLED")) is not None:
117
+ if str(v).lower() == "false":
118
+ svc._config.long_term_extraction_enabled = False
119
+ elif str(v).lower() == "true":
120
+ svc._config.long_term_extraction_enabled = True
121
+ if (v := settings.get("MEMORY_CONFIDENCE_THRESHOLD")) is not None and isinstance(
122
+ v, (int, float, str)
123
+ ):
124
+ svc._config.long_term_confidence_threshold = float(v)
125
+ if (v := settings.get("MEMORY_EXTRACTION_THRESHOLD")) is not None and isinstance(
126
+ v, (int, float, str)
127
+ ):
128
+ svc._config.long_term_extraction_threshold = int(v)
129
+ if (v := settings.get("MEMORY_EXTRACTION_INTERVAL")) is not None and isinstance(
130
+ v, (int, float, str)
131
+ ):
132
+ svc._config.long_term_extraction_interval = int(v)
133
+
134
+ runtime.logger.info("MemoryService started successfully", src="service:memory")
135
+ return svc
136
+
137
+ async def stop(self) -> None:
138
+ self._session_summaries.clear()
139
+ self._long_term.clear()
140
+ self._extraction_checkpoints.clear()
141
+
142
+ def get_config(self) -> MemoryConfig:
143
+ return MemoryConfig(**self._config.__dict__)
144
+
145
+ def _checkpoint_key(self, entity_id: UUID, room_id: UUID) -> str:
146
+ return f"memory:extraction:{entity_id}:{room_id}"
147
+
148
+ async def get_last_extraction_checkpoint(self, entity_id: UUID, room_id: UUID) -> int:
149
+ runtime = self.runtime
150
+ key = self._checkpoint_key(entity_id, room_id)
151
+ if runtime is not None and getattr(runtime, "_adapter", None) is not None:
152
+ cached = await runtime.get_cache(key)
153
+ if cached is None:
154
+ return 0
155
+ try:
156
+ if isinstance(cached, (int, float, str)):
157
+ return int(cached)
158
+ return 0
159
+ except Exception:
160
+ return 0
161
+ return int(self._extraction_checkpoints.get(key, 0))
162
+
163
+ async def set_last_extraction_checkpoint(
164
+ self, entity_id: UUID, room_id: UUID, message_count: int
165
+ ) -> None:
166
+ runtime = self.runtime
167
+ key = self._checkpoint_key(entity_id, room_id)
168
+ if runtime is not None and getattr(runtime, "_adapter", None) is not None:
169
+ _ = await runtime.set_cache(key, int(message_count))
170
+ return
171
+ self._extraction_checkpoints[key] = int(message_count)
172
+
173
+ async def should_run_extraction(
174
+ self, entity_id: UUID, room_id: UUID, current_message_count: int
175
+ ) -> bool:
176
+ threshold = self._config.long_term_extraction_threshold
177
+ interval = self._config.long_term_extraction_interval
178
+ if current_message_count < threshold:
179
+ return False
180
+ last_cp = await self.get_last_extraction_checkpoint(entity_id, room_id)
181
+ current_cp = (current_message_count // interval) * interval
182
+ return current_cp > last_cp
183
+
184
+ async def get_current_session_summary(self, room_id: UUID) -> SessionSummary | None:
185
+ runtime = self.runtime
186
+ if runtime is None:
187
+ return None
188
+
189
+ # Prefer DB-backed retrieval when available.
190
+ if getattr(runtime, "_adapter", None) is not None:
191
+ # Session summary is stored under the agent entity_id, scoped to the room.
192
+ mems = await runtime.get_memories(
193
+ {
194
+ "roomId": str(room_id),
195
+ "entityId": str(runtime.agent_id),
196
+ "agentId": str(runtime.agent_id),
197
+ "count": 10,
198
+ }
199
+ )
200
+ for m in mems:
201
+ if not isinstance(m, dict):
202
+ continue
203
+ meta = m.get("metadata")
204
+ if not isinstance(meta, dict):
205
+ meta = {}
206
+ if meta.get("type") != _TABLE_SESSION_SUMMARY:
207
+ continue
208
+ try:
209
+ return SessionSummary(
210
+ id=UUID(str(m.get("id"))),
211
+ agent_id=UUID(str(m.get("agentId") or runtime.agent_id)),
212
+ room_id=UUID(str(m.get("roomId") or room_id)),
213
+ entity_id=UUID(str(meta["entityId"])) if meta.get("entityId") else None,
214
+ summary=str((m.get("content") or {}).get("text") or ""),
215
+ message_count=int(meta.get("messageCount") or 0),
216
+ last_message_offset=int(meta.get("lastMessageOffset") or 0),
217
+ topics=[str(t) for t in (meta.get("topics") or [])],
218
+ metadata=dict(meta.get("metadata") or {}),
219
+ )
220
+ except Exception:
221
+ # Best-effort; ignore corrupt rows.
222
+ continue
223
+
224
+ return self._session_summaries.get(str(room_id))
225
+
226
+ async def store_session_summary(
227
+ self,
228
+ agent_id: UUID,
229
+ room_id: UUID,
230
+ summary: str,
231
+ message_count: int,
232
+ last_message_offset: int,
233
+ entity_id: UUID | None = None,
234
+ topics: list[str] | None = None,
235
+ metadata: dict[str, object] | None = None,
236
+ ) -> SessionSummary:
237
+ runtime = self.runtime
238
+ s = SessionSummary(
239
+ id=uuid4(),
240
+ agent_id=agent_id,
241
+ room_id=room_id,
242
+ entity_id=entity_id,
243
+ summary=summary,
244
+ message_count=message_count,
245
+ last_message_offset=last_message_offset,
246
+ topics=topics or [],
247
+ metadata=metadata or {},
248
+ )
249
+
250
+ if runtime is not None and getattr(runtime, "_adapter", None) is not None:
251
+ existing = await self.get_current_session_summary(room_id)
252
+ mem = {
253
+ "id": str(existing.id) if existing else str(s.id),
254
+ "entityId": str(runtime.agent_id),
255
+ "agentId": str(runtime.agent_id),
256
+ "roomId": str(room_id),
257
+ "worldId": None,
258
+ "content": {"text": summary},
259
+ "metadata": {
260
+ "type": _TABLE_SESSION_SUMMARY,
261
+ "messageCount": int(message_count),
262
+ "lastMessageOffset": int(last_message_offset),
263
+ "topics": list(s.topics),
264
+ "entityId": str(entity_id) if entity_id else None,
265
+ "metadata": dict(s.metadata),
266
+ },
267
+ }
268
+ if existing:
269
+ _ = await runtime.update_memory(mem)
270
+ return s
271
+ _ = await runtime.create_memory(
272
+ cast(dict[str, object], mem), _TABLE_SESSION_SUMMARY, unique=False
273
+ )
274
+ return s
275
+
276
+ self._session_summaries[str(room_id)] = s
277
+ return s
278
+
279
+ async def update_session_summary(
280
+ self, summary_id: UUID, room_id: UUID, **updates: object
281
+ ) -> None:
282
+ runtime = self.runtime
283
+ if runtime is not None and getattr(runtime, "_adapter", None) is not None:
284
+ existing = await self.get_current_session_summary(room_id)
285
+ if not existing or existing.id != summary_id:
286
+ return
287
+ # Reuse store_session_summary to persist updated state.
288
+ summary_text = (
289
+ str(updates["summary"])
290
+ if "summary" in updates and isinstance(updates["summary"], str)
291
+ else existing.summary
292
+ )
293
+ msg_count_raw = updates.get("message_count")
294
+ msg_count = (
295
+ int(msg_count_raw)
296
+ if isinstance(msg_count_raw, (int, float, str))
297
+ else existing.message_count
298
+ )
299
+ last_off_raw = updates.get("last_message_offset")
300
+ last_off = (
301
+ int(last_off_raw)
302
+ if isinstance(last_off_raw, (int, float, str))
303
+ else existing.last_message_offset
304
+ )
305
+ topics = (
306
+ [str(t) for t in updates["topics"]]
307
+ if "topics" in updates and isinstance(updates["topics"], list)
308
+ else existing.topics
309
+ )
310
+ meta = (
311
+ {str(k): v for k, v in updates["metadata"].items()}
312
+ if "metadata" in updates and isinstance(updates["metadata"], dict)
313
+ else existing.metadata
314
+ )
315
+ await self.store_session_summary(
316
+ agent_id=existing.agent_id,
317
+ room_id=existing.room_id,
318
+ summary=summary_text,
319
+ message_count=msg_count,
320
+ last_message_offset=last_off,
321
+ entity_id=existing.entity_id,
322
+ topics=topics,
323
+ metadata=meta,
324
+ )
325
+ return
326
+
327
+ existing = self._session_summaries.get(str(room_id))
328
+ if not existing or existing.id != summary_id:
329
+ return
330
+ if "summary" in updates and isinstance(updates["summary"], str):
331
+ existing.summary = updates["summary"]
332
+ if "message_count" in updates and isinstance(updates["message_count"], (int, float, str)):
333
+ existing.message_count = int(updates["message_count"])
334
+ if "last_message_offset" in updates and isinstance(
335
+ updates["last_message_offset"], (int, float, str)
336
+ ):
337
+ existing.last_message_offset = int(updates["last_message_offset"])
338
+ if "topics" in updates and isinstance(updates["topics"], list):
339
+ existing.topics = [str(t) for t in updates["topics"]]
340
+ if "metadata" in updates and isinstance(updates["metadata"], dict):
341
+ existing.metadata = {str(k): v for k, v in updates["metadata"].items()}
342
+
343
+ async def store_long_term_memory(
344
+ self,
345
+ agent_id: UUID,
346
+ entity_id: UUID,
347
+ category: LongTermMemoryCategory,
348
+ content: str,
349
+ confidence: float = 1.0,
350
+ source: str | None = None,
351
+ metadata: dict[str, object] | None = None,
352
+ ) -> LongTermMemory:
353
+ runtime = self.runtime
354
+ m = LongTermMemory(
355
+ id=uuid4(),
356
+ agent_id=agent_id,
357
+ entity_id=entity_id,
358
+ category=category,
359
+ content=content,
360
+ confidence=float(confidence),
361
+ source=source,
362
+ metadata=metadata or {},
363
+ )
364
+
365
+ if runtime is not None and getattr(runtime, "_adapter", None) is not None:
366
+ mem = {
367
+ "id": str(m.id),
368
+ "entityId": str(entity_id),
369
+ "agentId": str(runtime.agent_id),
370
+ "roomId": str(_GLOBAL_LONG_TERM_ROOM_ID),
371
+ "worldId": None,
372
+ "content": {"text": content},
373
+ "metadata": {
374
+ "type": _TABLE_LONG_TERM_MEMORY,
375
+ "category": category.value,
376
+ "confidence": float(m.confidence),
377
+ "source": m.source,
378
+ "metadata": dict(m.metadata),
379
+ },
380
+ }
381
+ _ = await runtime.create_memory(
382
+ cast(dict[str, object], mem), _TABLE_LONG_TERM_MEMORY, unique=False
383
+ )
384
+ return m
385
+
386
+ self._long_term.setdefault(str(entity_id), []).append(m)
387
+ return m
388
+
389
+ async def get_long_term_memories(
390
+ self,
391
+ entity_id: UUID,
392
+ category: LongTermMemoryCategory | None = None,
393
+ limit: int = 25,
394
+ ) -> list[LongTermMemory]:
395
+ if limit <= 0:
396
+ return []
397
+ runtime = self.runtime
398
+ if runtime is not None and getattr(runtime, "_adapter", None) is not None:
399
+ db_mems = await runtime.get_memories(
400
+ {
401
+ "roomId": str(_GLOBAL_LONG_TERM_ROOM_ID),
402
+ "entityId": str(entity_id),
403
+ "agentId": str(runtime.agent_id),
404
+ "count": 200,
405
+ }
406
+ )
407
+ out: list[LongTermMemory] = []
408
+ for m in db_mems:
409
+ if not isinstance(m, dict):
410
+ continue
411
+ meta = m.get("metadata")
412
+ if not isinstance(meta, dict):
413
+ meta = {}
414
+ if meta.get("type") != _TABLE_LONG_TERM_MEMORY:
415
+ continue
416
+ cat_raw = str(meta.get("category") or "")
417
+ try:
418
+ cat = LongTermMemoryCategory(cat_raw)
419
+ except Exception:
420
+ continue
421
+ if category is not None and cat != category:
422
+ continue
423
+ try:
424
+ out.append(
425
+ LongTermMemory(
426
+ id=UUID(str(m.get("id"))),
427
+ agent_id=UUID(str(m.get("agentId") or runtime.agent_id)),
428
+ entity_id=UUID(str(m.get("entityId") or entity_id)),
429
+ category=cat,
430
+ content=str((m.get("content") or {}).get("text") or ""),
431
+ confidence=float(meta.get("confidence") or 1.0),
432
+ source=str(meta.get("source"))
433
+ if meta.get("source") is not None
434
+ else None,
435
+ metadata=dict(meta.get("metadata") or {}),
436
+ )
437
+ )
438
+ except Exception:
439
+ continue
440
+ return _top_k_by_confidence(out, limit)
441
+
442
+ local_mems = self._long_term.get(str(entity_id), [])
443
+ if category is not None:
444
+ local_mems = [m for m in local_mems if m.category == category]
445
+ return _top_k_by_confidence(local_mems, limit)
446
+
447
+ async def get_formatted_long_term_memories(self, entity_id: UUID) -> str:
448
+ mems = await self.get_long_term_memories(entity_id, None, 20)
449
+ if not mems:
450
+ return ""
451
+ grouped: dict[LongTermMemoryCategory, list[LongTermMemory]] = {}
452
+ for m in mems:
453
+ grouped.setdefault(m.category, []).append(m)
454
+ sections: list[str] = []
455
+ for cat, items in grouped.items():
456
+ name = cat.value.replace("_", " ").title()
457
+ sections.append(f"**{name}**:\n" + "\n".join(f"- {x.content}" for x in items))
458
+ return "\n\n".join(sections)
459
+
460
+ async def summarize_from_messages(
461
+ self, room_id: UUID, agent_id: UUID, agent_name: str, messages: list[object]
462
+ ) -> None:
463
+ # `messages` is a list of elizaos Memory objects; we only use common fields.
464
+ dialogue = []
465
+ for m in messages:
466
+ content = getattr(m, "content", None)
467
+ text = getattr(content, "text", None)
468
+ if not text:
469
+ continue
470
+ sender = agent_name if getattr(m, "entity_id", None) == agent_id else "User"
471
+ dialogue.append(f"{sender}: {text}")
472
+ if not dialogue:
473
+ return
474
+
475
+ existing = await self.get_current_session_summary(room_id)
476
+ if existing:
477
+ prompt = UPDATE_SUMMARIZATION_TEMPLATE.format(
478
+ existing_summary=existing.summary,
479
+ existing_topics=", ".join(existing.topics) if existing.topics else "None",
480
+ new_messages="\n".join(dialogue[-self._config.summary_max_new_messages :]),
481
+ )
482
+ else:
483
+ prompt = INITIAL_SUMMARIZATION_TEMPLATE.format(recent_messages="\n".join(dialogue))
484
+
485
+ response = await self.runtime.use_model(
486
+ ModelType.TEXT_LARGE,
487
+ {"prompt": prompt, "temperature": 0.2, "maxTokens": self._config.summary_max_tokens},
488
+ )
489
+ parsed = _parse_summary_xml(str(response))
490
+ if existing:
491
+ await self.update_session_summary(
492
+ existing.id,
493
+ room_id,
494
+ summary=parsed.summary,
495
+ message_count=existing.message_count + len(dialogue),
496
+ last_message_offset=existing.last_message_offset + len(dialogue),
497
+ topics=parsed.topics,
498
+ metadata={"keyPoints": parsed.key_points},
499
+ )
500
+ else:
501
+ await self.store_session_summary(
502
+ agent_id=agent_id,
503
+ room_id=room_id,
504
+ entity_id=None,
505
+ summary=parsed.summary,
506
+ message_count=len(dialogue),
507
+ last_message_offset=len(dialogue),
508
+ topics=parsed.topics,
509
+ metadata={"keyPoints": parsed.key_points},
510
+ )
511
+
512
+ async def extract_long_term_from_messages(
513
+ self,
514
+ entity_id: UUID,
515
+ room_id: UUID,
516
+ agent_id: UUID,
517
+ agent_name: str,
518
+ messages: list[object],
519
+ ) -> None:
520
+ if not self._config.long_term_extraction_enabled:
521
+ return
522
+ formatted = []
523
+ for m in messages:
524
+ content = getattr(m, "content", None)
525
+ text = getattr(content, "text", None)
526
+ if not text:
527
+ continue
528
+ sender = agent_name if getattr(m, "entity_id", None) == agent_id else "User"
529
+ formatted.append(f"{sender}: {text}")
530
+ existing = await self.get_long_term_memories(entity_id, None, 30)
531
+ existing_text = (
532
+ "\n".join(
533
+ f"[{m.category.value}] {m.content} (confidence: {m.confidence})" for m in existing
534
+ )
535
+ if existing
536
+ else "None yet"
537
+ )
538
+ prompt = LONG_TERM_EXTRACTION_TEMPLATE.format(
539
+ recent_messages="\n".join(formatted[-20:]),
540
+ existing_memories=existing_text,
541
+ )
542
+ response = await self.runtime.use_model(
543
+ ModelType.TEXT_LARGE, {"prompt": prompt, "temperature": 0.3, "maxTokens": 2000}
544
+ )
545
+ extractions = _parse_memory_extraction_xml(str(response))
546
+ for ex in extractions:
547
+ if ex.confidence >= max(self._config.long_term_confidence_threshold, 0.85):
548
+ await self.store_long_term_memory(
549
+ agent_id=agent_id,
550
+ entity_id=entity_id,
551
+ category=ex.category,
552
+ content=ex.content,
553
+ confidence=ex.confidence,
554
+ source="conversation",
555
+ metadata={"roomId": str(room_id), "extractedAt": int(time.time() * 1000)},
556
+ )
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from elizaos.types import Plugin
4
+
5
+ from .evaluators import long_term_extraction_evaluator, summarization_evaluator
6
+ from .memory_service import MemoryService
7
+ from .providers import context_summary_provider, long_term_memory_provider
8
+
9
+
10
+ def create_advanced_memory_plugin() -> Plugin:
11
+ async def init_plugin(_config, runtime) -> None:
12
+ runtime.logger.info(
13
+ "Advanced memory enabled",
14
+ src="plugin:advanced-memory",
15
+ agentId=str(runtime.agent_id),
16
+ )
17
+
18
+ return Plugin(
19
+ name="memory",
20
+ description="Built-in advanced memory (summaries + long-term facts)",
21
+ init=init_plugin,
22
+ config={},
23
+ services=[MemoryService],
24
+ actions=[],
25
+ providers=[long_term_memory_provider, context_summary_provider],
26
+ evaluators=[summarization_evaluator, long_term_extraction_evaluator],
27
+ )
28
+
29
+
30
+ advanced_memory_plugin = create_advanced_memory_plugin()
@@ -0,0 +1,12 @@
1
+ # Re-export from centralized prompts
2
+ from elizaos.prompts import (
3
+ INITIAL_SUMMARIZATION_TEMPLATE,
4
+ LONG_TERM_EXTRACTION_TEMPLATE,
5
+ UPDATE_SUMMARIZATION_TEMPLATE,
6
+ )
7
+
8
+ __all__ = [
9
+ "INITIAL_SUMMARIZATION_TEMPLATE",
10
+ "UPDATE_SUMMARIZATION_TEMPLATE",
11
+ "LONG_TERM_EXTRACTION_TEMPLATE",
12
+ ]