@elizaos/python 2.0.0-alpha.3 → 2.0.0-alpha.31

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 (167) hide show
  1. package/elizaos/__init__.py +0 -1
  2. package/elizaos/advanced_capabilities/__init__.py +6 -41
  3. package/elizaos/advanced_capabilities/actions/__init__.py +1 -21
  4. package/elizaos/advanced_capabilities/actions/add_contact.py +24 -13
  5. package/elizaos/advanced_capabilities/actions/follow_room.py +29 -29
  6. package/elizaos/advanced_capabilities/actions/image_generation.py +15 -28
  7. package/elizaos/advanced_capabilities/actions/mute_room.py +15 -28
  8. package/elizaos/advanced_capabilities/actions/remove_contact.py +17 -3
  9. package/elizaos/advanced_capabilities/actions/roles.py +17 -30
  10. package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +70 -15
  11. package/elizaos/advanced_capabilities/actions/search_contacts.py +17 -3
  12. package/elizaos/advanced_capabilities/actions/send_message.py +184 -51
  13. package/elizaos/advanced_capabilities/actions/settings.py +17 -3
  14. package/elizaos/advanced_capabilities/actions/unfollow_room.py +15 -28
  15. package/elizaos/advanced_capabilities/actions/unmute_room.py +15 -28
  16. package/elizaos/advanced_capabilities/actions/update_contact.py +17 -3
  17. package/elizaos/advanced_capabilities/actions/update_entity.py +17 -3
  18. package/elizaos/advanced_capabilities/evaluators/__init__.py +2 -9
  19. package/elizaos/advanced_capabilities/evaluators/reflection.py +3 -132
  20. package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +5 -201
  21. package/elizaos/advanced_capabilities/providers/__init__.py +1 -12
  22. package/elizaos/advanced_capabilities/providers/knowledge.py +23 -3
  23. package/elizaos/advanced_capabilities/services/__init__.py +2 -9
  24. package/elizaos/advanced_capabilities/services/rolodex.py +2 -2
  25. package/elizaos/advanced_memory/actions/reset_session.py +143 -0
  26. package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
  27. package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
  28. package/elizaos/advanced_memory/memory_service.py +69 -27
  29. package/elizaos/advanced_memory/plugin.py +2 -1
  30. package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
  31. package/elizaos/advanced_memory/types.py +2 -2
  32. package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
  33. package/elizaos/advanced_planning/planning_service.py +26 -14
  34. package/elizaos/basic_capabilities/__init__.py +0 -2
  35. package/elizaos/basic_capabilities/providers/__init__.py +0 -3
  36. package/elizaos/basic_capabilities/providers/actions.py +118 -29
  37. package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
  38. package/elizaos/basic_capabilities/providers/character.py +19 -21
  39. package/elizaos/basic_capabilities/providers/contacts.py +79 -0
  40. package/elizaos/basic_capabilities/providers/current_time.py +7 -4
  41. package/elizaos/basic_capabilities/providers/facts.py +87 -0
  42. package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
  43. package/elizaos/basic_capabilities/providers/knowledge.py +96 -0
  44. package/elizaos/basic_capabilities/providers/recent_messages.py +5 -0
  45. package/elizaos/basic_capabilities/providers/relationships.py +113 -0
  46. package/elizaos/basic_capabilities/providers/roles.py +96 -0
  47. package/elizaos/basic_capabilities/providers/settings.py +56 -0
  48. package/elizaos/basic_capabilities/providers/time.py +7 -4
  49. package/elizaos/basic_capabilities/services/embedding.py +10 -7
  50. package/elizaos/basic_capabilities/services/task.py +3 -3
  51. package/elizaos/bootstrap/__init__.py +21 -2
  52. package/elizaos/bootstrap/actions/__init__.py +3 -0
  53. package/elizaos/bootstrap/actions/reset_session.py +3 -0
  54. package/elizaos/bootstrap/actions/roles.py +5 -4
  55. package/elizaos/bootstrap/actions/schedule_follow_up.py +65 -7
  56. package/elizaos/bootstrap/actions/send_message.py +162 -15
  57. package/elizaos/bootstrap/autonomy/__init__.py +5 -1
  58. package/elizaos/bootstrap/autonomy/action.py +161 -0
  59. package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
  60. package/elizaos/bootstrap/autonomy/service.py +238 -28
  61. package/elizaos/bootstrap/plugin.py +7 -0
  62. package/elizaos/bootstrap/providers/actions.py +118 -27
  63. package/elizaos/bootstrap/providers/agent_settings.py +1 -0
  64. package/elizaos/bootstrap/providers/attachments.py +1 -0
  65. package/elizaos/bootstrap/providers/capabilities.py +1 -0
  66. package/elizaos/bootstrap/providers/character.py +1 -0
  67. package/elizaos/bootstrap/providers/choice.py +1 -0
  68. package/elizaos/bootstrap/providers/contacts.py +1 -0
  69. package/elizaos/bootstrap/providers/current_time.py +8 -2
  70. package/elizaos/bootstrap/providers/entities.py +1 -0
  71. package/elizaos/bootstrap/providers/evaluators.py +1 -0
  72. package/elizaos/bootstrap/providers/facts.py +1 -0
  73. package/elizaos/bootstrap/providers/follow_ups.py +1 -0
  74. package/elizaos/bootstrap/providers/knowledge.py +26 -3
  75. package/elizaos/bootstrap/providers/providers_list.py +1 -0
  76. package/elizaos/bootstrap/providers/recent_messages.py +5 -0
  77. package/elizaos/bootstrap/providers/relationships.py +20 -13
  78. package/elizaos/bootstrap/providers/roles.py +1 -0
  79. package/elizaos/bootstrap/providers/settings.py +1 -0
  80. package/elizaos/bootstrap/providers/time.py +8 -4
  81. package/elizaos/bootstrap/providers/world.py +1 -0
  82. package/elizaos/bootstrap/services/embedding.py +206 -8
  83. package/elizaos/bootstrap/services/rolodex.py +2 -2
  84. package/elizaos/bootstrap/services/task.py +3 -3
  85. package/elizaos/deterministic.py +193 -0
  86. package/elizaos/generated/__init__.py +1 -0
  87. package/elizaos/generated/action_docs.py +3181 -0
  88. package/elizaos/generated/spec_helpers.py +175 -0
  89. package/elizaos/media/mime.py +4 -4
  90. package/elizaos/media/search.py +23 -23
  91. package/elizaos/runtime.py +223 -64
  92. package/elizaos/services/hook_service.py +3 -3
  93. package/elizaos/services/message_service.py +175 -29
  94. package/elizaos/types/components.py +2 -2
  95. package/elizaos/types/generated/__init__.py +12 -0
  96. package/elizaos/types/generated/eliza/v1/agent_pb2.py +63 -0
  97. package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +159 -0
  98. package/elizaos/types/generated/eliza/v1/components_pb2.py +65 -0
  99. package/elizaos/types/generated/eliza/v1/components_pb2.pyi +160 -0
  100. package/elizaos/types/generated/eliza/v1/database_pb2.py +78 -0
  101. package/elizaos/types/generated/eliza/v1/database_pb2.pyi +305 -0
  102. package/elizaos/types/generated/eliza/v1/environment_pb2.py +58 -0
  103. package/elizaos/types/generated/eliza/v1/environment_pb2.pyi +135 -0
  104. package/elizaos/types/generated/eliza/v1/events_pb2.py +82 -0
  105. package/elizaos/types/generated/eliza/v1/events_pb2.pyi +322 -0
  106. package/elizaos/types/generated/eliza/v1/ipc_pb2.py +113 -0
  107. package/elizaos/types/generated/eliza/v1/ipc_pb2.pyi +367 -0
  108. package/elizaos/types/generated/eliza/v1/knowledge_pb2.py +41 -0
  109. package/elizaos/types/generated/eliza/v1/knowledge_pb2.pyi +26 -0
  110. package/elizaos/types/generated/eliza/v1/memory_pb2.py +55 -0
  111. package/elizaos/types/generated/eliza/v1/memory_pb2.pyi +111 -0
  112. package/elizaos/types/generated/eliza/v1/message_service_pb2.py +48 -0
  113. package/elizaos/types/generated/eliza/v1/message_service_pb2.pyi +69 -0
  114. package/elizaos/types/generated/eliza/v1/messaging_pb2.py +51 -0
  115. package/elizaos/types/generated/eliza/v1/messaging_pb2.pyi +97 -0
  116. package/elizaos/types/generated/eliza/v1/model_pb2.py +84 -0
  117. package/elizaos/types/generated/eliza/v1/model_pb2.pyi +280 -0
  118. package/elizaos/types/generated/eliza/v1/payment_pb2.py +44 -0
  119. package/elizaos/types/generated/eliza/v1/payment_pb2.pyi +70 -0
  120. package/elizaos/types/generated/eliza/v1/plugin_pb2.py +68 -0
  121. package/elizaos/types/generated/eliza/v1/plugin_pb2.pyi +145 -0
  122. package/elizaos/types/generated/eliza/v1/primitives_pb2.py +48 -0
  123. package/elizaos/types/generated/eliza/v1/primitives_pb2.pyi +92 -0
  124. package/elizaos/types/generated/eliza/v1/prompts_pb2.py +52 -0
  125. package/elizaos/types/generated/eliza/v1/prompts_pb2.pyi +74 -0
  126. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.py +211 -0
  127. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.pyi +1296 -0
  128. package/elizaos/types/generated/eliza/v1/service_pb2.py +42 -0
  129. package/elizaos/types/generated/eliza/v1/service_pb2.pyi +69 -0
  130. package/elizaos/types/generated/eliza/v1/settings_pb2.py +58 -0
  131. package/elizaos/types/generated/eliza/v1/settings_pb2.pyi +85 -0
  132. package/elizaos/types/generated/eliza/v1/state_pb2.py +60 -0
  133. package/elizaos/types/generated/eliza/v1/state_pb2.pyi +114 -0
  134. package/elizaos/types/generated/eliza/v1/task_pb2.py +42 -0
  135. package/elizaos/types/generated/eliza/v1/task_pb2.pyi +58 -0
  136. package/elizaos/types/generated/eliza/v1/tee_pb2.py +52 -0
  137. package/elizaos/types/generated/eliza/v1/tee_pb2.pyi +90 -0
  138. package/elizaos/types/generated/eliza/v1/testing_pb2.py +39 -0
  139. package/elizaos/types/generated/eliza/v1/testing_pb2.pyi +23 -0
  140. package/elizaos/types/model.py +33 -3
  141. package/elizaos/types/primitives.py +3 -3
  142. package/elizaos/types/runtime.py +17 -3
  143. package/elizaos/types/state.py +2 -2
  144. package/elizaos/utils/streaming.py +3 -3
  145. package/elizaos/utils/validation.py +76 -0
  146. package/package.json +4 -3
  147. package/pyproject.toml +1 -2
  148. package/requirements-dev.lock +2 -2
  149. package/requirements.in +1 -2
  150. package/requirements.lock +2 -2
  151. package/tests/test_action_parameters.py +2 -3
  152. package/tests/test_actions_provider_examples.py +58 -1
  153. package/tests/test_advanced_memory_behavior.py +0 -2
  154. package/tests/test_advanced_memory_flag.py +0 -2
  155. package/tests/test_advanced_planning_behavior.py +11 -5
  156. package/tests/test_async_embedding.py +124 -0
  157. package/tests/test_autonomy.py +24 -3
  158. package/tests/test_history_compaction.py +104 -0
  159. package/tests/test_memory_bounds.py +115 -0
  160. package/tests/test_runtime.py +8 -17
  161. package/tests/test_schedule_follow_up_action.py +260 -0
  162. package/tests/test_send_message_action_targets.py +114 -0
  163. package/tests/test_settings_crypto.py +0 -2
  164. package/tests/test_validation.py +141 -0
  165. package/tests/verify_memory_architecture.py +192 -0
  166. package/uv.lock +1565 -0
  167. package/elizaos/basic_capabilities/providers/capabilities.py +0 -62
@@ -1,8 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ import asyncio
4
+ import contextlib
5
+ from collections import OrderedDict
6
+ from typing import TYPE_CHECKING, Any
4
7
 
5
8
  from elizaos.types import ModelType, Service, ServiceType
9
+ from elizaos.types.events import EventType
6
10
 
7
11
  if TYPE_CHECKING:
8
12
  from elizaos.types import IAgentRuntime
@@ -18,14 +22,28 @@ class EmbeddingService(Service):
18
22
 
19
23
  def __init__(self) -> None:
20
24
  self._runtime: IAgentRuntime | None = None
21
- self._cache: dict[str, list[float]] = {}
25
+ self._cache: OrderedDict[str, list[float]] = OrderedDict()
22
26
  self._cache_enabled: bool = True
23
27
  self._max_cache_size: int = 1000
28
+ self._queue_max_size: int = 1000
29
+ self._queue: asyncio.Queue[tuple[str | None, Any]] = asyncio.Queue(
30
+ maxsize=self._queue_max_size
31
+ )
32
+ self._pending_payload_keys: set[str] = set()
33
+ self._worker_task: asyncio.Task | None = None
24
34
 
25
35
  @classmethod
26
36
  async def start(cls, runtime: IAgentRuntime) -> EmbeddingService:
27
37
  service = cls()
28
38
  service._runtime = runtime
39
+
40
+ # Register event handler
41
+ event_name = EventType.Name(EventType.EVENT_TYPE_EMBEDDING_GENERATION_REQUESTED)
42
+ runtime.register_event(event_name, service._handle_embedding_request)
43
+
44
+ # Start worker
45
+ service._worker_task = asyncio.create_task(service._worker())
46
+
29
47
  runtime.logger.info(
30
48
  "Embedding service started",
31
49
  src="service:embedding",
@@ -34,6 +52,12 @@ class EmbeddingService(Service):
34
52
  return service
35
53
 
36
54
  async def stop(self) -> None:
55
+ if self._worker_task:
56
+ self._worker_task.cancel()
57
+ with contextlib.suppress(asyncio.CancelledError):
58
+ await self._worker_task
59
+ self._worker_task = None
60
+
37
61
  if self._runtime:
38
62
  self._runtime.logger.info(
39
63
  "Embedding service stopped",
@@ -41,6 +65,8 @@ class EmbeddingService(Service):
41
65
  agentId=str(self._runtime.agent_id),
42
66
  )
43
67
  self._cache.clear()
68
+ self._pending_payload_keys.clear()
69
+ self._queue = asyncio.Queue(maxsize=self._queue_max_size)
44
70
  self._runtime = None
45
71
 
46
72
  # Max characters for embedding input (~8K tokens at ~4 chars/token)
@@ -51,7 +77,9 @@ class EmbeddingService(Service):
51
77
  raise ValueError("Embedding service not started - no runtime available")
52
78
 
53
79
  if self._cache_enabled and text in self._cache:
54
- return self._cache[text]
80
+ embedding = self._cache.pop(text)
81
+ self._cache[text] = embedding
82
+ return embedding
55
83
 
56
84
  # Truncate to stay within embedding model token limits
57
85
  embed_text = text
@@ -87,9 +115,10 @@ class EmbeddingService(Service):
87
115
  return embeddings
88
116
 
89
117
  def _add_to_cache(self, text: str, embedding: list[float]) -> None:
90
- if len(self._cache) >= self._max_cache_size:
91
- oldest_key = next(iter(self._cache))
92
- del self._cache[oldest_key]
118
+ if text in self._cache:
119
+ self._cache.pop(text)
120
+ elif len(self._cache) >= self._max_cache_size:
121
+ self._cache.popitem(last=False)
93
122
  self._cache[text] = embedding
94
123
 
95
124
  def clear_cache(self) -> None:
@@ -105,8 +134,7 @@ class EmbeddingService(Service):
105
134
  raise ValueError("Cache size must be positive")
106
135
  self._max_cache_size = size
107
136
  while len(self._cache) > self._max_cache_size:
108
- oldest_key = next(iter(self._cache))
109
- del self._cache[oldest_key]
137
+ self._cache.popitem(last=False)
110
138
 
111
139
  async def similarity(self, text1: str, text2: str) -> float:
112
140
  embedding1 = await self.embed(text1)
@@ -120,3 +148,173 @@ class EmbeddingService(Service):
120
148
  return 0.0
121
149
 
122
150
  return dot_product / (magnitude1 * magnitude2)
151
+
152
+ async def _handle_embedding_request(self, payload: Any) -> None:
153
+ """Handle embedding generation request event."""
154
+ payload_key = self._get_payload_key(payload)
155
+ if payload_key is not None:
156
+ if payload_key in self._pending_payload_keys:
157
+ return
158
+ self._pending_payload_keys.add(payload_key)
159
+
160
+ try:
161
+ await self._queue.put((payload_key, payload))
162
+ except Exception:
163
+ if payload_key is not None:
164
+ self._pending_payload_keys.discard(payload_key)
165
+ raise
166
+
167
+ def _get_payload_key(self, payload: Any) -> str | None:
168
+ memory_data = getattr(payload, "memory", None)
169
+ if memory_data is None:
170
+ extra = getattr(payload, "extra", None)
171
+ if hasattr(extra, "__getitem__"):
172
+ with contextlib.suppress(Exception):
173
+ if "memory" in extra:
174
+ memory_data = extra["memory"]
175
+ if memory_data is None and isinstance(payload, dict):
176
+ memory_data = payload.get("memory")
177
+ if memory_data is None:
178
+ return None
179
+
180
+ if isinstance(memory_data, dict):
181
+ memory_id = memory_data.get("id")
182
+ else:
183
+ memory_id = getattr(memory_data, "id", None)
184
+ if memory_id is None:
185
+ return None
186
+ return str(memory_id)
187
+
188
+ async def _worker(self) -> None:
189
+ """Background worker for processing embedding requests."""
190
+ while True:
191
+ try:
192
+ payload_key, payload = await self._queue.get()
193
+ except asyncio.CancelledError:
194
+ break
195
+
196
+ try:
197
+ await self._process_embedding_request(payload)
198
+ except Exception as e:
199
+ if self._runtime:
200
+ self._runtime.logger.error(f"Error in embedding worker: {e}", exc_info=True)
201
+ finally:
202
+ if payload_key is not None:
203
+ self._pending_payload_keys.discard(payload_key)
204
+ self._queue.task_done()
205
+
206
+ async def _process_embedding_request(self, payload: Any) -> None:
207
+ from elizaos.types.events import EventType
208
+ from elizaos.types.memory import Memory
209
+
210
+ # Extract memory from payload
211
+ # Handle both protobuf object and dict/wrapper
212
+ memory_data = None
213
+ if hasattr(payload, "memory"): # specific payload
214
+ memory_data = payload.memory
215
+ elif hasattr(payload, "extra") and hasattr(
216
+ payload.extra, "__getitem__"
217
+ ): # generic event payload
218
+ try:
219
+ # Check if 'memory' is in extra
220
+ # payload.extra might be a Struct or dict
221
+ if "memory" in payload.extra:
222
+ from elizaos.runtime import _struct_value_to_python
223
+
224
+ mem_val = payload.extra["memory"]
225
+ if hasattr(mem_val, "struct_value"):
226
+ if mem_val.HasField("struct_value"):
227
+ memory_data = _struct_value_to_python(mem_val)
228
+ else:
229
+ memory_data = mem_val
230
+ else:
231
+ memory_data = mem_val
232
+ except Exception:
233
+ pass
234
+
235
+ if not memory_data:
236
+ return
237
+
238
+ # Convert to Memory object if needed
239
+ if isinstance(memory_data, dict):
240
+ memory = Memory(
241
+ id=memory_data.get("id"),
242
+ content=memory_data.get("content"),
243
+ room_id=memory_data.get("roomId") or memory_data.get("room_id"),
244
+ entity_id=memory_data.get("entityId")
245
+ or memory_data.get("entity_id")
246
+ or memory_data.get("userId")
247
+ or memory_data.get("user_id"),
248
+ agent_id=memory_data.get("agentId") or memory_data.get("agent_id"),
249
+ )
250
+ if "embedding" in memory_data:
251
+ memory.embedding = memory_data["embedding"]
252
+ if "metadata" in memory_data:
253
+ memory.metadata = memory_data["metadata"]
254
+ else:
255
+ memory = memory_data
256
+
257
+ if not memory.id:
258
+ return
259
+
260
+ if memory.embedding and len(memory.embedding) > 0:
261
+ return
262
+
263
+ text = (
264
+ memory.content.text
265
+ if hasattr(memory.content, "text")
266
+ else getattr(memory.content, "text", "")
267
+ )
268
+ if not text:
269
+ return
270
+
271
+ embedding_source_text = text
272
+
273
+ # Intent generation logic
274
+ if len(text) > 20:
275
+ has_intent = False
276
+ if memory.metadata and isinstance(memory.metadata, dict):
277
+ has_intent = "intent" in memory.metadata
278
+
279
+ if not has_intent:
280
+ prompt = (
281
+ "Analyze the following message and extract the core user intent or a summary "
282
+ "of what they are asking/saying. Return ONLY the intent text.\n"
283
+ f'Message:\n"{text}"\n\nIntent:'
284
+ )
285
+
286
+ try:
287
+ output = await self._runtime.use_model(ModelType.TEXT_SMALL, prompt=prompt)
288
+
289
+ intent = str(output).strip()
290
+ if intent:
291
+ embedding_source_text = intent
292
+ # Update metadata
293
+ # Use custom metadata for intent
294
+ memory.metadata.custom.custom_data["intent"] = intent
295
+ except Exception as e:
296
+ self._runtime.logger.warning(f"Failed to generate intent: {e}")
297
+
298
+ # Generate embedding
299
+ try:
300
+ embedding = await self.embed(embedding_source_text)
301
+ # Protobuf repeated field assignment must extend or use slice
302
+ if hasattr(memory.embedding, "extend"): # It's a repeated field
303
+ del memory.embedding[:]
304
+ memory.embedding.extend(embedding)
305
+ else:
306
+ # If it's a list (unlikely based on error)
307
+ memory.embedding = embedding
308
+
309
+ # Update in DB
310
+ if getattr(self._runtime, "_adapter", None):
311
+ await self._runtime._adapter.update_memory(memory)
312
+
313
+ # Emit completion
314
+ await self._runtime.emit_event(
315
+ EventType.Name(EventType.EVENT_TYPE_EMBEDDING_GENERATION_COMPLETED),
316
+ {"source": "embedding_service", "memory_id": str(memory.id)},
317
+ )
318
+
319
+ except Exception as e:
320
+ self._runtime.logger.error(f"Failed to generate embedding: {e}")
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
4
  from datetime import datetime
5
- from enum import Enum
5
+ from enum import StrEnum
6
6
  from typing import TYPE_CHECKING
7
7
  from uuid import UUID
8
8
 
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
12
12
  from elizaos.types import IAgentRuntime
13
13
 
14
14
 
15
- class ContactCategory(str, Enum):
15
+ class ContactCategory(StrEnum):
16
16
  FRIEND = "friend"
17
17
  FAMILY = "family"
18
18
  COLLEAGUE = "colleague"
@@ -14,7 +14,7 @@ import asyncio
14
14
  import contextlib
15
15
  import time
16
16
  from dataclasses import dataclass
17
- from enum import Enum
17
+ from enum import StrEnum
18
18
  from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
19
19
  from uuid import UUID, uuid4
20
20
 
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
27
27
  TICK_INTERVAL_MS = 1000
28
28
 
29
29
 
30
- class TaskStatus(str, Enum):
30
+ class TaskStatus(StrEnum):
31
31
  """Task status enum."""
32
32
 
33
33
  PENDING = "pending"
@@ -37,7 +37,7 @@ class TaskStatus(str, Enum):
37
37
  CANCELLED = "cancelled"
38
38
 
39
39
 
40
- class TaskPriority(str, Enum):
40
+ class TaskPriority(StrEnum):
41
41
  """Task priority enum."""
42
42
 
43
43
  LOW = "low"
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ from hashlib import sha256
5
+ from typing import Protocol
6
+ from uuid import UUID
7
+
8
+ DEFAULT_TIME_BUCKET_MS = 5 * 60 * 1000
9
+
10
+ SeedPart = str | int | float | bool | None
11
+
12
+
13
+ class RuntimeLike(Protocol):
14
+ @property
15
+ def agent_id(self) -> object: ...
16
+
17
+ @property
18
+ def character(self) -> object: ...
19
+
20
+ def get_setting(self, key: str) -> object | None: ...
21
+
22
+
23
+ def _normalize_seed_part(part: SeedPart) -> str:
24
+ if part is None:
25
+ return "none"
26
+ return str(part)
27
+
28
+
29
+ def _coerce_non_empty_string(value: object | None) -> str | None:
30
+ if value is None:
31
+ return None
32
+ as_text = str(value).strip()
33
+ if not as_text:
34
+ return None
35
+ return as_text
36
+
37
+
38
+ def _get_field(obj: object | None, *names: str) -> object | None:
39
+ if obj is None:
40
+ return None
41
+
42
+ if isinstance(obj, dict):
43
+ for name in names:
44
+ if name in obj:
45
+ value = obj[name]
46
+ if value is not None and value != "":
47
+ return value
48
+ return None
49
+
50
+ for name in names:
51
+ value = getattr(obj, name, None)
52
+ if value is not None and value != "":
53
+ return value
54
+ return None
55
+
56
+
57
+ def build_deterministic_seed(parts: list[SeedPart]) -> str:
58
+ return "|".join(_normalize_seed_part(part) for part in parts)
59
+
60
+
61
+ def deterministic_hex(seed: str, surface: str, length: int = 16) -> str:
62
+ if length <= 0:
63
+ return ""
64
+
65
+ output = ""
66
+ counter = 0
67
+ while len(output) < length:
68
+ payload = f"{seed}|{surface}|{counter}".encode()
69
+ output += sha256(payload).hexdigest()
70
+ counter += 1
71
+ return output[:length]
72
+
73
+
74
+ def deterministic_int(seed: str, surface: str, max_exclusive: int) -> int:
75
+ if max_exclusive <= 1:
76
+ return 0
77
+ value = int(deterministic_hex(seed, surface, 12), 16)
78
+ return value % max_exclusive
79
+
80
+
81
+ def deterministic_uuid(seed: str, surface: str) -> str:
82
+ return str(UUID(hex=deterministic_hex(seed, surface, 32)))
83
+
84
+
85
+ def parse_boolean_setting(value: object | None) -> bool:
86
+ if isinstance(value, bool):
87
+ return value
88
+ if isinstance(value, (int, float)):
89
+ return value != 0
90
+ if isinstance(value, str):
91
+ normalized = value.strip().lower()
92
+ return normalized in ("1", "true", "yes", "on", "enabled")
93
+ return False
94
+
95
+
96
+ def parse_positive_int_setting(value: object | None, fallback: int) -> int:
97
+ if isinstance(value, bool):
98
+ return fallback
99
+ if isinstance(value, (int, float)):
100
+ numeric = int(value)
101
+ return numeric if numeric > 0 else fallback
102
+ if isinstance(value, str):
103
+ try:
104
+ numeric = int(float(value))
105
+ return numeric if numeric > 0 else fallback
106
+ except ValueError:
107
+ return fallback
108
+ return fallback
109
+
110
+
111
+ def build_conversation_seed(
112
+ runtime: RuntimeLike,
113
+ message: object | None,
114
+ state: object | None,
115
+ surface: str,
116
+ *,
117
+ bucket_ms: int | None = None,
118
+ now_ms: int | None = None,
119
+ ) -> str:
120
+ now_ms_value = now_ms if now_ms is not None else int(datetime.now(UTC).timestamp() * 1000)
121
+
122
+ state_data = _get_field(state, "data")
123
+ room_obj = _get_field(state_data, "room")
124
+ world_obj = _get_field(state_data, "world")
125
+
126
+ room_id = (
127
+ _coerce_non_empty_string(_get_field(room_obj, "id"))
128
+ or _coerce_non_empty_string(_get_field(state_data, "room_id", "roomId"))
129
+ or _coerce_non_empty_string(_get_field(message, "room_id", "roomId"))
130
+ or "room:none"
131
+ )
132
+ world_id = (
133
+ _coerce_non_empty_string(_get_field(world_obj, "id"))
134
+ or _coerce_non_empty_string(_get_field(room_obj, "world_id", "worldId"))
135
+ or _coerce_non_empty_string(_get_field(state_data, "world_id", "worldId"))
136
+ or _coerce_non_empty_string(_get_field(message, "world_id", "worldId"))
137
+ or "world:none"
138
+ )
139
+
140
+ character_obj = _get_field(runtime, "character")
141
+ character_id = (
142
+ _coerce_non_empty_string(_get_field(character_obj, "id"))
143
+ or _coerce_non_empty_string(_get_field(runtime, "agent_id"))
144
+ or "agent:none"
145
+ )
146
+
147
+ epoch_bucket = 0
148
+ if bucket_ms and bucket_ms > 0:
149
+ epoch_bucket = now_ms_value // bucket_ms
150
+
151
+ return build_deterministic_seed(
152
+ [
153
+ "eliza-prompt-cache-v1",
154
+ world_id,
155
+ room_id,
156
+ character_id,
157
+ epoch_bucket,
158
+ surface,
159
+ ]
160
+ )
161
+
162
+
163
+ def get_prompt_reference_datetime(
164
+ runtime: RuntimeLike,
165
+ message: object | None,
166
+ state: object | None,
167
+ surface: str,
168
+ *,
169
+ now: datetime | None = None,
170
+ ) -> datetime:
171
+ now_utc = now.astimezone(UTC) if now is not None else datetime.now(UTC)
172
+ deterministic_enabled = parse_boolean_setting(
173
+ runtime.get_setting("PROMPT_CACHE_DETERMINISTIC_TIME")
174
+ )
175
+ if not deterministic_enabled:
176
+ return now_utc
177
+
178
+ bucket_ms = parse_positive_int_setting(
179
+ runtime.get_setting("PROMPT_CACHE_TIME_BUCKET_MS"),
180
+ DEFAULT_TIME_BUCKET_MS,
181
+ )
182
+ now_ms = int(now_utc.timestamp() * 1000)
183
+ seed = build_conversation_seed(
184
+ runtime,
185
+ message,
186
+ state,
187
+ surface,
188
+ bucket_ms=bucket_ms,
189
+ now_ms=now_ms,
190
+ )
191
+ bucket_start = (now_ms // bucket_ms) * bucket_ms
192
+ offset = deterministic_int(seed, "time-offset-ms", bucket_ms)
193
+ return datetime.fromtimestamp((bucket_start + offset) / 1000, tz=UTC)
@@ -0,0 +1 @@
1
+ """Auto-generated module package."""