@elizaos/python 2.0.0-alpha.11 → 2.0.0-alpha.27
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/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/search_contacts.py +17 -3
- package/elizaos/advanced_capabilities/actions/send_message.py +317 -9
- 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/test_advanced_memory.py +357 -0
- package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
- package/elizaos/basic_capabilities/__init__.py +0 -2
- package/elizaos/basic_capabilities/providers/__init__.py +0 -3
- package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
- package/elizaos/basic_capabilities/providers/contacts.py +79 -0
- 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/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 +8 -0
- package/elizaos/bootstrap/plugin.py +7 -0
- package/elizaos/bootstrap/providers/knowledge.py +26 -3
- package/elizaos/bootstrap/services/embedding.py +156 -1
- package/elizaos/runtime.py +63 -18
- package/elizaos/services/message_service.py +173 -23
- package/elizaos/types/generated/eliza/v1/agent_pb2.py +16 -16
- package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +2 -4
- package/elizaos/types/model.py +27 -0
- package/elizaos/types/runtime.py +5 -1
- package/elizaos/utils/validation.py +76 -0
- package/package.json +2 -2
- package/tests/test_actions_provider_examples.py +58 -1
- package/tests/test_async_embedding.py +124 -0
- package/tests/test_autonomy.py +13 -2
- package/tests/test_validation.py +141 -0
- package/tests/verify_memory_architecture.py +192 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +0 -62
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from elizaos.types.memory import Memory
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_action_keywords(
|
|
7
|
+
message: Memory, recent_messages: list[Memory], keywords: list[str]
|
|
8
|
+
) -> bool:
|
|
9
|
+
"""
|
|
10
|
+
Validates if any of the given keywords are present in the recent message history.
|
|
11
|
+
|
|
12
|
+
Checks:
|
|
13
|
+
1. The current message content
|
|
14
|
+
2. The last 5 messages in recent_messages
|
|
15
|
+
"""
|
|
16
|
+
if not keywords:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
relevant_text = []
|
|
20
|
+
|
|
21
|
+
# 1. Current message content
|
|
22
|
+
if message.content and message.content.text:
|
|
23
|
+
relevant_text.append(message.content.text)
|
|
24
|
+
|
|
25
|
+
# 2. Recent messages (last 5)
|
|
26
|
+
# Take the last 5 messages
|
|
27
|
+
recent_subset = recent_messages[-5:] if recent_messages else []
|
|
28
|
+
|
|
29
|
+
for msg in recent_subset:
|
|
30
|
+
if msg.content and msg.content.text:
|
|
31
|
+
relevant_text.append(msg.content.text)
|
|
32
|
+
|
|
33
|
+
if not relevant_text:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
combined_text = "\n".join(relevant_text).lower()
|
|
37
|
+
|
|
38
|
+
return any(keyword.lower() in combined_text for keyword in keywords)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def validate_action_regex(
|
|
42
|
+
message: Memory, recent_messages: list[Memory], regex_pattern: str
|
|
43
|
+
) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Validates if any of the recent message history matches the given regex pattern.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
message: The current message memory
|
|
49
|
+
recent_messages: List of recent memories
|
|
50
|
+
regex_pattern: The regular expression pattern to check against
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
bool: True if the regex matches any message content, False otherwise
|
|
54
|
+
"""
|
|
55
|
+
if not regex_pattern:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
relevant_text = []
|
|
59
|
+
|
|
60
|
+
# 1. Current message content
|
|
61
|
+
if message.content and message.content.text:
|
|
62
|
+
relevant_text.append(message.content.text)
|
|
63
|
+
|
|
64
|
+
# 2. Recent messages (last 5)
|
|
65
|
+
recent_subset = recent_messages[-5:] if recent_messages else []
|
|
66
|
+
|
|
67
|
+
for msg in recent_subset:
|
|
68
|
+
if msg.content and msg.content.text:
|
|
69
|
+
relevant_text.append(msg.content.text)
|
|
70
|
+
|
|
71
|
+
if not relevant_text:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
combined_text = "\n".join(relevant_text)
|
|
75
|
+
|
|
76
|
+
return bool(re.search(regex_pattern, combined_text, re.MULTILINE))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elizaos/python",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.27",
|
|
4
4
|
"description": "elizaOS Core - Python runtime and types",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -16,5 +16,5 @@
|
|
|
16
16
|
},
|
|
17
17
|
"author": "elizaOS",
|
|
18
18
|
"license": "MIT",
|
|
19
|
-
"gitHead": "
|
|
19
|
+
"gitHead": "645f273ed2eee2f5777193ee7d9d94c8a4444767"
|
|
20
20
|
}
|
|
@@ -3,7 +3,26 @@ import pytest
|
|
|
3
3
|
from elizaos.action_docs import with_canonical_action_docs
|
|
4
4
|
from elizaos.bootstrap.actions import send_message_action
|
|
5
5
|
from elizaos.runtime import AgentRuntime
|
|
6
|
-
from elizaos.types import Character, Content, Memory, as_uuid
|
|
6
|
+
from elizaos.types import Action, Character, Content, Memory, as_uuid
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def _noop_handler(
|
|
10
|
+
runtime: AgentRuntime,
|
|
11
|
+
message: Memory,
|
|
12
|
+
state: object | None = None,
|
|
13
|
+
options: object | None = None,
|
|
14
|
+
callback: object | None = None,
|
|
15
|
+
responses: object | None = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def _always_valid(
|
|
21
|
+
runtime: AgentRuntime,
|
|
22
|
+
message: Memory,
|
|
23
|
+
state: object | None = None,
|
|
24
|
+
) -> bool:
|
|
25
|
+
return True
|
|
7
26
|
|
|
8
27
|
|
|
9
28
|
@pytest.mark.asyncio
|
|
@@ -37,3 +56,41 @@ async def test_actions_provider_includes_examples_and_parameter_examples() -> No
|
|
|
37
56
|
# Canonical docs include examples for SEND_MESSAGE parameters
|
|
38
57
|
assert "SEND_MESSAGE" in text
|
|
39
58
|
assert "# Action Call Examples" in text
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_actions_provider_does_not_trim_available_actions_to_top_10() -> None:
|
|
63
|
+
runtime = AgentRuntime(
|
|
64
|
+
character=Character(name="NoTrimTest", bio=["no trim test"], system="test"),
|
|
65
|
+
log_level="ERROR",
|
|
66
|
+
)
|
|
67
|
+
await runtime.initialize()
|
|
68
|
+
|
|
69
|
+
custom_action_names: list[str] = []
|
|
70
|
+
for index in range(12):
|
|
71
|
+
action_name = f"CUSTOM_ACTION_{index:02d}"
|
|
72
|
+
custom_action_names.append(action_name)
|
|
73
|
+
runtime.register_action(
|
|
74
|
+
Action(
|
|
75
|
+
name=action_name,
|
|
76
|
+
description=f"Custom action {index}",
|
|
77
|
+
handler=_noop_handler,
|
|
78
|
+
validate=_always_valid,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
actions_provider = next(p for p in runtime.providers if p.name == "ACTIONS")
|
|
83
|
+
|
|
84
|
+
message = Memory(
|
|
85
|
+
id=as_uuid("42345678-1234-1234-1234-123456789012"),
|
|
86
|
+
entity_id=as_uuid("42345678-1234-1234-1234-123456789013"),
|
|
87
|
+
room_id=as_uuid("42345678-1234-1234-1234-123456789014"),
|
|
88
|
+
content=Content(text="show me every action"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
state = await runtime.compose_state(message)
|
|
92
|
+
result = await actions_provider.get(runtime, message, state)
|
|
93
|
+
|
|
94
|
+
text = result.text or ""
|
|
95
|
+
for action_name in custom_action_names:
|
|
96
|
+
assert action_name in text
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import unittest
|
|
3
|
+
import uuid
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
5
|
+
|
|
6
|
+
from elizaos.bootstrap.services.embedding import EmbeddingService
|
|
7
|
+
from elizaos.types import ModelType
|
|
8
|
+
from elizaos.types.events import EventType
|
|
9
|
+
from elizaos.types.memory import Memory
|
|
10
|
+
from elizaos.types.primitives import Content
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestAsyncEmbedding(unittest.IsolatedAsyncioTestCase):
|
|
14
|
+
async def test_async_embedding_generation(self):
|
|
15
|
+
# Mock runtime
|
|
16
|
+
runtime = (
|
|
17
|
+
MagicMock()
|
|
18
|
+
) # Don't use spec=IAgentRuntime to avoid abstract methods issues for now
|
|
19
|
+
runtime.agent_id = uuid.uuid4()
|
|
20
|
+
runtime.logger = MagicMock()
|
|
21
|
+
|
|
22
|
+
# Event handling
|
|
23
|
+
events = {}
|
|
24
|
+
|
|
25
|
+
def register_event(event, handler):
|
|
26
|
+
if event not in events:
|
|
27
|
+
events[event] = []
|
|
28
|
+
events[event].append(handler)
|
|
29
|
+
|
|
30
|
+
async def emit_event(event, payload):
|
|
31
|
+
handlers = events.get(event, [])
|
|
32
|
+
for handler in handlers:
|
|
33
|
+
if asyncio.iscoroutinefunction(handler):
|
|
34
|
+
await handler(payload)
|
|
35
|
+
else:
|
|
36
|
+
handler(payload)
|
|
37
|
+
|
|
38
|
+
async def use_model(model_type, **kwargs):
|
|
39
|
+
if model_type == ModelType.TEXT_SMALL:
|
|
40
|
+
return "intent"
|
|
41
|
+
if model_type == ModelType.TEXT_EMBEDDING:
|
|
42
|
+
return [0.1] * 384
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
runtime.register_event = register_event
|
|
46
|
+
runtime.emit_event = AsyncMock(side_effect=emit_event)
|
|
47
|
+
runtime.use_model = AsyncMock(side_effect=use_model)
|
|
48
|
+
|
|
49
|
+
# Mock adapter
|
|
50
|
+
adapter = AsyncMock()
|
|
51
|
+
runtime.db = adapter
|
|
52
|
+
runtime._adapter = adapter
|
|
53
|
+
service = await EmbeddingService.start(runtime)
|
|
54
|
+
|
|
55
|
+
# Setup completion listener
|
|
56
|
+
completed_future = asyncio.Future()
|
|
57
|
+
|
|
58
|
+
async def on_completed(payload):
|
|
59
|
+
completed_future.set_result(payload)
|
|
60
|
+
|
|
61
|
+
runtime.register_event(
|
|
62
|
+
EventType.Name(EventType.EVENT_TYPE_EMBEDDING_GENERATION_COMPLETED), on_completed
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Create memory
|
|
66
|
+
memory_id = str(uuid.uuid4())
|
|
67
|
+
memory = Memory(
|
|
68
|
+
id=memory_id,
|
|
69
|
+
content=Content(
|
|
70
|
+
text="A very long message that should trigger intent generation and then embedding."
|
|
71
|
+
),
|
|
72
|
+
room_id=str(uuid.uuid4()),
|
|
73
|
+
entity_id=str(uuid.uuid4()),
|
|
74
|
+
agent_id=str(runtime.agent_id),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Emit request
|
|
78
|
+
# Construct proper EventPayload
|
|
79
|
+
# EventPayload (protobuf) expects 'extra' to be a Struct or map<string, Value>
|
|
80
|
+
# But for python test convenience, we can use a mock that has attributes
|
|
81
|
+
|
|
82
|
+
# Actually EventPayload 'extra' field is google.protobuf.Struct usually
|
|
83
|
+
# But here we just need an object with .extra attribute for our code to work
|
|
84
|
+
# Or we can use the actual proto class
|
|
85
|
+
|
|
86
|
+
# Let's use a simple Namespace
|
|
87
|
+
from types import SimpleNamespace
|
|
88
|
+
|
|
89
|
+
payload = SimpleNamespace(extra={"memory": memory})
|
|
90
|
+
|
|
91
|
+
# Manually trigger handler because emit_event in MockRuntime doesn't wait for queue processing
|
|
92
|
+
# In real runtime, emit_event calls handlers. embedding service handler writes to queue.
|
|
93
|
+
# Worker reads from queue.
|
|
94
|
+
|
|
95
|
+
event_name = EventType.Name(EventType.EVENT_TYPE_EMBEDDING_GENERATION_REQUESTED)
|
|
96
|
+
await runtime.emit_event(event_name, payload)
|
|
97
|
+
|
|
98
|
+
# Wait for completion
|
|
99
|
+
try:
|
|
100
|
+
result = await asyncio.wait_for(completed_future, timeout=5.0)
|
|
101
|
+
self.assertEqual(result["memory_id"], str(memory_id))
|
|
102
|
+
except TimeoutError:
|
|
103
|
+
print("TIMEOUT! Logging errors:")
|
|
104
|
+
for call in runtime.logger.error.call_args_list:
|
|
105
|
+
print(call)
|
|
106
|
+
for call in runtime.logger.warning.call_args_list:
|
|
107
|
+
print(call)
|
|
108
|
+
self.fail("Embedding generation timed out")
|
|
109
|
+
|
|
110
|
+
# Verify db update
|
|
111
|
+
runtime._adapter.update_memory.assert_called_once()
|
|
112
|
+
call_args = runtime._adapter.update_memory.call_args
|
|
113
|
+
updated_memory = call_args[0][0]
|
|
114
|
+
self.assertIsNotNone(updated_memory.embedding)
|
|
115
|
+
self.assertEqual(len(updated_memory.embedding), 384)
|
|
116
|
+
# Check intent (metadata update)
|
|
117
|
+
# Verify in custom metadata
|
|
118
|
+
self.assertEqual(updated_memory.metadata.custom.custom_data["intent"], "intent")
|
|
119
|
+
|
|
120
|
+
await service.stop()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
unittest.main()
|
package/tests/test_autonomy.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
from unittest.mock import AsyncMock, MagicMock
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
@@ -182,13 +183,14 @@ class TestAutonomyService:
|
|
|
182
183
|
test_runtime.get_setting = MagicMock(return_value=str(target_room_id))
|
|
183
184
|
|
|
184
185
|
dup_id = as_uuid(TEST_MESSAGE_ID)
|
|
186
|
+
now_ms = int(time.time() * 1000)
|
|
185
187
|
older = Memory(
|
|
186
188
|
id=dup_id,
|
|
187
189
|
room_id=target_room_id,
|
|
188
190
|
entity_id=as_uuid(TEST_ENTITY_ID),
|
|
189
191
|
agent_id=as_uuid(TEST_AGENT_ID),
|
|
190
192
|
content=Content(text="old"),
|
|
191
|
-
created_at=
|
|
193
|
+
created_at=now_ms - 60_000, # 1 minute ago (within 1-hour window)
|
|
192
194
|
)
|
|
193
195
|
newer = Memory(
|
|
194
196
|
id=dup_id,
|
|
@@ -196,7 +198,7 @@ class TestAutonomyService:
|
|
|
196
198
|
entity_id=as_uuid(TEST_ENTITY_ID),
|
|
197
199
|
agent_id=as_uuid(TEST_AGENT_ID),
|
|
198
200
|
content=Content(text="new"),
|
|
199
|
-
created_at=
|
|
201
|
+
created_at=now_ms - 30_000, # 30 seconds ago (within 1-hour window)
|
|
200
202
|
)
|
|
201
203
|
|
|
202
204
|
async def get_memories(params):
|
|
@@ -445,11 +447,20 @@ class TestAutonomyIntegration:
|
|
|
445
447
|
AutonomyService,
|
|
446
448
|
admin_chat_provider,
|
|
447
449
|
autonomy_status_provider,
|
|
450
|
+
disable_autonomy_action,
|
|
451
|
+
enable_autonomy_action,
|
|
452
|
+
post_action_evaluator,
|
|
448
453
|
send_to_admin_action,
|
|
449
454
|
)
|
|
450
455
|
|
|
451
456
|
assert AutonomyService is not None
|
|
452
457
|
assert AUTONOMY_SERVICE_TYPE == "AUTONOMY"
|
|
453
458
|
assert send_to_admin_action is not None
|
|
459
|
+
assert enable_autonomy_action is not None
|
|
460
|
+
assert enable_autonomy_action.name == "ENABLE_AUTONOMY"
|
|
461
|
+
assert disable_autonomy_action is not None
|
|
462
|
+
assert disable_autonomy_action.name == "DISABLE_AUTONOMY"
|
|
463
|
+
assert post_action_evaluator is not None
|
|
464
|
+
assert post_action_evaluator.name == "POST_ACTION_EVALUATOR"
|
|
454
465
|
assert admin_chat_provider is not None
|
|
455
466
|
assert autonomy_status_provider is not None
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from elizaos.types.memory import Memory
|
|
4
|
+
from elizaos.types.primitives import Content
|
|
5
|
+
from elizaos.utils.validation import validate_action_keywords, validate_action_regex
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_mock_memory(text: str, id: str = "1") -> Memory:
|
|
9
|
+
return Memory(
|
|
10
|
+
id=id,
|
|
11
|
+
entity_id="user1",
|
|
12
|
+
room_id="room1",
|
|
13
|
+
agent_id="agent1",
|
|
14
|
+
content=Content(text=text),
|
|
15
|
+
created_at=int(time.time() * 1000),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_validate_action_keywords():
|
|
20
|
+
mock_message = Memory(
|
|
21
|
+
id="123",
|
|
22
|
+
entity_id="user1",
|
|
23
|
+
room_id="room1",
|
|
24
|
+
agent_id="agent1",
|
|
25
|
+
content=Content(text="Hello world"),
|
|
26
|
+
created_at=int(time.time() * 1000),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
mock_recent_messages = [
|
|
30
|
+
Memory(
|
|
31
|
+
id="1",
|
|
32
|
+
entity_id="user1",
|
|
33
|
+
room_id="room1",
|
|
34
|
+
agent_id="agent1",
|
|
35
|
+
content=Content(text="Previous message 1"),
|
|
36
|
+
created_at=0,
|
|
37
|
+
),
|
|
38
|
+
Memory(
|
|
39
|
+
id="2",
|
|
40
|
+
entity_id="user1",
|
|
41
|
+
room_id="room1",
|
|
42
|
+
agent_id="agent1",
|
|
43
|
+
content=Content(text="Previous message 2"),
|
|
44
|
+
created_at=0,
|
|
45
|
+
),
|
|
46
|
+
Memory(
|
|
47
|
+
id="3",
|
|
48
|
+
entity_id="user1",
|
|
49
|
+
room_id="room1",
|
|
50
|
+
agent_id="agent1",
|
|
51
|
+
content=Content(text="Crypto is cool"),
|
|
52
|
+
created_at=0,
|
|
53
|
+
),
|
|
54
|
+
Memory(
|
|
55
|
+
id="4",
|
|
56
|
+
entity_id="user1",
|
|
57
|
+
room_id="room1",
|
|
58
|
+
agent_id="agent1",
|
|
59
|
+
content=Content(text="Another message"),
|
|
60
|
+
created_at=0,
|
|
61
|
+
),
|
|
62
|
+
Memory(
|
|
63
|
+
id="5",
|
|
64
|
+
entity_id="user1",
|
|
65
|
+
room_id="room1",
|
|
66
|
+
agent_id="agent1",
|
|
67
|
+
content=Content(text="Last one"),
|
|
68
|
+
created_at=0,
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# 1. Keyword in current message
|
|
73
|
+
msg = create_mock_memory("I want to transfer sol", "124")
|
|
74
|
+
assert validate_action_keywords(msg, [], ["transfer"])
|
|
75
|
+
|
|
76
|
+
# 2. Keyword in recent messages
|
|
77
|
+
assert validate_action_keywords(mock_message, mock_recent_messages, ["crypto"])
|
|
78
|
+
|
|
79
|
+
# 3. Keyword not found
|
|
80
|
+
assert not validate_action_keywords(mock_message, mock_recent_messages, ["banana"])
|
|
81
|
+
|
|
82
|
+
# 4. Case insensitive
|
|
83
|
+
msg_upper = create_mock_memory("I want to TRANSFER sol", "125")
|
|
84
|
+
assert validate_action_keywords(msg_upper, [], ["transfer"])
|
|
85
|
+
|
|
86
|
+
# 5. Empty keywords list
|
|
87
|
+
assert not validate_action_keywords(mock_message, mock_recent_messages, [])
|
|
88
|
+
|
|
89
|
+
# 6. Partial match
|
|
90
|
+
msg_partial = Memory(
|
|
91
|
+
id="126",
|
|
92
|
+
entity_id="user1",
|
|
93
|
+
room_id="room1",
|
|
94
|
+
agent_id="agent1",
|
|
95
|
+
content=Content(text="cryptography"),
|
|
96
|
+
created_at=0,
|
|
97
|
+
)
|
|
98
|
+
assert validate_action_keywords(msg_partial, [], ["crypto"])
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_validate_action_regex():
|
|
102
|
+
mock_message = create_mock_memory("Hello world", "123")
|
|
103
|
+
mock_recent_messages = [
|
|
104
|
+
create_mock_memory("Previous message 1", "1"),
|
|
105
|
+
create_mock_memory("Previous message 2", "2"),
|
|
106
|
+
create_mock_memory("Crypto is cool", "3"),
|
|
107
|
+
create_mock_memory("Another message", "4"),
|
|
108
|
+
create_mock_memory("Last one", "5"),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# Regex in current message
|
|
112
|
+
msg = create_mock_memory("Transfer 100 SOL")
|
|
113
|
+
# Default re.search is case-sensitive
|
|
114
|
+
assert not validate_action_regex(msg, [], r"transfer \d+ sol")
|
|
115
|
+
# Use inline flag for case-insensitive
|
|
116
|
+
assert validate_action_regex(msg, [], r"(?i)transfer \d+ sol")
|
|
117
|
+
|
|
118
|
+
# Regex in recent messages
|
|
119
|
+
assert validate_action_regex(mock_message, mock_recent_messages, r"(?i)crypto")
|
|
120
|
+
|
|
121
|
+
# No match
|
|
122
|
+
assert not validate_action_regex(mock_message, mock_recent_messages, r"banana")
|
|
123
|
+
|
|
124
|
+
# Complex regex
|
|
125
|
+
msg = create_mock_memory("user@example.com")
|
|
126
|
+
assert validate_action_regex(msg, [], r"^[\w\.-]+@([\w-]+\.)+[\w-]{2,4}$") # Empty pattern
|
|
127
|
+
assert not validate_action_regex(mock_message, mock_recent_messages, "")
|
|
128
|
+
|
|
129
|
+
# Unicode characters
|
|
130
|
+
msg = create_mock_memory("Transfer 100 €")
|
|
131
|
+
assert not validate_action_regex(msg, [], r"transfer \d+ €") # case sensitive
|
|
132
|
+
assert validate_action_regex(msg, [], r"(?i)transfer \d+ €")
|
|
133
|
+
|
|
134
|
+
# Special characters
|
|
135
|
+
msg = create_mock_memory("Hello (world) [ok]")
|
|
136
|
+
assert validate_action_regex(msg, [], r"\(world\)")
|
|
137
|
+
|
|
138
|
+
# Long inputs (basic DoS check)
|
|
139
|
+
long_text = "a" * 10000 + "transfer"
|
|
140
|
+
msg = create_mock_memory(long_text)
|
|
141
|
+
assert validate_action_regex(msg, [], r"transfer")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import math
|
|
3
|
+
import unittest
|
|
4
|
+
import uuid
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
6
|
+
|
|
7
|
+
from elizaos.bootstrap.services.embedding import EmbeddingService
|
|
8
|
+
from elizaos.types import ModelType
|
|
9
|
+
from elizaos.types.events import EventType
|
|
10
|
+
from elizaos.types.memory import Memory
|
|
11
|
+
from elizaos.types.primitives import Content
|
|
12
|
+
|
|
13
|
+
# Mock vector for "hello world" - simple 384 dim vector
|
|
14
|
+
MOCK_VECTOR_HELLO = [0.1] * 384
|
|
15
|
+
# Mock vector for something else - orthogonal or different
|
|
16
|
+
MOCK_VECTOR_OTHER = [-0.1] * 384
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MockRuntime(MagicMock):
|
|
20
|
+
def __init__(self, *args, **kwargs):
|
|
21
|
+
super().__init__(*args, **kwargs)
|
|
22
|
+
self.agent_id = uuid.uuid4()
|
|
23
|
+
self.logger = MagicMock()
|
|
24
|
+
self.events = {}
|
|
25
|
+
self._adapter = AsyncMock()
|
|
26
|
+
self._models = {}
|
|
27
|
+
|
|
28
|
+
# Setup model mock
|
|
29
|
+
async def use_model_side_effect(model_type, **kwargs):
|
|
30
|
+
if model_type == ModelType.TEXT_EMBEDDING:
|
|
31
|
+
text = kwargs.get("text", "")
|
|
32
|
+
if "hello" in text.lower():
|
|
33
|
+
return MOCK_VECTOR_HELLO
|
|
34
|
+
return MOCK_VECTOR_OTHER
|
|
35
|
+
if model_type == ModelType.TEXT_SMALL:
|
|
36
|
+
return "intent"
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
self.use_model = AsyncMock(side_effect=use_model_side_effect)
|
|
40
|
+
|
|
41
|
+
def register_event(self, event, handler):
|
|
42
|
+
if event not in self.events:
|
|
43
|
+
self.events[event] = []
|
|
44
|
+
self.events[event].append(handler)
|
|
45
|
+
|
|
46
|
+
async def emit_event(self, event, payload):
|
|
47
|
+
handlers = self.events.get(event, [])
|
|
48
|
+
for handler in handlers:
|
|
49
|
+
if asyncio.iscoroutinefunction(handler):
|
|
50
|
+
await handler(payload)
|
|
51
|
+
else:
|
|
52
|
+
handler(payload)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def dot_product(v1: list[float], v2: list[float]) -> float:
|
|
56
|
+
return sum(x * y for x, y in zip(v1, v2, strict=False))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def magnitude(v: list[float]) -> float:
|
|
60
|
+
return math.sqrt(sum(x * x for x in v))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def cosine_similarity(v1: list[float], v2: list[float]) -> float:
|
|
64
|
+
m1 = magnitude(v1)
|
|
65
|
+
m2 = magnitude(v2)
|
|
66
|
+
if m1 == 0 or m2 == 0:
|
|
67
|
+
return 0.0
|
|
68
|
+
return dot_product(v1, v2) / (m1 * m2)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class VerifyMemoryArchitecture(unittest.IsolatedAsyncioTestCase):
|
|
72
|
+
async def test_end_to_end_memory_flow(self):
|
|
73
|
+
print("\n=== Starting Architectural Verification ===")
|
|
74
|
+
|
|
75
|
+
# 1. Setup
|
|
76
|
+
runtime = MockRuntime()
|
|
77
|
+
service = await EmbeddingService.start(runtime)
|
|
78
|
+
|
|
79
|
+
# --- TEST CASE 1: Short Message (Direct Embedding) ---
|
|
80
|
+
print("\n[Test 1] Short Message (< 20 chars) -> Direct Embedding")
|
|
81
|
+
|
|
82
|
+
short_id = str(uuid.uuid4())
|
|
83
|
+
short_memory = Memory(
|
|
84
|
+
id=short_id,
|
|
85
|
+
content=Content(text="Hello World"), # < 20 chars
|
|
86
|
+
room_id=str(uuid.uuid4()),
|
|
87
|
+
entity_id=str(uuid.uuid4()),
|
|
88
|
+
agent_id=str(runtime.agent_id),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Reset Update Memory Mock
|
|
92
|
+
runtime._adapter.update_memory.reset_mock()
|
|
93
|
+
|
|
94
|
+
# Trigger
|
|
95
|
+
from types import SimpleNamespace
|
|
96
|
+
|
|
97
|
+
payload = SimpleNamespace(extra={"memory": short_memory})
|
|
98
|
+
await self._run_pipeline(runtime, payload)
|
|
99
|
+
|
|
100
|
+
# Verify
|
|
101
|
+
runtime._adapter.update_memory.assert_called_once()
|
|
102
|
+
stored_short = runtime._adapter.update_memory.call_args[0][0]
|
|
103
|
+
|
|
104
|
+
# "Hello World" contains "hello" -> MOCK_VECTOR_HELLO
|
|
105
|
+
sim = cosine_similarity(list(stored_short.embedding), MOCK_VECTOR_HELLO)
|
|
106
|
+
self.assertAlmostEqual(
|
|
107
|
+
sim, 1.0, places=4, msg="Short message should use direct embedding (Hello)"
|
|
108
|
+
)
|
|
109
|
+
print(f" -> Verified: Short message embedding matches content (sim={sim:.4f})")
|
|
110
|
+
|
|
111
|
+
# --- TEST CASE 2: Long Message (Intent Embedding) ---
|
|
112
|
+
print("\n[Test 2] Long Message (> 20 chars) -> Intent Embedding")
|
|
113
|
+
|
|
114
|
+
long_id = str(uuid.uuid4())
|
|
115
|
+
# "Hello World" repeated to be long, but also contains "hello"
|
|
116
|
+
# However, the logic generates INTENT.
|
|
117
|
+
# Mock returns "intent" as intent text.
|
|
118
|
+
# "intent" does NOT contain "hello", so mock returns MOCK_VECTOR_OTHER.
|
|
119
|
+
long_memory = Memory(
|
|
120
|
+
id=long_id,
|
|
121
|
+
content=Content(text="Hello World " * 5),
|
|
122
|
+
room_id=str(uuid.uuid4()),
|
|
123
|
+
entity_id=str(uuid.uuid4()),
|
|
124
|
+
agent_id=str(runtime.agent_id),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
runtime._adapter.update_memory.reset_mock()
|
|
128
|
+
|
|
129
|
+
payload_long = SimpleNamespace(extra={"memory": long_memory})
|
|
130
|
+
await self._run_pipeline(runtime, payload_long)
|
|
131
|
+
|
|
132
|
+
runtime._adapter.update_memory.assert_called_once()
|
|
133
|
+
stored_long = runtime._adapter.update_memory.call_args[0][0]
|
|
134
|
+
|
|
135
|
+
# Expect MOCK_VECTOR_OTHER because embedding was on "intent"
|
|
136
|
+
sim_intent = cosine_similarity(list(stored_long.embedding), MOCK_VECTOR_OTHER)
|
|
137
|
+
self.assertAlmostEqual(
|
|
138
|
+
sim_intent, 1.0, places=4, msg="Long message should use intent embedding"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Verify metadata
|
|
142
|
+
self.assertEqual(stored_long.metadata.custom.custom_data["intent"], "intent")
|
|
143
|
+
print(f" -> Verified: Long message uses intent embedding (sim={sim_intent:.4f})")
|
|
144
|
+
print(" -> Verified: Intent metadata stored")
|
|
145
|
+
|
|
146
|
+
# 4. Verify Retrieval & Similarity
|
|
147
|
+
print("\n[3] Verifying Retrieval & Similarity logic...")
|
|
148
|
+
# "Hello there" -> MOCK_VECTOR_HELLO
|
|
149
|
+
# "General Kenobi" -> MOCK_VECTOR_OTHER
|
|
150
|
+
score = await service.similarity("Hello there", "General Kenobi")
|
|
151
|
+
|
|
152
|
+
# Expected: dot(HELLO, OTHER)
|
|
153
|
+
# 384 * (0.1 * -0.1) = 384 * -0.01 = -3.84
|
|
154
|
+
# Mag(HELLO) = sqrt(384 * 0.01) = sqrt(3.84) ~= 1.9596
|
|
155
|
+
# Mag(OTHER) = sqrt(384 * 0.01) = sqrt(3.84) ~= 1.9596
|
|
156
|
+
# Cos = -3.84 / (1.9596 * 1.9596) = -3.84 / 3.84 = -1.0
|
|
157
|
+
expected_sim = cosine_similarity(MOCK_VECTOR_HELLO, MOCK_VECTOR_OTHER)
|
|
158
|
+
self.assertAlmostEqual(score, expected_sim, places=4)
|
|
159
|
+
print(f" -> Similarity calculation verified: {score:.4f} (expected {expected_sim:.4f})")
|
|
160
|
+
|
|
161
|
+
await service.stop()
|
|
162
|
+
print("=== Verification Complete ===")
|
|
163
|
+
|
|
164
|
+
async def _run_pipeline(self, runtime, payload):
|
|
165
|
+
event_name = EventType.Name(EventType.EVENT_TYPE_EMBEDDING_GENERATION_REQUESTED)
|
|
166
|
+
completion_future = asyncio.Future()
|
|
167
|
+
|
|
168
|
+
async def on_complete(p):
|
|
169
|
+
if not completion_future.done():
|
|
170
|
+
completion_future.set_result(p)
|
|
171
|
+
|
|
172
|
+
# We need to register/unregister to avoid duplicate calls if running multiple times
|
|
173
|
+
# But MockRuntime implementation appends.
|
|
174
|
+
# For simplicity, just append and ensure we trigger the right future?
|
|
175
|
+
# A new future is needed for each run.
|
|
176
|
+
# Let's clear mocks events for clean slate or just handle it.
|
|
177
|
+
# We can just register a new one.
|
|
178
|
+
|
|
179
|
+
runtime.register_event(
|
|
180
|
+
EventType.Name(EventType.EVENT_TYPE_EMBEDDING_GENERATION_COMPLETED), on_complete
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
await runtime.emit_event(event_name, payload)
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
await asyncio.wait_for(completion_future, timeout=2.0)
|
|
187
|
+
except TimeoutError:
|
|
188
|
+
self.fail("Async pipeline timed out")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
if __name__ == "__main__":
|
|
192
|
+
unittest.main()
|