@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.
- package/elizaos/__init__.py +0 -1
- package/elizaos/advanced_capabilities/__init__.py +6 -41
- package/elizaos/advanced_capabilities/actions/__init__.py +1 -21
- package/elizaos/advanced_capabilities/actions/add_contact.py +21 -11
- package/elizaos/advanced_capabilities/actions/follow_room.py +28 -28
- package/elizaos/advanced_capabilities/actions/image_generation.py +13 -26
- package/elizaos/advanced_capabilities/actions/mute_room.py +13 -26
- package/elizaos/advanced_capabilities/actions/remove_contact.py +16 -2
- package/elizaos/advanced_capabilities/actions/roles.py +13 -27
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +70 -15
- package/elizaos/advanced_capabilities/actions/search_contacts.py +17 -3
- package/elizaos/advanced_capabilities/actions/send_message.py +183 -50
- package/elizaos/advanced_capabilities/actions/settings.py +16 -2
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +13 -26
- package/elizaos/advanced_capabilities/actions/unmute_room.py +13 -26
- package/elizaos/advanced_capabilities/actions/update_contact.py +16 -2
- package/elizaos/advanced_capabilities/actions/update_entity.py +16 -2
- package/elizaos/advanced_capabilities/evaluators/__init__.py +2 -9
- package/elizaos/advanced_capabilities/evaluators/reflection.py +3 -132
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +5 -201
- package/elizaos/advanced_capabilities/providers/__init__.py +1 -12
- package/elizaos/advanced_capabilities/providers/knowledge.py +24 -3
- package/elizaos/advanced_capabilities/services/__init__.py +2 -9
- package/elizaos/advanced_memory/actions/reset_session.py +11 -0
- package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
- package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_memory/memory_service.py +15 -17
- package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
- package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
- package/elizaos/advanced_planning/planning_service.py +26 -14
- package/elizaos/basic_capabilities/__init__.py +0 -2
- package/elizaos/basic_capabilities/providers/__init__.py +0 -3
- package/elizaos/basic_capabilities/providers/actions.py +118 -29
- package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
- package/elizaos/basic_capabilities/providers/character.py +19 -21
- package/elizaos/basic_capabilities/providers/contacts.py +79 -0
- package/elizaos/basic_capabilities/providers/current_time.py +7 -4
- package/elizaos/basic_capabilities/providers/facts.py +87 -0
- package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
- package/elizaos/basic_capabilities/providers/knowledge.py +97 -0
- package/elizaos/basic_capabilities/providers/relationships.py +107 -0
- package/elizaos/basic_capabilities/providers/roles.py +96 -0
- package/elizaos/basic_capabilities/providers/settings.py +56 -0
- package/elizaos/basic_capabilities/providers/time.py +7 -4
- package/elizaos/bootstrap/__init__.py +21 -2
- package/elizaos/bootstrap/actions/schedule_follow_up.py +65 -7
- package/elizaos/bootstrap/actions/send_message.py +162 -15
- package/elizaos/bootstrap/autonomy/__init__.py +5 -1
- package/elizaos/bootstrap/autonomy/action.py +161 -0
- package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
- package/elizaos/bootstrap/autonomy/service.py +238 -28
- package/elizaos/bootstrap/plugin.py +7 -0
- package/elizaos/bootstrap/providers/actions.py +118 -27
- package/elizaos/bootstrap/providers/agent_settings.py +1 -0
- package/elizaos/bootstrap/providers/attachments.py +1 -0
- package/elizaos/bootstrap/providers/capabilities.py +1 -0
- package/elizaos/bootstrap/providers/character.py +1 -0
- package/elizaos/bootstrap/providers/choice.py +1 -0
- package/elizaos/bootstrap/providers/contacts.py +1 -0
- package/elizaos/bootstrap/providers/current_time.py +8 -2
- package/elizaos/bootstrap/providers/entities.py +1 -0
- package/elizaos/bootstrap/providers/evaluators.py +1 -0
- package/elizaos/bootstrap/providers/facts.py +1 -0
- package/elizaos/bootstrap/providers/follow_ups.py +1 -0
- package/elizaos/bootstrap/providers/knowledge.py +27 -3
- package/elizaos/bootstrap/providers/providers_list.py +1 -0
- package/elizaos/bootstrap/providers/relationships.py +1 -0
- package/elizaos/bootstrap/providers/roles.py +1 -0
- package/elizaos/bootstrap/providers/settings.py +1 -0
- package/elizaos/bootstrap/providers/time.py +8 -4
- package/elizaos/bootstrap/providers/world.py +1 -0
- package/elizaos/bootstrap/services/embedding.py +156 -1
- package/elizaos/deterministic.py +193 -0
- package/elizaos/generated/__init__.py +1 -0
- package/elizaos/generated/action_docs.py +3181 -0
- package/elizaos/generated/spec_helpers.py +175 -0
- package/elizaos/media/mime.py +2 -2
- package/elizaos/media/search.py +23 -23
- package/elizaos/runtime.py +215 -57
- package/elizaos/services/message_service.py +175 -29
- package/elizaos/types/components.py +2 -2
- package/elizaos/types/generated/__init__.py +12 -0
- package/elizaos/types/generated/eliza/v1/agent_pb2.py +63 -0
- package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +159 -0
- package/elizaos/types/generated/eliza/v1/components_pb2.py +65 -0
- package/elizaos/types/generated/eliza/v1/components_pb2.pyi +160 -0
- package/elizaos/types/generated/eliza/v1/database_pb2.py +78 -0
- package/elizaos/types/generated/eliza/v1/database_pb2.pyi +305 -0
- package/elizaos/types/generated/eliza/v1/environment_pb2.py +58 -0
- package/elizaos/types/generated/eliza/v1/environment_pb2.pyi +135 -0
- package/elizaos/types/generated/eliza/v1/events_pb2.py +82 -0
- package/elizaos/types/generated/eliza/v1/events_pb2.pyi +322 -0
- package/elizaos/types/generated/eliza/v1/ipc_pb2.py +113 -0
- package/elizaos/types/generated/eliza/v1/ipc_pb2.pyi +367 -0
- package/elizaos/types/generated/eliza/v1/knowledge_pb2.py +41 -0
- package/elizaos/types/generated/eliza/v1/knowledge_pb2.pyi +26 -0
- package/elizaos/types/generated/eliza/v1/memory_pb2.py +55 -0
- package/elizaos/types/generated/eliza/v1/memory_pb2.pyi +111 -0
- package/elizaos/types/generated/eliza/v1/message_service_pb2.py +48 -0
- package/elizaos/types/generated/eliza/v1/message_service_pb2.pyi +69 -0
- package/elizaos/types/generated/eliza/v1/messaging_pb2.py +51 -0
- package/elizaos/types/generated/eliza/v1/messaging_pb2.pyi +97 -0
- package/elizaos/types/generated/eliza/v1/model_pb2.py +84 -0
- package/elizaos/types/generated/eliza/v1/model_pb2.pyi +280 -0
- package/elizaos/types/generated/eliza/v1/payment_pb2.py +44 -0
- package/elizaos/types/generated/eliza/v1/payment_pb2.pyi +70 -0
- package/elizaos/types/generated/eliza/v1/plugin_pb2.py +68 -0
- package/elizaos/types/generated/eliza/v1/plugin_pb2.pyi +145 -0
- package/elizaos/types/generated/eliza/v1/primitives_pb2.py +48 -0
- package/elizaos/types/generated/eliza/v1/primitives_pb2.pyi +92 -0
- package/elizaos/types/generated/eliza/v1/prompts_pb2.py +52 -0
- package/elizaos/types/generated/eliza/v1/prompts_pb2.pyi +74 -0
- package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.py +211 -0
- package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.pyi +1296 -0
- package/elizaos/types/generated/eliza/v1/service_pb2.py +42 -0
- package/elizaos/types/generated/eliza/v1/service_pb2.pyi +69 -0
- package/elizaos/types/generated/eliza/v1/settings_pb2.py +58 -0
- package/elizaos/types/generated/eliza/v1/settings_pb2.pyi +85 -0
- package/elizaos/types/generated/eliza/v1/state_pb2.py +60 -0
- package/elizaos/types/generated/eliza/v1/state_pb2.pyi +114 -0
- package/elizaos/types/generated/eliza/v1/task_pb2.py +42 -0
- package/elizaos/types/generated/eliza/v1/task_pb2.pyi +58 -0
- package/elizaos/types/generated/eliza/v1/tee_pb2.py +52 -0
- package/elizaos/types/generated/eliza/v1/tee_pb2.pyi +90 -0
- package/elizaos/types/generated/eliza/v1/testing_pb2.py +39 -0
- package/elizaos/types/generated/eliza/v1/testing_pb2.pyi +23 -0
- package/elizaos/types/model.py +30 -0
- package/elizaos/types/runtime.py +6 -2
- package/elizaos/utils/validation.py +76 -0
- package/package.json +3 -2
- package/tests/test_action_parameters.py +2 -3
- package/tests/test_actions_provider_examples.py +58 -1
- package/tests/test_advanced_memory_behavior.py +0 -2
- package/tests/test_advanced_memory_flag.py +0 -2
- package/tests/test_advanced_planning_behavior.py +11 -5
- package/tests/test_async_embedding.py +124 -0
- package/tests/test_autonomy.py +24 -3
- package/tests/test_runtime.py +8 -17
- package/tests/test_schedule_follow_up_action.py +260 -0
- package/tests/test_send_message_action_targets.py +114 -0
- package/tests/test_settings_crypto.py +0 -2
- package/tests/test_validation.py +141 -0
- package/tests/verify_memory_architecture.py +192 -0
- package/uv.lock +1565 -0
- 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
|
-
|
|
33
|
-
|
|
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
|
)
|
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
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."""
|