@elizaos/python 2.0.0-alpha.10 → 2.0.0-alpha.26

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 (145) 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 +21 -11
  5. package/elizaos/advanced_capabilities/actions/follow_room.py +28 -28
  6. package/elizaos/advanced_capabilities/actions/image_generation.py +13 -26
  7. package/elizaos/advanced_capabilities/actions/mute_room.py +13 -26
  8. package/elizaos/advanced_capabilities/actions/remove_contact.py +16 -2
  9. package/elizaos/advanced_capabilities/actions/roles.py +13 -27
  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 +183 -50
  13. package/elizaos/advanced_capabilities/actions/settings.py +16 -2
  14. package/elizaos/advanced_capabilities/actions/unfollow_room.py +13 -26
  15. package/elizaos/advanced_capabilities/actions/unmute_room.py +13 -26
  16. package/elizaos/advanced_capabilities/actions/update_contact.py +16 -2
  17. package/elizaos/advanced_capabilities/actions/update_entity.py +16 -2
  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 +24 -3
  23. package/elizaos/advanced_capabilities/services/__init__.py +2 -9
  24. package/elizaos/advanced_memory/actions/reset_session.py +11 -0
  25. package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
  26. package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
  27. package/elizaos/advanced_memory/memory_service.py +15 -17
  28. package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
  29. package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
  30. package/elizaos/advanced_planning/planning_service.py +26 -14
  31. package/elizaos/basic_capabilities/__init__.py +0 -2
  32. package/elizaos/basic_capabilities/providers/__init__.py +0 -3
  33. package/elizaos/basic_capabilities/providers/actions.py +118 -29
  34. package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
  35. package/elizaos/basic_capabilities/providers/character.py +19 -21
  36. package/elizaos/basic_capabilities/providers/contacts.py +79 -0
  37. package/elizaos/basic_capabilities/providers/current_time.py +7 -4
  38. package/elizaos/basic_capabilities/providers/facts.py +87 -0
  39. package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
  40. package/elizaos/basic_capabilities/providers/knowledge.py +97 -0
  41. package/elizaos/basic_capabilities/providers/relationships.py +107 -0
  42. package/elizaos/basic_capabilities/providers/roles.py +96 -0
  43. package/elizaos/basic_capabilities/providers/settings.py +56 -0
  44. package/elizaos/basic_capabilities/providers/time.py +7 -4
  45. package/elizaos/bootstrap/__init__.py +21 -2
  46. package/elizaos/bootstrap/actions/schedule_follow_up.py +65 -7
  47. package/elizaos/bootstrap/actions/send_message.py +162 -15
  48. package/elizaos/bootstrap/autonomy/__init__.py +5 -1
  49. package/elizaos/bootstrap/autonomy/action.py +161 -0
  50. package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
  51. package/elizaos/bootstrap/autonomy/service.py +238 -28
  52. package/elizaos/bootstrap/plugin.py +7 -0
  53. package/elizaos/bootstrap/providers/actions.py +118 -27
  54. package/elizaos/bootstrap/providers/agent_settings.py +1 -0
  55. package/elizaos/bootstrap/providers/attachments.py +1 -0
  56. package/elizaos/bootstrap/providers/capabilities.py +1 -0
  57. package/elizaos/bootstrap/providers/character.py +1 -0
  58. package/elizaos/bootstrap/providers/choice.py +1 -0
  59. package/elizaos/bootstrap/providers/contacts.py +1 -0
  60. package/elizaos/bootstrap/providers/current_time.py +8 -2
  61. package/elizaos/bootstrap/providers/entities.py +1 -0
  62. package/elizaos/bootstrap/providers/evaluators.py +1 -0
  63. package/elizaos/bootstrap/providers/facts.py +1 -0
  64. package/elizaos/bootstrap/providers/follow_ups.py +1 -0
  65. package/elizaos/bootstrap/providers/knowledge.py +27 -3
  66. package/elizaos/bootstrap/providers/providers_list.py +1 -0
  67. package/elizaos/bootstrap/providers/relationships.py +1 -0
  68. package/elizaos/bootstrap/providers/roles.py +1 -0
  69. package/elizaos/bootstrap/providers/settings.py +1 -0
  70. package/elizaos/bootstrap/providers/time.py +8 -4
  71. package/elizaos/bootstrap/providers/world.py +1 -0
  72. package/elizaos/bootstrap/services/embedding.py +156 -1
  73. package/elizaos/deterministic.py +193 -0
  74. package/elizaos/generated/__init__.py +1 -0
  75. package/elizaos/generated/action_docs.py +3181 -0
  76. package/elizaos/generated/spec_helpers.py +175 -0
  77. package/elizaos/media/mime.py +2 -2
  78. package/elizaos/media/search.py +23 -23
  79. package/elizaos/runtime.py +215 -57
  80. package/elizaos/services/message_service.py +175 -29
  81. package/elizaos/types/components.py +2 -2
  82. package/elizaos/types/generated/__init__.py +12 -0
  83. package/elizaos/types/generated/eliza/v1/agent_pb2.py +63 -0
  84. package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +159 -0
  85. package/elizaos/types/generated/eliza/v1/components_pb2.py +65 -0
  86. package/elizaos/types/generated/eliza/v1/components_pb2.pyi +160 -0
  87. package/elizaos/types/generated/eliza/v1/database_pb2.py +78 -0
  88. package/elizaos/types/generated/eliza/v1/database_pb2.pyi +305 -0
  89. package/elizaos/types/generated/eliza/v1/environment_pb2.py +58 -0
  90. package/elizaos/types/generated/eliza/v1/environment_pb2.pyi +135 -0
  91. package/elizaos/types/generated/eliza/v1/events_pb2.py +82 -0
  92. package/elizaos/types/generated/eliza/v1/events_pb2.pyi +322 -0
  93. package/elizaos/types/generated/eliza/v1/ipc_pb2.py +113 -0
  94. package/elizaos/types/generated/eliza/v1/ipc_pb2.pyi +367 -0
  95. package/elizaos/types/generated/eliza/v1/knowledge_pb2.py +41 -0
  96. package/elizaos/types/generated/eliza/v1/knowledge_pb2.pyi +26 -0
  97. package/elizaos/types/generated/eliza/v1/memory_pb2.py +55 -0
  98. package/elizaos/types/generated/eliza/v1/memory_pb2.pyi +111 -0
  99. package/elizaos/types/generated/eliza/v1/message_service_pb2.py +48 -0
  100. package/elizaos/types/generated/eliza/v1/message_service_pb2.pyi +69 -0
  101. package/elizaos/types/generated/eliza/v1/messaging_pb2.py +51 -0
  102. package/elizaos/types/generated/eliza/v1/messaging_pb2.pyi +97 -0
  103. package/elizaos/types/generated/eliza/v1/model_pb2.py +84 -0
  104. package/elizaos/types/generated/eliza/v1/model_pb2.pyi +280 -0
  105. package/elizaos/types/generated/eliza/v1/payment_pb2.py +44 -0
  106. package/elizaos/types/generated/eliza/v1/payment_pb2.pyi +70 -0
  107. package/elizaos/types/generated/eliza/v1/plugin_pb2.py +68 -0
  108. package/elizaos/types/generated/eliza/v1/plugin_pb2.pyi +145 -0
  109. package/elizaos/types/generated/eliza/v1/primitives_pb2.py +48 -0
  110. package/elizaos/types/generated/eliza/v1/primitives_pb2.pyi +92 -0
  111. package/elizaos/types/generated/eliza/v1/prompts_pb2.py +52 -0
  112. package/elizaos/types/generated/eliza/v1/prompts_pb2.pyi +74 -0
  113. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.py +211 -0
  114. package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.pyi +1296 -0
  115. package/elizaos/types/generated/eliza/v1/service_pb2.py +42 -0
  116. package/elizaos/types/generated/eliza/v1/service_pb2.pyi +69 -0
  117. package/elizaos/types/generated/eliza/v1/settings_pb2.py +58 -0
  118. package/elizaos/types/generated/eliza/v1/settings_pb2.pyi +85 -0
  119. package/elizaos/types/generated/eliza/v1/state_pb2.py +60 -0
  120. package/elizaos/types/generated/eliza/v1/state_pb2.pyi +114 -0
  121. package/elizaos/types/generated/eliza/v1/task_pb2.py +42 -0
  122. package/elizaos/types/generated/eliza/v1/task_pb2.pyi +58 -0
  123. package/elizaos/types/generated/eliza/v1/tee_pb2.py +52 -0
  124. package/elizaos/types/generated/eliza/v1/tee_pb2.pyi +90 -0
  125. package/elizaos/types/generated/eliza/v1/testing_pb2.py +39 -0
  126. package/elizaos/types/generated/eliza/v1/testing_pb2.pyi +23 -0
  127. package/elizaos/types/model.py +30 -0
  128. package/elizaos/types/runtime.py +6 -2
  129. package/elizaos/utils/validation.py +76 -0
  130. package/package.json +3 -2
  131. package/tests/test_action_parameters.py +2 -3
  132. package/tests/test_actions_provider_examples.py +58 -1
  133. package/tests/test_advanced_memory_behavior.py +0 -2
  134. package/tests/test_advanced_memory_flag.py +0 -2
  135. package/tests/test_advanced_planning_behavior.py +11 -5
  136. package/tests/test_async_embedding.py +124 -0
  137. package/tests/test_autonomy.py +24 -3
  138. package/tests/test_runtime.py +8 -17
  139. package/tests/test_schedule_follow_up_action.py +260 -0
  140. package/tests/test_send_message_action_targets.py +114 -0
  141. package/tests/test_settings_crypto.py +0 -2
  142. package/tests/test_validation.py +141 -0
  143. package/tests/verify_memory_architecture.py +192 -0
  144. package/uv.lock +1565 -0
  145. package/elizaos/basic_capabilities/providers/capabilities.py +0 -62
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
4
4
 
5
5
  from elizaos.generated.spec_helpers import require_provider_spec
6
6
  from elizaos.types import Provider, ProviderResult
7
+ from elizaos.types.database import MemorySearchOptions
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from elizaos.types import IAgentRuntime, Memory, State
@@ -29,11 +30,33 @@ async def get_knowledge_context(
29
30
  text="", values={"knowledgeCount": 0, "hasKnowledge": False}, data={"entries": []}
30
31
  )
31
32
 
32
- relevant_knowledge = await runtime.search_knowledge(
33
- query=query_text,
34
- limit=5,
33
+ # 1. Fetch recent messages to get embeddings
34
+ recent_messages = await runtime.get_memories(
35
+ room_id=message.room_id, limit=5, table_name="messages"
35
36
  )
36
37
 
38
+ # 2. Extract valid embeddings
39
+ embeddings = [m.embedding for m in recent_messages if m and m.embedding]
40
+
41
+ relevant_knowledge = []
42
+ # 3. Search using the most recent embedding if available
43
+ if embeddings:
44
+ primary_embedding = embeddings[0]
45
+ params = MemorySearchOptions(
46
+ table_name="knowledge",
47
+ room_id=message.room_id,
48
+ embedding=primary_embedding,
49
+ match_threshold=0.75,
50
+ match_count=5,
51
+ unique=True,
52
+ )
53
+ relevant_knowledge = await runtime.search_memories(params)
54
+ elif query_text:
55
+ # Fallback to search_knowledge if no embeddings found?
56
+ # TS implementation might rely on search_memories logic handling missing embedding?
57
+ # No, TS skips if no embedding.
58
+ pass
59
+
37
60
  for entry in relevant_knowledge:
38
61
  if entry.content and entry.content.text:
39
62
  knowledge_text = entry.content.text
@@ -70,4 +93,5 @@ knowledge_provider = Provider(
70
93
  description=_spec["description"],
71
94
  get=get_knowledge_context,
72
95
  dynamic=_spec.get("dynamic", True),
96
+ position=_spec.get("position"),
73
97
  )
@@ -56,4 +56,5 @@ providers_list_provider = Provider(
56
56
  description=_spec["description"],
57
57
  get=get_providers_list,
58
58
  dynamic=_spec.get("dynamic", False),
59
+ position=_spec.get("position"),
59
60
  )
@@ -103,4 +103,5 @@ relationships_provider = Provider(
103
103
  description=_spec["description"],
104
104
  get=get_relationships,
105
105
  dynamic=_spec.get("dynamic", True),
106
+ position=_spec.get("position"),
106
107
  )
@@ -92,4 +92,5 @@ roles_provider = Provider(
92
92
  description=_spec["description"],
93
93
  get=get_roles,
94
94
  dynamic=_spec.get("dynamic", True),
95
+ position=_spec.get("position"),
95
96
  )
@@ -52,4 +52,5 @@ settings_provider = Provider(
52
52
  description=_spec["description"],
53
53
  get=get_settings_context,
54
54
  dynamic=_spec.get("dynamic", True),
55
+ position=_spec.get("position"),
55
56
  )
@@ -1,8 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from datetime import UTC, datetime
4
3
  from typing import TYPE_CHECKING
5
4
 
5
+ from elizaos.deterministic import get_prompt_reference_datetime
6
6
  from elizaos.generated.spec_helpers import require_provider_spec
7
7
  from elizaos.types import Provider, ProviderResult
8
8
 
@@ -18,9 +18,12 @@ async def get_time_context(
18
18
  message: Memory,
19
19
  state: State | None = None,
20
20
  ) -> ProviderResult:
21
- _ = runtime, message, state
22
-
23
- now = datetime.now(UTC)
21
+ now = get_prompt_reference_datetime(
22
+ runtime,
23
+ message,
24
+ state,
25
+ "provider:time",
26
+ )
24
27
  iso_string = now.isoformat()
25
28
  timestamp_ms = int(now.timestamp() * 1000)
26
29
  human_readable = now.strftime("%A, %B %d, %Y at %H:%M:%S UTC")
@@ -42,4 +45,5 @@ time_provider = Provider(
42
45
  description=_spec["description"],
43
46
  get=get_time_context,
44
47
  dynamic=_spec.get("dynamic", True),
48
+ position=_spec.get("position"),
45
49
  )
@@ -94,4 +94,5 @@ world_provider = Provider(
94
94
  description=_spec["description"],
95
95
  get=get_world_context,
96
96
  dynamic=_spec.get("dynamic", True),
97
+ position=_spec.get("position"),
97
98
  )
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ import asyncio
4
+ import contextlib
5
+ from typing import TYPE_CHECKING, Any
4
6
 
5
7
  from elizaos.types import ModelType, Service, ServiceType
8
+ from elizaos.types.events import EventType
6
9
 
7
10
  if TYPE_CHECKING:
8
11
  from elizaos.types import IAgentRuntime
@@ -21,11 +24,21 @@ class EmbeddingService(Service):
21
24
  self._cache: dict[str, list[float]] = {}
22
25
  self._cache_enabled: bool = True
23
26
  self._max_cache_size: int = 1000
27
+ self._queue: asyncio.Queue = asyncio.Queue()
28
+ self._worker_task: asyncio.Task | None = None
24
29
 
25
30
  @classmethod
26
31
  async def start(cls, runtime: IAgentRuntime) -> EmbeddingService:
27
32
  service = cls()
28
33
  service._runtime = runtime
34
+
35
+ # Register event handler
36
+ event_name = EventType.Name(EventType.EVENT_TYPE_EMBEDDING_GENERATION_REQUESTED)
37
+ runtime.register_event(event_name, service._handle_embedding_request)
38
+
39
+ # Start worker
40
+ service._worker_task = asyncio.create_task(service._worker())
41
+
29
42
  runtime.logger.info(
30
43
  "Embedding service started",
31
44
  src="service:embedding",
@@ -34,6 +47,12 @@ class EmbeddingService(Service):
34
47
  return service
35
48
 
36
49
  async def stop(self) -> None:
50
+ if self._worker_task:
51
+ self._worker_task.cancel()
52
+ with contextlib.suppress(asyncio.CancelledError):
53
+ await self._worker_task
54
+ self._worker_task = None
55
+
37
56
  if self._runtime:
38
57
  self._runtime.logger.info(
39
58
  "Embedding service stopped",
@@ -120,3 +139,139 @@ class EmbeddingService(Service):
120
139
  return 0.0
121
140
 
122
141
  return dot_product / (magnitude1 * magnitude2)
142
+
143
+ async def _handle_embedding_request(self, payload: Any) -> None:
144
+ """Handle embedding generation request event."""
145
+ await self._queue.put(payload)
146
+
147
+ async def _worker(self) -> None:
148
+ """Background worker for processing embedding requests."""
149
+ while True:
150
+ try:
151
+ payload = await self._queue.get()
152
+ except asyncio.CancelledError:
153
+ break
154
+
155
+ try:
156
+ await self._process_embedding_request(payload)
157
+ except Exception as e:
158
+ if self._runtime:
159
+ self._runtime.logger.error(f"Error in embedding worker: {e}", exc_info=True)
160
+ finally:
161
+ self._queue.task_done()
162
+
163
+ async def _process_embedding_request(self, payload: Any) -> None:
164
+ from elizaos.types.events import EventType
165
+ from elizaos.types.memory import Memory
166
+
167
+ # Extract memory from payload
168
+ # Handle both protobuf object and dict/wrapper
169
+ memory_data = None
170
+ if hasattr(payload, "memory"): # specific payload
171
+ memory_data = payload.memory
172
+ elif hasattr(payload, "extra") and hasattr(
173
+ payload.extra, "__getitem__"
174
+ ): # generic event payload
175
+ try:
176
+ # Check if 'memory' is in extra
177
+ # payload.extra might be a Struct or dict
178
+ if "memory" in payload.extra:
179
+ from elizaos.runtime import _struct_value_to_python
180
+
181
+ mem_val = payload.extra["memory"]
182
+ if hasattr(mem_val, "struct_value"):
183
+ if mem_val.HasField("struct_value"):
184
+ memory_data = _struct_value_to_python(mem_val)
185
+ else:
186
+ memory_data = mem_val
187
+ else:
188
+ memory_data = mem_val
189
+ except Exception:
190
+ pass
191
+
192
+ if not memory_data:
193
+ return
194
+
195
+ # Convert to Memory object if needed
196
+ if isinstance(memory_data, dict):
197
+ memory = Memory(
198
+ id=memory_data.get("id"),
199
+ content=memory_data.get("content"),
200
+ room_id=memory_data.get("roomId") or memory_data.get("room_id"),
201
+ entity_id=memory_data.get("entityId")
202
+ or memory_data.get("entity_id")
203
+ or memory_data.get("userId")
204
+ or memory_data.get("user_id"),
205
+ agent_id=memory_data.get("agentId") or memory_data.get("agent_id"),
206
+ )
207
+ if "embedding" in memory_data:
208
+ memory.embedding = memory_data["embedding"]
209
+ if "metadata" in memory_data:
210
+ memory.metadata = memory_data["metadata"]
211
+ else:
212
+ memory = memory_data
213
+
214
+ if not memory.id:
215
+ return
216
+
217
+ if memory.embedding and len(memory.embedding) > 0:
218
+ return
219
+
220
+ text = (
221
+ memory.content.text
222
+ if hasattr(memory.content, "text")
223
+ else getattr(memory.content, "text", "")
224
+ )
225
+ if not text:
226
+ return
227
+
228
+ embedding_source_text = text
229
+
230
+ # Intent generation logic
231
+ if len(text) > 20:
232
+ has_intent = False
233
+ if memory.metadata and isinstance(memory.metadata, dict):
234
+ has_intent = "intent" in memory.metadata
235
+
236
+ if not has_intent:
237
+ prompt = (
238
+ "Analyze the following message and extract the core user intent or a summary "
239
+ "of what they are asking/saying. Return ONLY the intent text.\n"
240
+ f'Message:\n"{text}"\n\nIntent:'
241
+ )
242
+
243
+ try:
244
+ output = await self._runtime.use_model(ModelType.TEXT_SMALL, prompt=prompt)
245
+
246
+ intent = str(output).strip()
247
+ if intent:
248
+ embedding_source_text = intent
249
+ # Update metadata
250
+ # Use custom metadata for intent
251
+ memory.metadata.custom.custom_data["intent"] = intent
252
+ except Exception as e:
253
+ self._runtime.logger.warning(f"Failed to generate intent: {e}")
254
+
255
+ # Generate embedding
256
+ try:
257
+ embedding = await self.embed(embedding_source_text)
258
+ # Protobuf repeated field assignment must extend or use slice
259
+ if hasattr(memory.embedding, "extend"): # It's a repeated field
260
+ del memory.embedding[:]
261
+ memory.embedding.extend(embedding)
262
+ else:
263
+ # If it's a list (unlikely based on error)
264
+ memory.embedding = embedding
265
+
266
+ # Update in DB
267
+ if getattr(self._runtime, "_adapter", None):
268
+ await self._runtime._adapter.update_memory(memory)
269
+
270
+ # Emit completion
271
+ await self._runtime.emit_event(
272
+ EventType.Name(EventType.EVENT_TYPE_EMBEDDING_GENERATION_COMPLETED),
273
+ {"source": "embedding_service", "memory_id": str(memory.id)},
274
+ )
275
+
276
+ except Exception as e:
277
+ self._runtime.logger.error(f"Failed to generate embedding: {e}")
@@ -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."""