@elizaos/python 2.0.0-alpha.10
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/LICENSE +26 -0
- package/README.md +239 -0
- package/elizaos/__init__.py +280 -0
- package/elizaos/action_docs.py +149 -0
- package/elizaos/advanced_capabilities/__init__.py +85 -0
- package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
- package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
- package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
- package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
- package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
- package/elizaos/advanced_capabilities/actions/roles.py +207 -0
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
- package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
- package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
- package/elizaos/advanced_capabilities/actions/settings.py +151 -0
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
- package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
- package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
- package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
- package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
- package/elizaos/advanced_capabilities/providers/facts.py +82 -0
- package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
- package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
- package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
- package/elizaos/advanced_capabilities/providers/roles.py +97 -0
- package/elizaos/advanced_capabilities/providers/settings.py +51 -0
- package/elizaos/advanced_capabilities/services/__init__.py +18 -0
- package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
- package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
- package/elizaos/advanced_memory/__init__.py +3 -0
- package/elizaos/advanced_memory/evaluators.py +97 -0
- package/elizaos/advanced_memory/memory_service.py +556 -0
- package/elizaos/advanced_memory/plugin.py +30 -0
- package/elizaos/advanced_memory/prompts.py +12 -0
- package/elizaos/advanced_memory/providers.py +90 -0
- package/elizaos/advanced_memory/types.py +65 -0
- package/elizaos/advanced_planning/__init__.py +10 -0
- package/elizaos/advanced_planning/actions.py +145 -0
- package/elizaos/advanced_planning/message_classifier.py +127 -0
- package/elizaos/advanced_planning/planning_service.py +712 -0
- package/elizaos/advanced_planning/plugin.py +40 -0
- package/elizaos/advanced_planning/prompts.py +4 -0
- package/elizaos/basic_capabilities/__init__.py +66 -0
- package/elizaos/basic_capabilities/actions/__init__.py +24 -0
- package/elizaos/basic_capabilities/actions/choice.py +140 -0
- package/elizaos/basic_capabilities/actions/ignore.py +66 -0
- package/elizaos/basic_capabilities/actions/none.py +56 -0
- package/elizaos/basic_capabilities/actions/reply.py +120 -0
- package/elizaos/basic_capabilities/providers/__init__.py +54 -0
- package/elizaos/basic_capabilities/providers/action_state.py +113 -0
- package/elizaos/basic_capabilities/providers/actions.py +263 -0
- package/elizaos/basic_capabilities/providers/attachments.py +76 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
- package/elizaos/basic_capabilities/providers/character.py +113 -0
- package/elizaos/basic_capabilities/providers/choice.py +73 -0
- package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
- package/elizaos/basic_capabilities/providers/current_time.py +58 -0
- package/elizaos/basic_capabilities/providers/entities.py +99 -0
- package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
- package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
- package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
- package/elizaos/basic_capabilities/providers/time.py +45 -0
- package/elizaos/basic_capabilities/providers/world.py +93 -0
- package/elizaos/basic_capabilities/services/__init__.py +18 -0
- package/elizaos/basic_capabilities/services/embedding.py +122 -0
- package/elizaos/basic_capabilities/services/task.py +178 -0
- package/elizaos/bootstrap/__init__.py +12 -0
- package/elizaos/bootstrap/actions/__init__.py +68 -0
- package/elizaos/bootstrap/actions/add_contact.py +149 -0
- package/elizaos/bootstrap/actions/choice.py +147 -0
- package/elizaos/bootstrap/actions/follow_room.py +151 -0
- package/elizaos/bootstrap/actions/ignore.py +80 -0
- package/elizaos/bootstrap/actions/image_generation.py +135 -0
- package/elizaos/bootstrap/actions/mute_room.py +151 -0
- package/elizaos/bootstrap/actions/none.py +71 -0
- package/elizaos/bootstrap/actions/remove_contact.py +159 -0
- package/elizaos/bootstrap/actions/reply.py +140 -0
- package/elizaos/bootstrap/actions/roles.py +193 -0
- package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
- package/elizaos/bootstrap/actions/search_contacts.py +159 -0
- package/elizaos/bootstrap/actions/send_message.py +173 -0
- package/elizaos/bootstrap/actions/settings.py +165 -0
- package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
- package/elizaos/bootstrap/actions/unmute_room.py +151 -0
- package/elizaos/bootstrap/actions/update_contact.py +178 -0
- package/elizaos/bootstrap/actions/update_entity.py +175 -0
- package/elizaos/bootstrap/autonomy/__init__.py +18 -0
- package/elizaos/bootstrap/autonomy/action.py +197 -0
- package/elizaos/bootstrap/autonomy/providers.py +165 -0
- package/elizaos/bootstrap/autonomy/routes.py +171 -0
- package/elizaos/bootstrap/autonomy/service.py +562 -0
- package/elizaos/bootstrap/autonomy/types.py +18 -0
- package/elizaos/bootstrap/evaluators/__init__.py +19 -0
- package/elizaos/bootstrap/evaluators/reflection.py +118 -0
- package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
- package/elizaos/bootstrap/plugin.py +140 -0
- package/elizaos/bootstrap/providers/__init__.py +80 -0
- package/elizaos/bootstrap/providers/action_state.py +71 -0
- package/elizaos/bootstrap/providers/actions.py +256 -0
- package/elizaos/bootstrap/providers/agent_settings.py +63 -0
- package/elizaos/bootstrap/providers/attachments.py +76 -0
- package/elizaos/bootstrap/providers/capabilities.py +66 -0
- package/elizaos/bootstrap/providers/character.py +128 -0
- package/elizaos/bootstrap/providers/choice.py +77 -0
- package/elizaos/bootstrap/providers/contacts.py +78 -0
- package/elizaos/bootstrap/providers/context_bench.py +49 -0
- package/elizaos/bootstrap/providers/current_time.py +56 -0
- package/elizaos/bootstrap/providers/entities.py +99 -0
- package/elizaos/bootstrap/providers/evaluators.py +58 -0
- package/elizaos/bootstrap/providers/facts.py +86 -0
- package/elizaos/bootstrap/providers/follow_ups.py +116 -0
- package/elizaos/bootstrap/providers/knowledge.py +73 -0
- package/elizaos/bootstrap/providers/providers_list.py +59 -0
- package/elizaos/bootstrap/providers/recent_messages.py +85 -0
- package/elizaos/bootstrap/providers/relationships.py +106 -0
- package/elizaos/bootstrap/providers/roles.py +95 -0
- package/elizaos/bootstrap/providers/settings.py +55 -0
- package/elizaos/bootstrap/providers/time.py +45 -0
- package/elizaos/bootstrap/providers/world.py +97 -0
- package/elizaos/bootstrap/services/__init__.py +26 -0
- package/elizaos/bootstrap/services/embedding.py +122 -0
- package/elizaos/bootstrap/services/follow_up.py +138 -0
- package/elizaos/bootstrap/services/rolodex.py +244 -0
- package/elizaos/bootstrap/services/task.py +585 -0
- package/elizaos/bootstrap/types.py +54 -0
- package/elizaos/bootstrap/utils/__init__.py +7 -0
- package/elizaos/bootstrap/utils/xml.py +69 -0
- package/elizaos/character.py +149 -0
- package/elizaos/logger.py +179 -0
- package/elizaos/media/__init__.py +45 -0
- package/elizaos/media/mime.py +315 -0
- package/elizaos/media/search.py +161 -0
- package/elizaos/media/tests/__init__.py +1 -0
- package/elizaos/media/tests/test_mime.py +117 -0
- package/elizaos/media/tests/test_search.py +156 -0
- package/elizaos/plugin.py +191 -0
- package/elizaos/prompts.py +1071 -0
- package/elizaos/py.typed +0 -0
- package/elizaos/runtime.py +2572 -0
- package/elizaos/services/__init__.py +49 -0
- package/elizaos/services/hook_service.py +511 -0
- package/elizaos/services/message_service.py +1248 -0
- package/elizaos/settings.py +182 -0
- package/elizaos/streaming_context.py +159 -0
- package/elizaos/trajectory_context.py +18 -0
- package/elizaos/types/__init__.py +512 -0
- package/elizaos/types/agent.py +31 -0
- package/elizaos/types/components.py +208 -0
- package/elizaos/types/database.py +64 -0
- package/elizaos/types/environment.py +46 -0
- package/elizaos/types/events.py +47 -0
- package/elizaos/types/memory.py +45 -0
- package/elizaos/types/model.py +393 -0
- package/elizaos/types/plugin.py +188 -0
- package/elizaos/types/primitives.py +100 -0
- package/elizaos/types/runtime.py +460 -0
- package/elizaos/types/service.py +113 -0
- package/elizaos/types/service_interfaces.py +244 -0
- package/elizaos/types/state.py +188 -0
- package/elizaos/types/task.py +29 -0
- package/elizaos/utils/__init__.py +108 -0
- package/elizaos/utils/spec_examples.py +48 -0
- package/elizaos/utils/streaming.py +426 -0
- package/elizaos_atropos_shared/__init__.py +1 -0
- package/elizaos_atropos_shared/canonical_eliza.py +282 -0
- package/package.json +19 -0
- package/pyproject.toml +143 -0
- package/requirements-dev.in +11 -0
- package/requirements-dev.lock +134 -0
- package/requirements.in +9 -0
- package/requirements.lock +64 -0
- package/tests/__init__.py +0 -0
- package/tests/test_action_parameters.py +154 -0
- package/tests/test_actions_provider_examples.py +39 -0
- package/tests/test_advanced_memory_behavior.py +96 -0
- package/tests/test_advanced_memory_flag.py +30 -0
- package/tests/test_advanced_planning_behavior.py +225 -0
- package/tests/test_advanced_planning_flag.py +26 -0
- package/tests/test_autonomy.py +445 -0
- package/tests/test_bootstrap_initialize.py +37 -0
- package/tests/test_character.py +163 -0
- package/tests/test_character_provider.py +231 -0
- package/tests/test_dynamic_prompt_exec.py +561 -0
- package/tests/test_logger_redaction.py +43 -0
- package/tests/test_plugin.py +117 -0
- package/tests/test_runtime.py +422 -0
- package/tests/test_salt_production_enforcement.py +22 -0
- package/tests/test_settings_crypto.py +118 -0
- package/tests/test_streaming.py +295 -0
- package/tests/test_types.py +221 -0
- package/tests/test_uuid_parity.py +46 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from elizaos.bootstrap.autonomy import (
|
|
8
|
+
AUTONOMY_SERVICE_TYPE,
|
|
9
|
+
AutonomyService,
|
|
10
|
+
admin_chat_provider,
|
|
11
|
+
autonomy_status_provider,
|
|
12
|
+
send_to_admin_action,
|
|
13
|
+
)
|
|
14
|
+
from elizaos.bootstrap.autonomy.types import AutonomyStatus
|
|
15
|
+
from elizaos.types.memory import Memory
|
|
16
|
+
from elizaos.types.primitives import Content, as_uuid
|
|
17
|
+
|
|
18
|
+
TEST_AGENT_ID = "00000000-0000-0000-0000-000000000001"
|
|
19
|
+
TEST_ROOM_ID = "00000000-0000-0000-0000-000000000002"
|
|
20
|
+
TEST_ENTITY_ID = "00000000-0000-0000-0000-000000000003"
|
|
21
|
+
TEST_MESSAGE_ID = "00000000-0000-0000-0000-000000000004"
|
|
22
|
+
OTHER_ROOM_ID = "00000000-0000-0000-0000-000000000005"
|
|
23
|
+
AUTONOMOUS_ROOM_ID = "00000000-0000-0000-0000-000000000006"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def test_runtime():
|
|
28
|
+
runtime = MagicMock()
|
|
29
|
+
runtime.agent_id = as_uuid(TEST_AGENT_ID)
|
|
30
|
+
runtime.character = MagicMock()
|
|
31
|
+
runtime.character.name = "Test Agent"
|
|
32
|
+
|
|
33
|
+
runtime.ensure_world_exists = AsyncMock()
|
|
34
|
+
runtime.ensure_room_exists = AsyncMock()
|
|
35
|
+
runtime.add_participant = AsyncMock()
|
|
36
|
+
runtime.get_entity_by_id = AsyncMock(return_value=MagicMock(id=TEST_AGENT_ID))
|
|
37
|
+
runtime.get_memories = AsyncMock(return_value=[])
|
|
38
|
+
runtime.emit_event = AsyncMock()
|
|
39
|
+
runtime.create_memory = AsyncMock(return_value="memory-id")
|
|
40
|
+
|
|
41
|
+
runtime.get_setting = MagicMock(return_value=None)
|
|
42
|
+
runtime.set_setting = MagicMock()
|
|
43
|
+
runtime.enable_autonomy = False
|
|
44
|
+
|
|
45
|
+
runtime.logger = MagicMock()
|
|
46
|
+
runtime.logger.info = MagicMock()
|
|
47
|
+
runtime.logger.debug = MagicMock()
|
|
48
|
+
runtime.logger.error = MagicMock()
|
|
49
|
+
runtime.logger.warn = MagicMock()
|
|
50
|
+
runtime.logger.warning = MagicMock()
|
|
51
|
+
|
|
52
|
+
# Task system mocks (used by _create_autonomy_task / _remove_autonomy_task)
|
|
53
|
+
runtime.create_task = AsyncMock()
|
|
54
|
+
runtime.get_tasks = AsyncMock(return_value=[])
|
|
55
|
+
runtime.delete_task = AsyncMock()
|
|
56
|
+
runtime.register_task_worker = MagicMock()
|
|
57
|
+
|
|
58
|
+
return runtime
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.fixture
|
|
62
|
+
def test_memory():
|
|
63
|
+
return Memory(
|
|
64
|
+
id=as_uuid(TEST_MESSAGE_ID),
|
|
65
|
+
room_id=as_uuid(TEST_ROOM_ID),
|
|
66
|
+
entity_id=as_uuid(TEST_ENTITY_ID),
|
|
67
|
+
agent_id=as_uuid(TEST_AGENT_ID),
|
|
68
|
+
content=Content(text="Test message"),
|
|
69
|
+
created_at=1234567890,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestAutonomyService:
|
|
74
|
+
def test_service_type(self):
|
|
75
|
+
assert AutonomyService.service_type == AUTONOMY_SERVICE_TYPE
|
|
76
|
+
assert AutonomyService.service_type == "AUTONOMY"
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
async def test_start_creates_service(self, test_runtime):
|
|
80
|
+
service = await AutonomyService.start(test_runtime)
|
|
81
|
+
|
|
82
|
+
assert service is not None
|
|
83
|
+
assert isinstance(service, AutonomyService)
|
|
84
|
+
assert service.is_loop_running() is False
|
|
85
|
+
assert service.get_loop_interval() == 30000
|
|
86
|
+
assert service.get_autonomous_room_id() is not None
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_auto_start_when_enabled(self, test_runtime):
|
|
90
|
+
test_runtime.enable_autonomy = True
|
|
91
|
+
|
|
92
|
+
service = await AutonomyService.start(test_runtime)
|
|
93
|
+
|
|
94
|
+
assert service.is_loop_running() is True
|
|
95
|
+
|
|
96
|
+
await service.disable_autonomy()
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_ensure_context_on_initialization(self, test_runtime):
|
|
100
|
+
_ = await AutonomyService.start(test_runtime)
|
|
101
|
+
|
|
102
|
+
test_runtime.ensure_world_exists.assert_called_once()
|
|
103
|
+
test_runtime.ensure_room_exists.assert_called_once()
|
|
104
|
+
test_runtime.add_participant.assert_called_once()
|
|
105
|
+
|
|
106
|
+
world_call = test_runtime.ensure_world_exists.call_args[0][0]
|
|
107
|
+
assert world_call.name == "Autonomy World"
|
|
108
|
+
|
|
109
|
+
room_call = test_runtime.ensure_room_exists.call_args[0][0]
|
|
110
|
+
assert room_call.name == "Autonomous Thoughts"
|
|
111
|
+
assert room_call.source == "autonomy-service"
|
|
112
|
+
|
|
113
|
+
@pytest.mark.asyncio
|
|
114
|
+
async def test_start_stop_loop(self, test_runtime):
|
|
115
|
+
service = await AutonomyService.start(test_runtime)
|
|
116
|
+
|
|
117
|
+
assert service.is_loop_running() is False
|
|
118
|
+
|
|
119
|
+
await service.enable_autonomy()
|
|
120
|
+
assert service.is_loop_running() is True
|
|
121
|
+
assert test_runtime.enable_autonomy is True
|
|
122
|
+
|
|
123
|
+
await service.disable_autonomy()
|
|
124
|
+
assert service.is_loop_running() is False
|
|
125
|
+
assert test_runtime.enable_autonomy is False
|
|
126
|
+
|
|
127
|
+
@pytest.mark.asyncio
|
|
128
|
+
async def test_no_double_start(self, test_runtime):
|
|
129
|
+
service = await AutonomyService.start(test_runtime)
|
|
130
|
+
|
|
131
|
+
await service.enable_autonomy()
|
|
132
|
+
call_count = test_runtime.set_setting.call_count
|
|
133
|
+
|
|
134
|
+
await service.enable_autonomy()
|
|
135
|
+
assert test_runtime.set_setting.call_count == call_count
|
|
136
|
+
|
|
137
|
+
await service.disable_autonomy()
|
|
138
|
+
|
|
139
|
+
@pytest.mark.asyncio
|
|
140
|
+
async def test_no_double_stop(self, test_runtime):
|
|
141
|
+
service = await AutonomyService.start(test_runtime)
|
|
142
|
+
|
|
143
|
+
call_count = test_runtime.set_setting.call_count
|
|
144
|
+
await service.disable_autonomy()
|
|
145
|
+
assert test_runtime.set_setting.call_count == call_count
|
|
146
|
+
|
|
147
|
+
@pytest.mark.asyncio
|
|
148
|
+
async def test_interval_configuration(self, test_runtime):
|
|
149
|
+
service = await AutonomyService.start(test_runtime)
|
|
150
|
+
|
|
151
|
+
await service.set_loop_interval(60000)
|
|
152
|
+
assert service.get_loop_interval() == 60000
|
|
153
|
+
|
|
154
|
+
@pytest.mark.asyncio
|
|
155
|
+
async def test_interval_minimum_enforced(self, test_runtime):
|
|
156
|
+
service = await AutonomyService.start(test_runtime)
|
|
157
|
+
|
|
158
|
+
await service.set_loop_interval(1000)
|
|
159
|
+
assert service.get_loop_interval() == 5000
|
|
160
|
+
|
|
161
|
+
@pytest.mark.asyncio
|
|
162
|
+
async def test_interval_maximum_enforced(self, test_runtime):
|
|
163
|
+
service = await AutonomyService.start(test_runtime)
|
|
164
|
+
|
|
165
|
+
await service.set_loop_interval(1000000)
|
|
166
|
+
assert service.get_loop_interval() == 600000
|
|
167
|
+
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
async def test_target_room_context_dedupes_by_earliest_created_at(self, test_runtime):
|
|
170
|
+
service = await AutonomyService.start(test_runtime)
|
|
171
|
+
target_room_id = as_uuid(OTHER_ROOM_ID)
|
|
172
|
+
test_runtime.get_setting = MagicMock(return_value=str(target_room_id))
|
|
173
|
+
|
|
174
|
+
dup_id = as_uuid(TEST_MESSAGE_ID)
|
|
175
|
+
older = Memory(
|
|
176
|
+
id=dup_id,
|
|
177
|
+
room_id=target_room_id,
|
|
178
|
+
entity_id=as_uuid(TEST_ENTITY_ID),
|
|
179
|
+
agent_id=as_uuid(TEST_AGENT_ID),
|
|
180
|
+
content=Content(text="old"),
|
|
181
|
+
created_at=10,
|
|
182
|
+
)
|
|
183
|
+
newer = Memory(
|
|
184
|
+
id=dup_id,
|
|
185
|
+
room_id=target_room_id,
|
|
186
|
+
entity_id=as_uuid(TEST_ENTITY_ID),
|
|
187
|
+
agent_id=as_uuid(TEST_AGENT_ID),
|
|
188
|
+
content=Content(text="new"),
|
|
189
|
+
created_at=20,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def get_memories(params):
|
|
193
|
+
if params["tableName"] == "memories":
|
|
194
|
+
return [newer]
|
|
195
|
+
return [older]
|
|
196
|
+
|
|
197
|
+
test_runtime.get_memories = AsyncMock(side_effect=get_memories)
|
|
198
|
+
|
|
199
|
+
context = await service._get_target_room_context_text()
|
|
200
|
+
assert "old" in context
|
|
201
|
+
assert "new" not in context
|
|
202
|
+
|
|
203
|
+
@pytest.mark.asyncio
|
|
204
|
+
async def test_enable_autonomy(self, test_runtime):
|
|
205
|
+
service = await AutonomyService.start(test_runtime)
|
|
206
|
+
|
|
207
|
+
await service.enable_autonomy()
|
|
208
|
+
|
|
209
|
+
assert test_runtime.enable_autonomy is True
|
|
210
|
+
assert service.is_loop_running() is True
|
|
211
|
+
|
|
212
|
+
await service.disable_autonomy()
|
|
213
|
+
|
|
214
|
+
@pytest.mark.asyncio
|
|
215
|
+
async def test_disable_autonomy(self, test_runtime):
|
|
216
|
+
service = await AutonomyService.start(test_runtime)
|
|
217
|
+
|
|
218
|
+
await service.enable_autonomy()
|
|
219
|
+
await service.disable_autonomy()
|
|
220
|
+
|
|
221
|
+
assert test_runtime.enable_autonomy is False
|
|
222
|
+
assert service.is_loop_running() is False
|
|
223
|
+
|
|
224
|
+
@pytest.mark.asyncio
|
|
225
|
+
async def test_get_status(self, test_runtime):
|
|
226
|
+
test_runtime.enable_autonomy = True
|
|
227
|
+
service = await AutonomyService.start(test_runtime)
|
|
228
|
+
|
|
229
|
+
status = service.get_status()
|
|
230
|
+
|
|
231
|
+
assert isinstance(status, AutonomyStatus)
|
|
232
|
+
assert status.enabled is True
|
|
233
|
+
assert status.running is True
|
|
234
|
+
assert status.thinking is False
|
|
235
|
+
assert status.interval == 30000
|
|
236
|
+
assert status.autonomous_room_id is not None
|
|
237
|
+
|
|
238
|
+
@pytest.mark.asyncio
|
|
239
|
+
async def test_last_autonomous_thought_uses_latest_created_at(self, test_runtime):
|
|
240
|
+
service = await AutonomyService.start(test_runtime)
|
|
241
|
+
test_runtime.enable_autonomy = True
|
|
242
|
+
test_runtime.get_setting = MagicMock(return_value=None)
|
|
243
|
+
|
|
244
|
+
older = Memory(
|
|
245
|
+
id=as_uuid("12345678-1234-1234-1234-123456789010"),
|
|
246
|
+
room_id=service.get_autonomous_room_id(),
|
|
247
|
+
entity_id=as_uuid(TEST_AGENT_ID),
|
|
248
|
+
agent_id=as_uuid(TEST_AGENT_ID),
|
|
249
|
+
content=Content(text="older"),
|
|
250
|
+
created_at=10,
|
|
251
|
+
)
|
|
252
|
+
newer = Memory(
|
|
253
|
+
id=as_uuid("12345678-1234-1234-1234-123456789011"),
|
|
254
|
+
room_id=service.get_autonomous_room_id(),
|
|
255
|
+
entity_id=as_uuid(TEST_AGENT_ID),
|
|
256
|
+
agent_id=as_uuid(TEST_AGENT_ID),
|
|
257
|
+
content=Content(text="newer"),
|
|
258
|
+
created_at=20,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
test_runtime.get_memories = AsyncMock(return_value=[older, newer])
|
|
262
|
+
|
|
263
|
+
await service.perform_autonomous_think()
|
|
264
|
+
|
|
265
|
+
assert test_runtime.emit_event.called is True
|
|
266
|
+
payload = test_runtime.emit_event.call_args[0][1]
|
|
267
|
+
msg = payload.get("message")
|
|
268
|
+
assert msg is not None
|
|
269
|
+
assert "newer" in (msg.content.text or "")
|
|
270
|
+
|
|
271
|
+
await service.disable_autonomy()
|
|
272
|
+
|
|
273
|
+
@pytest.mark.asyncio
|
|
274
|
+
async def test_thinking_guard_initial_state(self, test_runtime):
|
|
275
|
+
service = await AutonomyService.start(test_runtime)
|
|
276
|
+
|
|
277
|
+
assert service.is_thinking_in_progress() is False
|
|
278
|
+
assert service.get_status().thinking is False
|
|
279
|
+
|
|
280
|
+
@pytest.mark.asyncio
|
|
281
|
+
async def test_thinking_guard_prevents_overlap(self, test_runtime):
|
|
282
|
+
service = await AutonomyService.start(test_runtime)
|
|
283
|
+
|
|
284
|
+
service._is_thinking = True
|
|
285
|
+
|
|
286
|
+
assert service.is_thinking_in_progress() is True
|
|
287
|
+
assert service.get_status().thinking is True
|
|
288
|
+
|
|
289
|
+
service._is_thinking = False
|
|
290
|
+
assert service.is_thinking_in_progress() is False
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class TestSendToAdminAction:
|
|
294
|
+
def test_action_metadata(self):
|
|
295
|
+
assert send_to_admin_action.name == "SEND_TO_ADMIN"
|
|
296
|
+
assert send_to_admin_action.description is not None
|
|
297
|
+
assert send_to_admin_action.examples is not None
|
|
298
|
+
assert len(send_to_admin_action.examples) > 0
|
|
299
|
+
|
|
300
|
+
@pytest.mark.asyncio
|
|
301
|
+
async def test_validate_in_autonomous_room(self, test_runtime):
|
|
302
|
+
room_id = as_uuid(TEST_ROOM_ID)
|
|
303
|
+
mock_service = MagicMock(spec=AutonomyService)
|
|
304
|
+
mock_service.get_autonomous_room_id = MagicMock(return_value=room_id)
|
|
305
|
+
test_runtime.get_service = MagicMock(return_value=mock_service)
|
|
306
|
+
test_runtime.get_setting = MagicMock(return_value="admin-user-id")
|
|
307
|
+
|
|
308
|
+
message = Memory(
|
|
309
|
+
id=as_uuid(TEST_MESSAGE_ID),
|
|
310
|
+
room_id=room_id,
|
|
311
|
+
entity_id=as_uuid(TEST_ENTITY_ID),
|
|
312
|
+
agent_id=as_uuid(TEST_AGENT_ID),
|
|
313
|
+
content=Content(text="Tell admin about this update"),
|
|
314
|
+
created_at=1234567890,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
is_valid = await send_to_admin_action.validate(test_runtime, message)
|
|
318
|
+
assert is_valid is True
|
|
319
|
+
|
|
320
|
+
@pytest.mark.asyncio
|
|
321
|
+
async def test_validate_not_in_autonomous_room(self, test_runtime, test_memory):
|
|
322
|
+
mock_service = MagicMock(spec=AutonomyService)
|
|
323
|
+
mock_service.get_autonomous_room_id = MagicMock(return_value=as_uuid(OTHER_ROOM_ID))
|
|
324
|
+
test_runtime.get_service = MagicMock(return_value=mock_service)
|
|
325
|
+
test_runtime.get_setting = MagicMock(return_value="admin-user-id")
|
|
326
|
+
|
|
327
|
+
is_valid = await send_to_admin_action.validate(test_runtime, test_memory)
|
|
328
|
+
assert is_valid is False
|
|
329
|
+
|
|
330
|
+
@pytest.mark.asyncio
|
|
331
|
+
async def test_validate_no_admin_configured(self, test_runtime, test_memory):
|
|
332
|
+
mock_service = MagicMock(spec=AutonomyService)
|
|
333
|
+
mock_service.get_autonomous_room_id = MagicMock(return_value=test_memory.room_id)
|
|
334
|
+
test_runtime.get_service = MagicMock(return_value=mock_service)
|
|
335
|
+
test_runtime.get_setting = MagicMock(return_value=None)
|
|
336
|
+
|
|
337
|
+
is_valid = await send_to_admin_action.validate(test_runtime, test_memory)
|
|
338
|
+
assert is_valid is False
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class TestAdminChatProvider:
|
|
342
|
+
def test_provider_metadata(self):
|
|
343
|
+
assert admin_chat_provider.name == "ADMIN_CHAT_HISTORY"
|
|
344
|
+
assert admin_chat_provider.description is not None
|
|
345
|
+
|
|
346
|
+
@pytest.mark.asyncio
|
|
347
|
+
async def test_returns_empty_when_no_service(self, test_runtime, test_memory):
|
|
348
|
+
test_runtime.get_service = MagicMock(return_value=None)
|
|
349
|
+
|
|
350
|
+
result = await admin_chat_provider.get(test_runtime, test_memory, {})
|
|
351
|
+
|
|
352
|
+
assert result.text == ""
|
|
353
|
+
|
|
354
|
+
@pytest.mark.asyncio
|
|
355
|
+
async def test_returns_empty_when_not_in_autonomous_room(self, test_runtime, test_memory):
|
|
356
|
+
mock_service = MagicMock(spec=AutonomyService)
|
|
357
|
+
mock_service.get_autonomous_room_id = MagicMock(return_value=as_uuid(OTHER_ROOM_ID))
|
|
358
|
+
test_runtime.get_service = MagicMock(return_value=mock_service)
|
|
359
|
+
|
|
360
|
+
result = await admin_chat_provider.get(test_runtime, test_memory, {})
|
|
361
|
+
|
|
362
|
+
assert result.text == ""
|
|
363
|
+
|
|
364
|
+
@pytest.mark.asyncio
|
|
365
|
+
async def test_indicates_no_admin_configured(self, test_runtime, test_memory):
|
|
366
|
+
mock_service = MagicMock(spec=AutonomyService)
|
|
367
|
+
mock_service.get_autonomous_room_id = MagicMock(return_value=test_memory.room_id)
|
|
368
|
+
test_runtime.get_service = MagicMock(return_value=mock_service)
|
|
369
|
+
test_runtime.get_setting = MagicMock(return_value=None)
|
|
370
|
+
|
|
371
|
+
result = await admin_chat_provider.get(test_runtime, test_memory, {})
|
|
372
|
+
|
|
373
|
+
assert "No admin user configured" in result.text
|
|
374
|
+
assert result.data == {"adminConfigured": False}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class TestAutonomyStatusProvider:
|
|
378
|
+
def test_provider_metadata(self):
|
|
379
|
+
assert autonomy_status_provider.name == "AUTONOMY_STATUS"
|
|
380
|
+
assert autonomy_status_provider.description is not None
|
|
381
|
+
|
|
382
|
+
@pytest.mark.asyncio
|
|
383
|
+
async def test_returns_empty_when_no_service(self, test_runtime, test_memory):
|
|
384
|
+
test_runtime.get_service = MagicMock(return_value=None)
|
|
385
|
+
|
|
386
|
+
result = await autonomy_status_provider.get(test_runtime, test_memory, {})
|
|
387
|
+
|
|
388
|
+
assert result.text == ""
|
|
389
|
+
|
|
390
|
+
@pytest.mark.asyncio
|
|
391
|
+
async def test_returns_empty_in_autonomous_room(self, test_runtime, test_memory):
|
|
392
|
+
mock_service = MagicMock(spec=AutonomyService)
|
|
393
|
+
mock_service.get_autonomous_room_id = MagicMock(return_value=test_memory.room_id)
|
|
394
|
+
test_runtime.get_service = MagicMock(return_value=mock_service)
|
|
395
|
+
|
|
396
|
+
result = await autonomy_status_provider.get(test_runtime, test_memory, {})
|
|
397
|
+
|
|
398
|
+
assert result.text == ""
|
|
399
|
+
|
|
400
|
+
@pytest.mark.asyncio
|
|
401
|
+
async def test_shows_running_status(self, test_runtime, test_memory):
|
|
402
|
+
mock_service = MagicMock(spec=AutonomyService)
|
|
403
|
+
mock_service.get_autonomous_room_id = MagicMock(return_value=as_uuid(AUTONOMOUS_ROOM_ID))
|
|
404
|
+
mock_service.is_loop_running = MagicMock(return_value=True)
|
|
405
|
+
mock_service.get_loop_interval = MagicMock(return_value=30000)
|
|
406
|
+
test_runtime.get_service = MagicMock(return_value=mock_service)
|
|
407
|
+
test_runtime.get_setting = MagicMock(return_value=True)
|
|
408
|
+
|
|
409
|
+
result = await autonomy_status_provider.get(test_runtime, test_memory, {})
|
|
410
|
+
|
|
411
|
+
assert "AUTONOMY_STATUS" in result.text
|
|
412
|
+
assert "running autonomously" in result.text
|
|
413
|
+
assert result.data["serviceRunning"] is True
|
|
414
|
+
assert result.data["status"] == "running"
|
|
415
|
+
|
|
416
|
+
@pytest.mark.asyncio
|
|
417
|
+
async def test_shows_disabled_status(self, test_runtime, test_memory):
|
|
418
|
+
mock_service = MagicMock(spec=AutonomyService)
|
|
419
|
+
mock_service.get_autonomous_room_id = MagicMock(return_value=as_uuid(AUTONOMOUS_ROOM_ID))
|
|
420
|
+
mock_service.is_loop_running = MagicMock(return_value=False)
|
|
421
|
+
mock_service.get_loop_interval = MagicMock(return_value=30000)
|
|
422
|
+
test_runtime.get_service = MagicMock(return_value=mock_service)
|
|
423
|
+
test_runtime.get_setting = MagicMock(return_value=False)
|
|
424
|
+
|
|
425
|
+
result = await autonomy_status_provider.get(test_runtime, test_memory, {})
|
|
426
|
+
|
|
427
|
+
assert "autonomy disabled" in result.text
|
|
428
|
+
assert result.data["status"] == "disabled"
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class TestAutonomyIntegration:
|
|
432
|
+
def test_exports_all_components(self):
|
|
433
|
+
from elizaos.bootstrap.autonomy import (
|
|
434
|
+
AUTONOMY_SERVICE_TYPE,
|
|
435
|
+
AutonomyService,
|
|
436
|
+
admin_chat_provider,
|
|
437
|
+
autonomy_status_provider,
|
|
438
|
+
send_to_admin_action,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
assert AutonomyService is not None
|
|
442
|
+
assert AUTONOMY_SERVICE_TYPE == "AUTONOMY"
|
|
443
|
+
assert send_to_admin_action is not None
|
|
444
|
+
assert admin_chat_provider is not None
|
|
445
|
+
assert autonomy_status_provider is not None
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from elizaos.runtime import AgentRuntime
|
|
4
|
+
from elizaos.types import Character
|
|
5
|
+
from elizaos.types.service import ServiceType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@pytest.mark.asyncio
|
|
9
|
+
async def test_runtime_initialize_registers_bootstrap_services() -> None:
|
|
10
|
+
"""
|
|
11
|
+
Ensure AgentRuntime.initialize() loads the bootstrap plugin and registers services.
|
|
12
|
+
|
|
13
|
+
This guards against regressions where bootstrap service registration silently
|
|
14
|
+
stores None or fails to start services properly.
|
|
15
|
+
"""
|
|
16
|
+
runtime = AgentRuntime(
|
|
17
|
+
character=Character(name="BootstrapTest", bio="bootstrap init test"),
|
|
18
|
+
log_level="ERROR",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
await runtime.initialize()
|
|
22
|
+
|
|
23
|
+
assert any(p.name == "bootstrap" for p in runtime.plugins)
|
|
24
|
+
|
|
25
|
+
# Bootstrap should register at least one service of type TASK.
|
|
26
|
+
assert runtime.has_service(ServiceType.TASK)
|
|
27
|
+
|
|
28
|
+
task_service = runtime.get_service(ServiceType.TASK)
|
|
29
|
+
assert task_service is not None
|
|
30
|
+
|
|
31
|
+
task_services = runtime.get_services_by_type(ServiceType.TASK)
|
|
32
|
+
assert task_services
|
|
33
|
+
assert all(s is not None for s in task_services)
|
|
34
|
+
|
|
35
|
+
# Bootstrap should also register some core actions/providers/evaluators.
|
|
36
|
+
assert runtime.actions
|
|
37
|
+
assert runtime.providers
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from elizaos.character import (
|
|
8
|
+
CharacterLoadError,
|
|
9
|
+
CharacterValidationError,
|
|
10
|
+
build_character_plugins,
|
|
11
|
+
load_character_from_file,
|
|
12
|
+
merge_character_defaults,
|
|
13
|
+
parse_character,
|
|
14
|
+
validate_character_config,
|
|
15
|
+
)
|
|
16
|
+
from elizaos.types import Character
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestParseCharacter:
|
|
20
|
+
def test_parse_character_object(self) -> None:
|
|
21
|
+
character = Character(name="Test", bio=["A test agent"])
|
|
22
|
+
result = parse_character(character)
|
|
23
|
+
assert result.name == "Test"
|
|
24
|
+
assert list(result.bio) == ["A test agent"]
|
|
25
|
+
|
|
26
|
+
def test_parse_character_dict(self) -> None:
|
|
27
|
+
data = {"name": "Test", "bio": ["A test agent"]}
|
|
28
|
+
result = parse_character(data)
|
|
29
|
+
assert result.name == "Test"
|
|
30
|
+
assert list(result.bio) == ["A test agent"]
|
|
31
|
+
|
|
32
|
+
def test_parse_character_invalid_dict(self) -> None:
|
|
33
|
+
data = {"bio": "Missing name"}
|
|
34
|
+
with pytest.raises(CharacterValidationError):
|
|
35
|
+
parse_character(data)
|
|
36
|
+
|
|
37
|
+
def test_parse_character_file_path(self) -> None:
|
|
38
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
|
39
|
+
json.dump({"name": "FileAgent", "bio": ["From file"]}, f)
|
|
40
|
+
f.flush()
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
result = parse_character(f.name)
|
|
44
|
+
assert result.name == "FileAgent"
|
|
45
|
+
finally:
|
|
46
|
+
os.unlink(f.name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestLoadCharacterFromFile:
|
|
50
|
+
def test_load_valid_file(self) -> None:
|
|
51
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
|
52
|
+
json.dump(
|
|
53
|
+
{
|
|
54
|
+
"name": "FileAgent",
|
|
55
|
+
"bio": ["A file-based agent"],
|
|
56
|
+
"topics": ["testing"],
|
|
57
|
+
},
|
|
58
|
+
f,
|
|
59
|
+
)
|
|
60
|
+
f.flush()
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
result = load_character_from_file(f.name)
|
|
64
|
+
assert result.name == "FileAgent"
|
|
65
|
+
assert list(result.topics) == ["testing"]
|
|
66
|
+
finally:
|
|
67
|
+
os.unlink(f.name)
|
|
68
|
+
|
|
69
|
+
def test_load_nonexistent_file(self) -> None:
|
|
70
|
+
with pytest.raises(CharacterLoadError, match="not found"):
|
|
71
|
+
load_character_from_file("/nonexistent/path/character.json")
|
|
72
|
+
|
|
73
|
+
def test_load_invalid_json(self) -> None:
|
|
74
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
|
|
75
|
+
f.write("not valid json {")
|
|
76
|
+
f.flush()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
with pytest.raises(CharacterLoadError, match="Invalid JSON"):
|
|
80
|
+
load_character_from_file(f.name)
|
|
81
|
+
finally:
|
|
82
|
+
os.unlink(f.name)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestValidateCharacterConfig:
|
|
86
|
+
def test_valid_character(self) -> None:
|
|
87
|
+
character = Character(name="Test", bio=["A test agent"])
|
|
88
|
+
result = validate_character_config(character)
|
|
89
|
+
assert result["isValid"] is True
|
|
90
|
+
assert result["errors"] == []
|
|
91
|
+
|
|
92
|
+
def test_character_with_all_fields(self) -> None:
|
|
93
|
+
character = Character(
|
|
94
|
+
name="CompleteAgent",
|
|
95
|
+
username="complete",
|
|
96
|
+
bio=["Line 1", "Line 2"],
|
|
97
|
+
system="You are a complete agent.",
|
|
98
|
+
topics=["all", "topics"],
|
|
99
|
+
adjectives=["thorough", "complete"],
|
|
100
|
+
plugins=["@elizaos/plugin-sql"],
|
|
101
|
+
)
|
|
102
|
+
result = validate_character_config(character)
|
|
103
|
+
assert result["isValid"] is True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestMergeCharacterDefaults:
|
|
107
|
+
def test_merge_empty(self) -> None:
|
|
108
|
+
result = merge_character_defaults({})
|
|
109
|
+
assert result.name == "Unnamed Character"
|
|
110
|
+
assert list(result.plugins) == []
|
|
111
|
+
|
|
112
|
+
def test_merge_partial(self) -> None:
|
|
113
|
+
result = merge_character_defaults({"name": "CustomAgent", "bio": ["Custom bio"]})
|
|
114
|
+
assert result.name == "CustomAgent"
|
|
115
|
+
assert list(result.bio) == ["Custom bio"]
|
|
116
|
+
|
|
117
|
+
def test_merge_preserves_values(self) -> None:
|
|
118
|
+
result = merge_character_defaults(
|
|
119
|
+
{
|
|
120
|
+
"name": "Agent",
|
|
121
|
+
"bio": ["Bio"],
|
|
122
|
+
"plugins": ["plugin-1"],
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
assert list(result.plugins) == ["plugin-1"]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class TestBuildCharacterPlugins:
|
|
129
|
+
def test_default_plugins(self) -> None:
|
|
130
|
+
plugins = build_character_plugins({})
|
|
131
|
+
assert "@elizaos/plugin-sql" in plugins
|
|
132
|
+
assert "@elizaos/plugin-ollama" in plugins
|
|
133
|
+
|
|
134
|
+
def test_with_openai(self) -> None:
|
|
135
|
+
plugins = build_character_plugins({"OPENAI_API_KEY": "test-key"})
|
|
136
|
+
assert "@elizaos/plugin-openai" in plugins
|
|
137
|
+
assert "@elizaos/plugin-ollama" not in plugins
|
|
138
|
+
|
|
139
|
+
def test_with_anthropic(self) -> None:
|
|
140
|
+
plugins = build_character_plugins({"ANTHROPIC_API_KEY": "test-key"})
|
|
141
|
+
assert "@elizaos/plugin-anthropic" in plugins
|
|
142
|
+
assert "@elizaos/plugin-ollama" not in plugins
|
|
143
|
+
|
|
144
|
+
def test_with_discord(self) -> None:
|
|
145
|
+
plugins = build_character_plugins(
|
|
146
|
+
{
|
|
147
|
+
"DISCORD_API_TOKEN": "test-token",
|
|
148
|
+
"OPENAI_API_KEY": "test-key",
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
assert "@elizaos/plugin-discord" in plugins
|
|
152
|
+
|
|
153
|
+
def test_plugin_order(self) -> None:
|
|
154
|
+
plugins = build_character_plugins(
|
|
155
|
+
{
|
|
156
|
+
"OPENAI_API_KEY": "key1",
|
|
157
|
+
"ANTHROPIC_API_KEY": "key2",
|
|
158
|
+
"DISCORD_API_TOKEN": "token",
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
assert plugins.index("@elizaos/plugin-sql") == 0
|
|
162
|
+
assert plugins.index("@elizaos/plugin-anthropic") < plugins.index("@elizaos/plugin-openai")
|
|
163
|
+
assert plugins.index("@elizaos/plugin-discord") > plugins.index("@elizaos/plugin-openai")
|