@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.
Files changed (57) hide show
  1. package/elizaos/advanced_capabilities/__init__.py +6 -41
  2. package/elizaos/advanced_capabilities/actions/__init__.py +1 -21
  3. package/elizaos/advanced_capabilities/actions/add_contact.py +21 -11
  4. package/elizaos/advanced_capabilities/actions/follow_room.py +28 -28
  5. package/elizaos/advanced_capabilities/actions/image_generation.py +13 -26
  6. package/elizaos/advanced_capabilities/actions/mute_room.py +13 -26
  7. package/elizaos/advanced_capabilities/actions/remove_contact.py +16 -2
  8. package/elizaos/advanced_capabilities/actions/roles.py +13 -27
  9. package/elizaos/advanced_capabilities/actions/search_contacts.py +17 -3
  10. package/elizaos/advanced_capabilities/actions/send_message.py +317 -9
  11. package/elizaos/advanced_capabilities/actions/settings.py +16 -2
  12. package/elizaos/advanced_capabilities/actions/unfollow_room.py +13 -26
  13. package/elizaos/advanced_capabilities/actions/unmute_room.py +13 -26
  14. package/elizaos/advanced_capabilities/actions/update_contact.py +16 -2
  15. package/elizaos/advanced_capabilities/actions/update_entity.py +16 -2
  16. package/elizaos/advanced_capabilities/evaluators/__init__.py +2 -9
  17. package/elizaos/advanced_capabilities/evaluators/reflection.py +3 -132
  18. package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +5 -201
  19. package/elizaos/advanced_capabilities/providers/__init__.py +1 -12
  20. package/elizaos/advanced_capabilities/providers/knowledge.py +24 -3
  21. package/elizaos/advanced_capabilities/services/__init__.py +2 -9
  22. package/elizaos/advanced_memory/actions/reset_session.py +11 -0
  23. package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
  24. package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
  25. package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
  26. package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
  27. package/elizaos/basic_capabilities/__init__.py +0 -2
  28. package/elizaos/basic_capabilities/providers/__init__.py +0 -3
  29. package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
  30. package/elizaos/basic_capabilities/providers/contacts.py +79 -0
  31. package/elizaos/basic_capabilities/providers/facts.py +87 -0
  32. package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
  33. package/elizaos/basic_capabilities/providers/knowledge.py +97 -0
  34. package/elizaos/basic_capabilities/providers/relationships.py +107 -0
  35. package/elizaos/basic_capabilities/providers/roles.py +96 -0
  36. package/elizaos/basic_capabilities/providers/settings.py +56 -0
  37. package/elizaos/bootstrap/autonomy/__init__.py +5 -1
  38. package/elizaos/bootstrap/autonomy/action.py +161 -0
  39. package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
  40. package/elizaos/bootstrap/autonomy/service.py +8 -0
  41. package/elizaos/bootstrap/plugin.py +7 -0
  42. package/elizaos/bootstrap/providers/knowledge.py +26 -3
  43. package/elizaos/bootstrap/services/embedding.py +156 -1
  44. package/elizaos/runtime.py +63 -18
  45. package/elizaos/services/message_service.py +173 -23
  46. package/elizaos/types/generated/eliza/v1/agent_pb2.py +16 -16
  47. package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +2 -4
  48. package/elizaos/types/model.py +27 -0
  49. package/elizaos/types/runtime.py +5 -1
  50. package/elizaos/utils/validation.py +76 -0
  51. package/package.json +2 -2
  52. package/tests/test_actions_provider_examples.py +58 -1
  53. package/tests/test_async_embedding.py +124 -0
  54. package/tests/test_autonomy.py +13 -2
  55. package/tests/test_validation.py +141 -0
  56. package/tests/verify_memory_architecture.py +192 -0
  57. 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.11",
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": "33e7c7ee0c64f519085218c9f53997a6c5740808"
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()
@@ -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=10,
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=20,
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()