@elizaos/python 2.0.0-alpha.3 → 2.0.0-alpha.30
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 +24 -13
- package/elizaos/advanced_capabilities/actions/follow_room.py +29 -29
- package/elizaos/advanced_capabilities/actions/image_generation.py +15 -28
- package/elizaos/advanced_capabilities/actions/mute_room.py +15 -28
- package/elizaos/advanced_capabilities/actions/remove_contact.py +17 -3
- package/elizaos/advanced_capabilities/actions/roles.py +17 -30
- 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 +184 -51
- package/elizaos/advanced_capabilities/actions/settings.py +17 -3
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +15 -28
- package/elizaos/advanced_capabilities/actions/unmute_room.py +15 -28
- package/elizaos/advanced_capabilities/actions/update_contact.py +17 -3
- package/elizaos/advanced_capabilities/actions/update_entity.py +17 -3
- 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 +23 -3
- package/elizaos/advanced_capabilities/services/__init__.py +2 -9
- package/elizaos/advanced_capabilities/services/rolodex.py +2 -2
- package/elizaos/advanced_memory/actions/reset_session.py +143 -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 +69 -27
- package/elizaos/advanced_memory/plugin.py +2 -1
- package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
- package/elizaos/advanced_memory/types.py +2 -2
- 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 +96 -0
- package/elizaos/basic_capabilities/providers/recent_messages.py +5 -0
- package/elizaos/basic_capabilities/providers/relationships.py +113 -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/basic_capabilities/services/embedding.py +10 -7
- package/elizaos/basic_capabilities/services/task.py +3 -3
- package/elizaos/bootstrap/__init__.py +21 -2
- package/elizaos/bootstrap/actions/__init__.py +3 -0
- package/elizaos/bootstrap/actions/reset_session.py +3 -0
- package/elizaos/bootstrap/actions/roles.py +5 -4
- 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 +26 -3
- package/elizaos/bootstrap/providers/providers_list.py +1 -0
- package/elizaos/bootstrap/providers/recent_messages.py +5 -0
- package/elizaos/bootstrap/providers/relationships.py +20 -13
- 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 +206 -8
- package/elizaos/bootstrap/services/rolodex.py +2 -2
- package/elizaos/bootstrap/services/task.py +3 -3
- 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 +4 -4
- package/elizaos/media/search.py +23 -23
- package/elizaos/runtime.py +223 -64
- package/elizaos/services/hook_service.py +3 -3
- 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 +33 -3
- package/elizaos/types/primitives.py +3 -3
- package/elizaos/types/runtime.py +17 -3
- package/elizaos/types/state.py +2 -2
- package/elizaos/utils/streaming.py +3 -3
- package/elizaos/utils/validation.py +76 -0
- package/package.json +4 -3
- package/pyproject.toml +1 -2
- package/requirements-dev.lock +2 -2
- package/requirements.in +1 -2
- package/requirements.lock +2 -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_history_compaction.py +104 -0
- package/tests/test_memory_bounds.py +115 -0
- 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 Any
|
|
|
4
4
|
from uuid import uuid4
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
+
from google.protobuf.json_format import MessageToDict
|
|
7
8
|
|
|
8
9
|
from elizaos.advanced_planning.planning_service import ActionPlan, ActionStep
|
|
9
10
|
from elizaos.runtime import AgentRuntime
|
|
@@ -15,7 +16,6 @@ from elizaos.types.model import ModelType
|
|
|
15
16
|
from elizaos.types.primitives import Content, as_uuid
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
@pytest.mark.skip(reason="State.values.extra access issue with protobuf")
|
|
19
19
|
@pytest.mark.asyncio
|
|
20
20
|
async def test_advanced_planning_provider_parses_model_output() -> None:
|
|
21
21
|
character = Character(name="AdvPlanningProvider", bio=["Test"], advanced_planning=True)
|
|
@@ -49,7 +49,13 @@ async def test_advanced_planning_provider_parses_model_output() -> None:
|
|
|
49
49
|
state = await runtime.compose_state(msg)
|
|
50
50
|
result = await provider.get(runtime, msg, state)
|
|
51
51
|
assert result.data is not None
|
|
52
|
-
|
|
52
|
+
parsed_data = (
|
|
53
|
+
MessageToDict(result.data, preserving_proto_field_name=True)
|
|
54
|
+
if hasattr(result.data, "DESCRIPTOR")
|
|
55
|
+
else result.data
|
|
56
|
+
)
|
|
57
|
+
assert isinstance(parsed_data, dict)
|
|
58
|
+
assert parsed_data.get("planningRequired") is True
|
|
53
59
|
|
|
54
60
|
|
|
55
61
|
@pytest.mark.asyncio
|
|
@@ -72,7 +78,6 @@ async def test_advanced_planning_service_creates_simple_plan() -> None:
|
|
|
72
78
|
assert any(step.action_name == "SEND_EMAIL" for step in plan.steps)
|
|
73
79
|
|
|
74
80
|
|
|
75
|
-
@pytest.mark.skip(reason="State.values.extra access issue with protobuf")
|
|
76
81
|
@pytest.mark.asyncio
|
|
77
82
|
async def test_advanced_planning_service_creates_comprehensive_plan_and_executes() -> None:
|
|
78
83
|
character = Character(name="AdvPlanningSvcExec", bio=["Test"], advanced_planning=True)
|
|
@@ -124,7 +129,6 @@ async def test_advanced_planning_service_creates_comprehensive_plan_and_executes
|
|
|
124
129
|
assert result.total_steps >= 1
|
|
125
130
|
|
|
126
131
|
|
|
127
|
-
@pytest.mark.skip(reason="validate lambda returns bool, not coroutine")
|
|
128
132
|
@pytest.mark.asyncio
|
|
129
133
|
async def test_advanced_planning_dag_executes_in_dependency_order() -> None:
|
|
130
134
|
character = Character(name="AdvPlanningDag", bio=["Test"], advanced_planning=True)
|
|
@@ -220,6 +224,8 @@ async def test_advanced_planning_dag_executes_in_dependency_order() -> None:
|
|
|
220
224
|
content=Content(text="run"),
|
|
221
225
|
)
|
|
222
226
|
state = await runtime.compose_state(msg)
|
|
223
|
-
await planning_service.execute_plan(plan, msg, state=state, callback=None)
|
|
227
|
+
execution_result = await planning_service.execute_plan(plan, msg, state=state, callback=None)
|
|
224
228
|
|
|
229
|
+
assert execution_result.success is True
|
|
230
|
+
assert execution_result.completed_steps == 3
|
|
225
231
|
assert execution_order == ["STEP_A", "STEP_B", "STEP_C"]
|
|
@@ -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
|
|
@@ -11,9 +12,10 @@ from elizaos.bootstrap.autonomy import (
|
|
|
11
12
|
autonomy_status_provider,
|
|
12
13
|
send_to_admin_action,
|
|
13
14
|
)
|
|
15
|
+
from elizaos.bootstrap.autonomy.service import AUTONOMY_TASK_TAGS
|
|
14
16
|
from elizaos.bootstrap.autonomy.types import AutonomyStatus
|
|
15
17
|
from elizaos.types.memory import Memory
|
|
16
|
-
from elizaos.types.primitives import Content, as_uuid
|
|
18
|
+
from elizaos.types.primitives import Content, as_uuid, string_to_uuid
|
|
17
19
|
|
|
18
20
|
TEST_AGENT_ID = "00000000-0000-0000-0000-000000000001"
|
|
19
21
|
TEST_ROOM_ID = "00000000-0000-0000-0000-000000000002"
|
|
@@ -75,6 +77,9 @@ class TestAutonomyService:
|
|
|
75
77
|
assert AutonomyService.service_type == AUTONOMY_SERVICE_TYPE
|
|
76
78
|
assert AutonomyService.service_type == "AUTONOMY"
|
|
77
79
|
|
|
80
|
+
def test_task_tags_match_typescript(self):
|
|
81
|
+
assert AUTONOMY_TASK_TAGS == ["queue", "repeat", "autonomy"]
|
|
82
|
+
|
|
78
83
|
@pytest.mark.asyncio
|
|
79
84
|
async def test_start_creates_service(self, test_runtime):
|
|
80
85
|
service = await AutonomyService.start(test_runtime)
|
|
@@ -85,6 +90,12 @@ class TestAutonomyService:
|
|
|
85
90
|
assert service.get_loop_interval() == 30000
|
|
86
91
|
assert service.get_autonomous_room_id() is not None
|
|
87
92
|
|
|
93
|
+
@pytest.mark.asyncio
|
|
94
|
+
async def test_autonomous_room_id_is_deterministic(self, test_runtime):
|
|
95
|
+
service = await AutonomyService.start(test_runtime)
|
|
96
|
+
expected_room_id = as_uuid(string_to_uuid(f"autonomy-room-{test_runtime.agent_id}"))
|
|
97
|
+
assert service.get_autonomous_room_id() == expected_room_id
|
|
98
|
+
|
|
88
99
|
@pytest.mark.asyncio
|
|
89
100
|
async def test_auto_start_when_enabled(self, test_runtime):
|
|
90
101
|
test_runtime.enable_autonomy = True
|
|
@@ -172,13 +183,14 @@ class TestAutonomyService:
|
|
|
172
183
|
test_runtime.get_setting = MagicMock(return_value=str(target_room_id))
|
|
173
184
|
|
|
174
185
|
dup_id = as_uuid(TEST_MESSAGE_ID)
|
|
186
|
+
now_ms = int(time.time() * 1000)
|
|
175
187
|
older = Memory(
|
|
176
188
|
id=dup_id,
|
|
177
189
|
room_id=target_room_id,
|
|
178
190
|
entity_id=as_uuid(TEST_ENTITY_ID),
|
|
179
191
|
agent_id=as_uuid(TEST_AGENT_ID),
|
|
180
192
|
content=Content(text="old"),
|
|
181
|
-
created_at=
|
|
193
|
+
created_at=now_ms - 60_000, # 1 minute ago (within 1-hour window)
|
|
182
194
|
)
|
|
183
195
|
newer = Memory(
|
|
184
196
|
id=dup_id,
|
|
@@ -186,7 +198,7 @@ class TestAutonomyService:
|
|
|
186
198
|
entity_id=as_uuid(TEST_ENTITY_ID),
|
|
187
199
|
agent_id=as_uuid(TEST_AGENT_ID),
|
|
188
200
|
content=Content(text="new"),
|
|
189
|
-
created_at=
|
|
201
|
+
created_at=now_ms - 30_000, # 30 seconds ago (within 1-hour window)
|
|
190
202
|
)
|
|
191
203
|
|
|
192
204
|
async def get_memories(params):
|
|
@@ -435,11 +447,20 @@ class TestAutonomyIntegration:
|
|
|
435
447
|
AutonomyService,
|
|
436
448
|
admin_chat_provider,
|
|
437
449
|
autonomy_status_provider,
|
|
450
|
+
disable_autonomy_action,
|
|
451
|
+
enable_autonomy_action,
|
|
452
|
+
post_action_evaluator,
|
|
438
453
|
send_to_admin_action,
|
|
439
454
|
)
|
|
440
455
|
|
|
441
456
|
assert AutonomyService is not None
|
|
442
457
|
assert AUTONOMY_SERVICE_TYPE == "AUTONOMY"
|
|
443
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"
|
|
444
465
|
assert admin_chat_provider is not None
|
|
445
466
|
assert autonomy_status_provider is not None
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from unittest.mock import AsyncMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from elizaos.advanced_memory.actions.reset_session import (
|
|
10
|
+
reset_session_action as advanced_reset_session_action,
|
|
11
|
+
)
|
|
12
|
+
from elizaos.basic_capabilities.providers.recent_messages import (
|
|
13
|
+
recent_messages_provider as basic_recent_messages_provider,
|
|
14
|
+
)
|
|
15
|
+
from elizaos.bootstrap.actions.reset_session import (
|
|
16
|
+
reset_session_action as bootstrap_reset_session_action,
|
|
17
|
+
)
|
|
18
|
+
from elizaos.bootstrap.providers.recent_messages import (
|
|
19
|
+
recent_messages_provider as bootstrap_recent_messages_provider,
|
|
20
|
+
)
|
|
21
|
+
from elizaos.types import Content, Memory, as_uuid
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class FakeRoom:
|
|
26
|
+
id: object
|
|
27
|
+
world_id: object | None
|
|
28
|
+
metadata: dict[str, object]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FakeRuntime:
|
|
32
|
+
def __init__(self, room: FakeRoom, *, role: str = "OWNER") -> None:
|
|
33
|
+
self.room = room
|
|
34
|
+
self.updated_room: FakeRoom | None = None
|
|
35
|
+
self.get_room = AsyncMock(return_value=room)
|
|
36
|
+
self.update_room = AsyncMock(side_effect=self._update_room)
|
|
37
|
+
self.get_memories = AsyncMock(return_value=[])
|
|
38
|
+
self.get_entity = AsyncMock(return_value=SimpleNamespace(name="User"))
|
|
39
|
+
self.agent_id = as_uuid("91000000-0000-0000-0000-000000000001")
|
|
40
|
+
self.character = SimpleNamespace(name="CompactionAgent")
|
|
41
|
+
self.get_world = AsyncMock(
|
|
42
|
+
return_value=SimpleNamespace(
|
|
43
|
+
metadata={"roles": {"91000000-0000-0000-0000-000000000002": role}}
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def _update_room(self, room: FakeRoom) -> None:
|
|
48
|
+
self.updated_room = room
|
|
49
|
+
self.room = room
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _message() -> Memory:
|
|
53
|
+
return Memory(
|
|
54
|
+
id=as_uuid("91000000-0000-0000-0000-000000000010"),
|
|
55
|
+
entity_id=as_uuid("91000000-0000-0000-0000-000000000002"),
|
|
56
|
+
room_id=as_uuid("91000000-0000-0000-0000-000000000003"),
|
|
57
|
+
content=Content(text="start over", source="test"),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
@pytest.mark.parametrize(
|
|
63
|
+
"action_under_test",
|
|
64
|
+
[bootstrap_reset_session_action, advanced_reset_session_action],
|
|
65
|
+
)
|
|
66
|
+
async def test_reset_session_action_updates_room_metadata(action_under_test: object) -> None:
|
|
67
|
+
room = FakeRoom(
|
|
68
|
+
id=as_uuid("91000000-0000-0000-0000-000000000003"),
|
|
69
|
+
world_id=as_uuid("91000000-0000-0000-0000-000000000004"),
|
|
70
|
+
metadata={
|
|
71
|
+
"lastCompactionAt": 1000,
|
|
72
|
+
"compactionHistory": [{"timestamp": 1000, "triggeredBy": "old", "reason": "manual"}],
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
runtime = FakeRuntime(room)
|
|
76
|
+
|
|
77
|
+
result = await action_under_test.handler(runtime, _message(), None, None, None, None)
|
|
78
|
+
|
|
79
|
+
assert result.success is True
|
|
80
|
+
assert runtime.updated_room is not None
|
|
81
|
+
assert runtime.updated_room.metadata["lastCompactionAt"] >= 1000
|
|
82
|
+
assert len(runtime.updated_room.metadata["compactionHistory"]) == 2
|
|
83
|
+
assert result.values["previousCompactionAt"] == 1000
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@pytest.mark.asyncio
|
|
87
|
+
@pytest.mark.parametrize(
|
|
88
|
+
"provider_under_test",
|
|
89
|
+
[bootstrap_recent_messages_provider, basic_recent_messages_provider],
|
|
90
|
+
)
|
|
91
|
+
async def test_recent_messages_provider_uses_last_compaction_boundary(
|
|
92
|
+
provider_under_test: object,
|
|
93
|
+
) -> None:
|
|
94
|
+
room = FakeRoom(
|
|
95
|
+
id=as_uuid("91000000-0000-0000-0000-000000000003"),
|
|
96
|
+
world_id=None,
|
|
97
|
+
metadata={"lastCompactionAt": 4242},
|
|
98
|
+
)
|
|
99
|
+
runtime = FakeRuntime(room)
|
|
100
|
+
|
|
101
|
+
result = await provider_under_test.get(runtime, _message(), None)
|
|
102
|
+
|
|
103
|
+
assert result.values["roomId"] == str(_message().room_id)
|
|
104
|
+
assert runtime.get_memories.await_args.kwargs["start"] == 4242
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from elizaos.advanced_memory.memory_service import MemoryService
|
|
10
|
+
from elizaos.advanced_memory.types import LongTermMemoryCategory
|
|
11
|
+
from elizaos.basic_capabilities.services.embedding import (
|
|
12
|
+
EmbeddingService as BasicEmbeddingService,
|
|
13
|
+
)
|
|
14
|
+
from elizaos.bootstrap.services.embedding import (
|
|
15
|
+
EmbeddingService as BootstrapEmbeddingService,
|
|
16
|
+
)
|
|
17
|
+
from elizaos.types import ModelType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _mock_runtime() -> MagicMock:
|
|
21
|
+
runtime = MagicMock()
|
|
22
|
+
runtime.agent_id = uuid.uuid4()
|
|
23
|
+
runtime.logger = MagicMock()
|
|
24
|
+
|
|
25
|
+
async def use_model(model_type, **_kwargs):
|
|
26
|
+
if model_type == ModelType.TEXT_EMBEDDING:
|
|
27
|
+
return [0.1, 0.2, 0.3]
|
|
28
|
+
return "ok"
|
|
29
|
+
|
|
30
|
+
runtime.use_model = AsyncMock(side_effect=use_model)
|
|
31
|
+
return runtime
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.mark.asyncio
|
|
35
|
+
async def test_embedding_services_use_lru_eviction() -> None:
|
|
36
|
+
for service_cls in (BasicEmbeddingService, BootstrapEmbeddingService):
|
|
37
|
+
runtime = _mock_runtime()
|
|
38
|
+
service = service_cls()
|
|
39
|
+
service._runtime = runtime
|
|
40
|
+
service.set_max_cache_size(2)
|
|
41
|
+
|
|
42
|
+
await service.embed("a")
|
|
43
|
+
await service.embed("b")
|
|
44
|
+
await service.embed("a")
|
|
45
|
+
await service.embed("c")
|
|
46
|
+
|
|
47
|
+
assert list(service._cache.keys()) == ["a", "c"]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_bootstrap_embedding_queue_deduplicates_and_clears_on_stop() -> None:
|
|
52
|
+
service = BootstrapEmbeddingService()
|
|
53
|
+
service._runtime = _mock_runtime()
|
|
54
|
+
|
|
55
|
+
payload = SimpleNamespace(extra={"memory": {"id": "memory-1"}})
|
|
56
|
+
|
|
57
|
+
await service._handle_embedding_request(payload)
|
|
58
|
+
await service._handle_embedding_request(payload)
|
|
59
|
+
|
|
60
|
+
assert service._queue.qsize() == 1
|
|
61
|
+
assert service._pending_payload_keys == {"memory-1"}
|
|
62
|
+
|
|
63
|
+
await service.stop()
|
|
64
|
+
|
|
65
|
+
assert service._queue.qsize() == 0
|
|
66
|
+
assert service._pending_payload_keys == set()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_advanced_memory_fallback_storage_is_bounded() -> None:
|
|
71
|
+
service = MemoryService(runtime=None)
|
|
72
|
+
agent_id = uuid.uuid4()
|
|
73
|
+
|
|
74
|
+
for index in range(service._MAX_LOCAL_SESSION_SUMMARIES + 25):
|
|
75
|
+
await service.store_session_summary(
|
|
76
|
+
agent_id=agent_id,
|
|
77
|
+
room_id=uuid.uuid4(),
|
|
78
|
+
summary=f"summary-{index}",
|
|
79
|
+
message_count=index,
|
|
80
|
+
last_message_offset=index,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
assert len(service._session_summaries) == service._MAX_LOCAL_SESSION_SUMMARIES
|
|
84
|
+
|
|
85
|
+
for index in range(service._MAX_LOCAL_EXTRACTION_CHECKPOINTS + 25):
|
|
86
|
+
await service.set_last_extraction_checkpoint(uuid.uuid4(), uuid.uuid4(), index)
|
|
87
|
+
|
|
88
|
+
assert len(service._extraction_checkpoints) == service._MAX_LOCAL_EXTRACTION_CHECKPOINTS
|
|
89
|
+
|
|
90
|
+
for _ in range(service._MAX_LOCAL_LONG_TERM_ENTITIES + 25):
|
|
91
|
+
entity_id = uuid.uuid4()
|
|
92
|
+
await service.store_long_term_memory(
|
|
93
|
+
agent_id=agent_id,
|
|
94
|
+
entity_id=entity_id,
|
|
95
|
+
category=LongTermMemoryCategory.SEMANTIC,
|
|
96
|
+
content=f"entity-{entity_id}",
|
|
97
|
+
confidence=0.9,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert len(service._long_term) == service._MAX_LOCAL_LONG_TERM_ENTITIES
|
|
101
|
+
|
|
102
|
+
hot_entity_id = uuid.uuid4()
|
|
103
|
+
for index in range(service._MAX_LOCAL_LONG_TERM_PER_ENTITY + 25):
|
|
104
|
+
await service.store_long_term_memory(
|
|
105
|
+
agent_id=agent_id,
|
|
106
|
+
entity_id=hot_entity_id,
|
|
107
|
+
category=LongTermMemoryCategory.SEMANTIC,
|
|
108
|
+
content=f"fact-{index}",
|
|
109
|
+
confidence=float(index),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
retained = service._long_term[str(hot_entity_id)]
|
|
113
|
+
assert len(retained) == service._MAX_LOCAL_LONG_TERM_PER_ENTITY
|
|
114
|
+
assert retained[0].content == "fact-25"
|
|
115
|
+
assert retained[-1].content == f"fact-{service._MAX_LOCAL_LONG_TERM_PER_ENTITY + 24}"
|
package/tests/test_runtime.py
CHANGED
|
@@ -59,17 +59,15 @@ class TestAgentRuntimeSettings:
|
|
|
59
59
|
runtime.set_setting("test_key", "test_value")
|
|
60
60
|
assert runtime.get_setting("test_key") == "test_value"
|
|
61
61
|
|
|
62
|
-
@pytest.mark.skip(reason="CharacterSettings proto doesn't support arbitrary fields")
|
|
63
62
|
def test_get_setting_from_character(self) -> None:
|
|
64
63
|
character = Character(
|
|
65
64
|
name="Test",
|
|
66
65
|
bio=["Test"],
|
|
67
|
-
settings={"char_setting": "char_value"},
|
|
66
|
+
settings={"extra": {"char_setting": "char_value"}},
|
|
68
67
|
)
|
|
69
68
|
runtime = AgentRuntime(character=character)
|
|
70
69
|
assert runtime.get_setting("char_setting") == "char_value"
|
|
71
70
|
|
|
72
|
-
@pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
|
|
73
71
|
def test_get_setting_from_secrets(self) -> None:
|
|
74
72
|
character = Character(
|
|
75
73
|
name="Test",
|
|
@@ -305,42 +303,38 @@ class TestAgentRuntimeLLMMode:
|
|
|
305
303
|
runtime = AgentRuntime(character=character, llm_mode=LLMMode.LARGE)
|
|
306
304
|
assert runtime.get_llm_mode() == LLMMode.LARGE
|
|
307
305
|
|
|
308
|
-
@pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
|
|
309
306
|
def test_character_setting_small(self) -> None:
|
|
310
307
|
character = Character(
|
|
311
308
|
name="Test",
|
|
312
309
|
bio=["Test"],
|
|
313
|
-
settings={"LLM_MODE": "SMALL"},
|
|
310
|
+
settings={"extra": {"LLM_MODE": "SMALL"}},
|
|
314
311
|
)
|
|
315
312
|
runtime = AgentRuntime(character=character)
|
|
316
313
|
assert runtime.get_llm_mode() == LLMMode.SMALL
|
|
317
314
|
|
|
318
|
-
@pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
|
|
319
315
|
def test_constructor_option_takes_precedence(self) -> None:
|
|
320
316
|
character = Character(
|
|
321
317
|
name="Test",
|
|
322
318
|
bio=["Test"],
|
|
323
|
-
settings={"LLM_MODE": "SMALL"},
|
|
319
|
+
settings={"extra": {"LLM_MODE": "SMALL"}},
|
|
324
320
|
)
|
|
325
321
|
runtime = AgentRuntime(character=character, llm_mode=LLMMode.LARGE)
|
|
326
322
|
assert runtime.get_llm_mode() == LLMMode.LARGE
|
|
327
323
|
|
|
328
|
-
@pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
|
|
329
324
|
def test_case_insensitive_character_setting(self) -> None:
|
|
330
325
|
character = Character(
|
|
331
326
|
name="Test",
|
|
332
327
|
bio=["Test"],
|
|
333
|
-
settings={"LLM_MODE": "small"},
|
|
328
|
+
settings={"extra": {"LLM_MODE": "small"}},
|
|
334
329
|
)
|
|
335
330
|
runtime = AgentRuntime(character=character)
|
|
336
331
|
assert runtime.get_llm_mode() == LLMMode.SMALL
|
|
337
332
|
|
|
338
|
-
@pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
|
|
339
333
|
def test_invalid_setting_defaults_to_default(self) -> None:
|
|
340
334
|
character = Character(
|
|
341
335
|
name="Test",
|
|
342
336
|
bio=["Test"],
|
|
343
|
-
settings={"LLM_MODE": "invalid"},
|
|
337
|
+
settings={"extra": {"LLM_MODE": "invalid"}},
|
|
344
338
|
)
|
|
345
339
|
runtime = AgentRuntime(character=character)
|
|
346
340
|
assert runtime.get_llm_mode() == LLMMode.DEFAULT
|
|
@@ -391,32 +385,29 @@ class TestAgentRuntimeCheckShouldRespond:
|
|
|
391
385
|
runtime = AgentRuntime(character=character, check_should_respond=True)
|
|
392
386
|
assert runtime.is_check_should_respond_enabled() is True
|
|
393
387
|
|
|
394
|
-
@pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
|
|
395
388
|
def test_character_setting_false(self) -> None:
|
|
396
389
|
character = Character(
|
|
397
390
|
name="Test",
|
|
398
391
|
bio=["Test"],
|
|
399
|
-
settings={"CHECK_SHOULD_RESPOND": "false"},
|
|
392
|
+
settings={"extra": {"CHECK_SHOULD_RESPOND": "false"}},
|
|
400
393
|
)
|
|
401
394
|
runtime = AgentRuntime(character=character)
|
|
402
395
|
assert runtime.is_check_should_respond_enabled() is False
|
|
403
396
|
|
|
404
|
-
@pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
|
|
405
397
|
def test_constructor_option_takes_precedence(self) -> None:
|
|
406
398
|
character = Character(
|
|
407
399
|
name="Test",
|
|
408
400
|
bio=["Test"],
|
|
409
|
-
settings={"CHECK_SHOULD_RESPOND": "false"},
|
|
401
|
+
settings={"extra": {"CHECK_SHOULD_RESPOND": "false"}},
|
|
410
402
|
)
|
|
411
403
|
runtime = AgentRuntime(character=character, check_should_respond=True)
|
|
412
404
|
assert runtime.is_check_should_respond_enabled() is True
|
|
413
405
|
|
|
414
|
-
@pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
|
|
415
406
|
def test_non_false_string_defaults_to_true(self) -> None:
|
|
416
407
|
character = Character(
|
|
417
408
|
name="Test",
|
|
418
409
|
bio=["Test"],
|
|
419
|
-
settings={"CHECK_SHOULD_RESPOND": "yes"},
|
|
410
|
+
settings={"extra": {"CHECK_SHOULD_RESPOND": "yes"}},
|
|
420
411
|
)
|
|
421
412
|
runtime = AgentRuntime(character=character)
|
|
422
413
|
assert runtime.is_check_should_respond_enabled() is True
|