@elizaos/python 2.0.0-alpha.10 → 2.0.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/elizaos/__init__.py +0 -1
- package/elizaos/advanced_capabilities/__init__.py +6 -41
- package/elizaos/advanced_capabilities/actions/__init__.py +1 -21
- package/elizaos/advanced_capabilities/actions/add_contact.py +21 -11
- package/elizaos/advanced_capabilities/actions/follow_room.py +28 -28
- package/elizaos/advanced_capabilities/actions/image_generation.py +13 -26
- package/elizaos/advanced_capabilities/actions/mute_room.py +13 -26
- package/elizaos/advanced_capabilities/actions/remove_contact.py +16 -2
- package/elizaos/advanced_capabilities/actions/roles.py +13 -27
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +70 -15
- package/elizaos/advanced_capabilities/actions/search_contacts.py +17 -3
- package/elizaos/advanced_capabilities/actions/send_message.py +183 -50
- package/elizaos/advanced_capabilities/actions/settings.py +16 -2
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +13 -26
- package/elizaos/advanced_capabilities/actions/unmute_room.py +13 -26
- package/elizaos/advanced_capabilities/actions/update_contact.py +16 -2
- package/elizaos/advanced_capabilities/actions/update_entity.py +16 -2
- package/elizaos/advanced_capabilities/evaluators/__init__.py +2 -9
- package/elizaos/advanced_capabilities/evaluators/reflection.py +3 -132
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +5 -201
- package/elizaos/advanced_capabilities/providers/__init__.py +1 -12
- package/elizaos/advanced_capabilities/providers/knowledge.py +24 -3
- package/elizaos/advanced_capabilities/services/__init__.py +2 -9
- package/elizaos/advanced_memory/actions/reset_session.py +11 -0
- package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
- package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_memory/memory_service.py +15 -17
- package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
- package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
- package/elizaos/advanced_planning/planning_service.py +26 -14
- package/elizaos/basic_capabilities/__init__.py +0 -2
- package/elizaos/basic_capabilities/providers/__init__.py +0 -3
- package/elizaos/basic_capabilities/providers/actions.py +118 -29
- package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
- package/elizaos/basic_capabilities/providers/character.py +19 -21
- package/elizaos/basic_capabilities/providers/contacts.py +79 -0
- package/elizaos/basic_capabilities/providers/current_time.py +7 -4
- package/elizaos/basic_capabilities/providers/facts.py +87 -0
- package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
- package/elizaos/basic_capabilities/providers/knowledge.py +97 -0
- package/elizaos/basic_capabilities/providers/relationships.py +107 -0
- package/elizaos/basic_capabilities/providers/roles.py +96 -0
- package/elizaos/basic_capabilities/providers/settings.py +56 -0
- package/elizaos/basic_capabilities/providers/time.py +7 -4
- package/elizaos/bootstrap/__init__.py +21 -2
- package/elizaos/bootstrap/actions/schedule_follow_up.py +65 -7
- package/elizaos/bootstrap/actions/send_message.py +162 -15
- package/elizaos/bootstrap/autonomy/__init__.py +5 -1
- package/elizaos/bootstrap/autonomy/action.py +161 -0
- package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
- package/elizaos/bootstrap/autonomy/service.py +238 -28
- package/elizaos/bootstrap/plugin.py +7 -0
- package/elizaos/bootstrap/providers/actions.py +118 -27
- package/elizaos/bootstrap/providers/agent_settings.py +1 -0
- package/elizaos/bootstrap/providers/attachments.py +1 -0
- package/elizaos/bootstrap/providers/capabilities.py +1 -0
- package/elizaos/bootstrap/providers/character.py +1 -0
- package/elizaos/bootstrap/providers/choice.py +1 -0
- package/elizaos/bootstrap/providers/contacts.py +1 -0
- package/elizaos/bootstrap/providers/current_time.py +8 -2
- package/elizaos/bootstrap/providers/entities.py +1 -0
- package/elizaos/bootstrap/providers/evaluators.py +1 -0
- package/elizaos/bootstrap/providers/facts.py +1 -0
- package/elizaos/bootstrap/providers/follow_ups.py +1 -0
- package/elizaos/bootstrap/providers/knowledge.py +27 -3
- package/elizaos/bootstrap/providers/providers_list.py +1 -0
- package/elizaos/bootstrap/providers/relationships.py +1 -0
- package/elizaos/bootstrap/providers/roles.py +1 -0
- package/elizaos/bootstrap/providers/settings.py +1 -0
- package/elizaos/bootstrap/providers/time.py +8 -4
- package/elizaos/bootstrap/providers/world.py +1 -0
- package/elizaos/bootstrap/services/embedding.py +156 -1
- package/elizaos/deterministic.py +193 -0
- package/elizaos/generated/__init__.py +1 -0
- package/elizaos/generated/action_docs.py +3181 -0
- package/elizaos/generated/spec_helpers.py +175 -0
- package/elizaos/media/mime.py +2 -2
- package/elizaos/media/search.py +23 -23
- package/elizaos/runtime.py +215 -57
- package/elizaos/services/message_service.py +175 -29
- package/elizaos/types/components.py +2 -2
- package/elizaos/types/generated/__init__.py +12 -0
- package/elizaos/types/generated/eliza/v1/agent_pb2.py +63 -0
- package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +159 -0
- package/elizaos/types/generated/eliza/v1/components_pb2.py +65 -0
- package/elizaos/types/generated/eliza/v1/components_pb2.pyi +160 -0
- package/elizaos/types/generated/eliza/v1/database_pb2.py +78 -0
- package/elizaos/types/generated/eliza/v1/database_pb2.pyi +305 -0
- package/elizaos/types/generated/eliza/v1/environment_pb2.py +58 -0
- package/elizaos/types/generated/eliza/v1/environment_pb2.pyi +135 -0
- package/elizaos/types/generated/eliza/v1/events_pb2.py +82 -0
- package/elizaos/types/generated/eliza/v1/events_pb2.pyi +322 -0
- package/elizaos/types/generated/eliza/v1/ipc_pb2.py +113 -0
- package/elizaos/types/generated/eliza/v1/ipc_pb2.pyi +367 -0
- package/elizaos/types/generated/eliza/v1/knowledge_pb2.py +41 -0
- package/elizaos/types/generated/eliza/v1/knowledge_pb2.pyi +26 -0
- package/elizaos/types/generated/eliza/v1/memory_pb2.py +55 -0
- package/elizaos/types/generated/eliza/v1/memory_pb2.pyi +111 -0
- package/elizaos/types/generated/eliza/v1/message_service_pb2.py +48 -0
- package/elizaos/types/generated/eliza/v1/message_service_pb2.pyi +69 -0
- package/elizaos/types/generated/eliza/v1/messaging_pb2.py +51 -0
- package/elizaos/types/generated/eliza/v1/messaging_pb2.pyi +97 -0
- package/elizaos/types/generated/eliza/v1/model_pb2.py +84 -0
- package/elizaos/types/generated/eliza/v1/model_pb2.pyi +280 -0
- package/elizaos/types/generated/eliza/v1/payment_pb2.py +44 -0
- package/elizaos/types/generated/eliza/v1/payment_pb2.pyi +70 -0
- package/elizaos/types/generated/eliza/v1/plugin_pb2.py +68 -0
- package/elizaos/types/generated/eliza/v1/plugin_pb2.pyi +145 -0
- package/elizaos/types/generated/eliza/v1/primitives_pb2.py +48 -0
- package/elizaos/types/generated/eliza/v1/primitives_pb2.pyi +92 -0
- package/elizaos/types/generated/eliza/v1/prompts_pb2.py +52 -0
- package/elizaos/types/generated/eliza/v1/prompts_pb2.pyi +74 -0
- package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.py +211 -0
- package/elizaos/types/generated/eliza/v1/service_interfaces_pb2.pyi +1296 -0
- package/elizaos/types/generated/eliza/v1/service_pb2.py +42 -0
- package/elizaos/types/generated/eliza/v1/service_pb2.pyi +69 -0
- package/elizaos/types/generated/eliza/v1/settings_pb2.py +58 -0
- package/elizaos/types/generated/eliza/v1/settings_pb2.pyi +85 -0
- package/elizaos/types/generated/eliza/v1/state_pb2.py +60 -0
- package/elizaos/types/generated/eliza/v1/state_pb2.pyi +114 -0
- package/elizaos/types/generated/eliza/v1/task_pb2.py +42 -0
- package/elizaos/types/generated/eliza/v1/task_pb2.pyi +58 -0
- package/elizaos/types/generated/eliza/v1/tee_pb2.py +52 -0
- package/elizaos/types/generated/eliza/v1/tee_pb2.pyi +90 -0
- package/elizaos/types/generated/eliza/v1/testing_pb2.py +39 -0
- package/elizaos/types/generated/eliza/v1/testing_pb2.pyi +23 -0
- package/elizaos/types/model.py +30 -0
- package/elizaos/types/runtime.py +6 -2
- package/elizaos/utils/validation.py +76 -0
- package/package.json +3 -2
- package/tests/test_action_parameters.py +2 -3
- package/tests/test_actions_provider_examples.py +58 -1
- package/tests/test_advanced_memory_behavior.py +0 -2
- package/tests/test_advanced_memory_flag.py +0 -2
- package/tests/test_advanced_planning_behavior.py +11 -5
- package/tests/test_async_embedding.py +124 -0
- package/tests/test_autonomy.py +24 -3
- package/tests/test_runtime.py +8 -17
- package/tests/test_schedule_follow_up_action.py +260 -0
- package/tests/test_send_message_action_targets.py +114 -0
- package/tests/test_settings_crypto.py +0 -2
- package/tests/test_validation.py +141 -0
- package/tests/verify_memory_architecture.py +192 -0
- package/uv.lock +1565 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +0 -62
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
|
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
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from typing import Protocol
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from elizaos.advanced_capabilities.actions.schedule_follow_up import (
|
|
10
|
+
schedule_follow_up_action as advanced_schedule_follow_up_action,
|
|
11
|
+
)
|
|
12
|
+
from elizaos.bootstrap.actions.schedule_follow_up import (
|
|
13
|
+
schedule_follow_up_action as bootstrap_schedule_follow_up_action,
|
|
14
|
+
)
|
|
15
|
+
from elizaos.bootstrap.services.follow_up import FollowUpService
|
|
16
|
+
from elizaos.bootstrap.services.rolodex import RolodexService
|
|
17
|
+
from elizaos.types import Content, Memory, as_uuid
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RolodexLike(Protocol):
|
|
21
|
+
async def search_contacts(
|
|
22
|
+
self,
|
|
23
|
+
categories: list[str] | None = None,
|
|
24
|
+
tags: list[str] | None = None,
|
|
25
|
+
search_term: str | None = None,
|
|
26
|
+
) -> list[SimpleNamespace]: ...
|
|
27
|
+
|
|
28
|
+
async def get_contact(self, entity_id: object) -> SimpleNamespace | None: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ActionLike(Protocol):
|
|
32
|
+
async def handler(
|
|
33
|
+
self,
|
|
34
|
+
runtime: object,
|
|
35
|
+
message: Memory,
|
|
36
|
+
state: object | None,
|
|
37
|
+
options: object | None,
|
|
38
|
+
callback: object | None,
|
|
39
|
+
responses: list[Memory] | None,
|
|
40
|
+
) -> object: ...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class FakeFollowUpCall:
|
|
45
|
+
entity_id: object
|
|
46
|
+
priority: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FakeFollowUpService(FollowUpService):
|
|
50
|
+
def __init__(self) -> None:
|
|
51
|
+
self.calls: list[FakeFollowUpCall] = []
|
|
52
|
+
|
|
53
|
+
async def schedule_follow_up(
|
|
54
|
+
self,
|
|
55
|
+
entity_id: object,
|
|
56
|
+
scheduled_at: object,
|
|
57
|
+
reason: str,
|
|
58
|
+
priority: str = "medium",
|
|
59
|
+
message: str | None = None,
|
|
60
|
+
) -> SimpleNamespace:
|
|
61
|
+
self.calls.append(FakeFollowUpCall(entity_id=entity_id, priority=priority))
|
|
62
|
+
return SimpleNamespace(
|
|
63
|
+
entity_id=entity_id,
|
|
64
|
+
scheduled_at=str(scheduled_at),
|
|
65
|
+
reason=reason,
|
|
66
|
+
priority=priority,
|
|
67
|
+
message=message,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FakeRolodexService(RolodexService):
|
|
72
|
+
def __init__(self, contacts: list[SimpleNamespace]) -> None:
|
|
73
|
+
self._contacts = contacts
|
|
74
|
+
|
|
75
|
+
async def search_contacts(
|
|
76
|
+
self,
|
|
77
|
+
categories: list[str] | None = None,
|
|
78
|
+
tags: list[str] | None = None,
|
|
79
|
+
search_term: str | None = None,
|
|
80
|
+
) -> list[SimpleNamespace]:
|
|
81
|
+
if not search_term:
|
|
82
|
+
return self._contacts
|
|
83
|
+
lowered = search_term.lower()
|
|
84
|
+
return [
|
|
85
|
+
contact
|
|
86
|
+
for contact in self._contacts
|
|
87
|
+
if lowered in str(contact.entity_id).lower()
|
|
88
|
+
or lowered in str(getattr(contact, "name", "")).lower()
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
async def get_contact(self, entity_id: object) -> SimpleNamespace | None:
|
|
92
|
+
for contact in self._contacts:
|
|
93
|
+
if contact.entity_id == entity_id:
|
|
94
|
+
return contact
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class FakeRuntime:
|
|
99
|
+
def __init__(self, rolodex: RolodexLike, follow_up: FakeFollowUpService, response: str) -> None:
|
|
100
|
+
self._rolodex = rolodex
|
|
101
|
+
self._follow_up = follow_up
|
|
102
|
+
self._response = response
|
|
103
|
+
|
|
104
|
+
def get_service(self, name: str) -> object | None:
|
|
105
|
+
if name == "rolodex":
|
|
106
|
+
return self._rolodex
|
|
107
|
+
if name == "follow_up":
|
|
108
|
+
return self._follow_up
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
async def compose_state(self, _message: Memory, _providers: list[str]) -> SimpleNamespace:
|
|
112
|
+
return SimpleNamespace(values={}, data={}, text="")
|
|
113
|
+
|
|
114
|
+
def get_setting(self, _key: str) -> object | None:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def compose_prompt_from_state(self, state: object, template: str) -> str:
|
|
118
|
+
return f"{template}\n{state}"
|
|
119
|
+
|
|
120
|
+
async def use_model(self, _model_type: object, _params: dict[str, object]) -> str:
|
|
121
|
+
return self._response
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@pytest.mark.asyncio
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
"action_under_test",
|
|
127
|
+
[bootstrap_schedule_follow_up_action, advanced_schedule_follow_up_action],
|
|
128
|
+
)
|
|
129
|
+
async def test_schedule_follow_up_fails_when_contact_unresolved(
|
|
130
|
+
action_under_test: ActionLike,
|
|
131
|
+
) -> None:
|
|
132
|
+
follow_up_service = FakeFollowUpService()
|
|
133
|
+
rolodex_service = FakeRolodexService(contacts=[])
|
|
134
|
+
runtime = FakeRuntime(
|
|
135
|
+
rolodex=rolodex_service,
|
|
136
|
+
follow_up=follow_up_service,
|
|
137
|
+
response=(
|
|
138
|
+
"<response>"
|
|
139
|
+
"<contactName>missing-contact</contactName>"
|
|
140
|
+
"<scheduledAt>2026-01-02T12:00:00Z</scheduledAt>"
|
|
141
|
+
"<reason>Check-in</reason>"
|
|
142
|
+
"<priority>high</priority>"
|
|
143
|
+
"</response>"
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
message = Memory(
|
|
148
|
+
id=as_uuid("70000000-0000-0000-0000-000000000001"),
|
|
149
|
+
room_id=as_uuid("70000000-0000-0000-0000-000000000002"),
|
|
150
|
+
entity_id=None,
|
|
151
|
+
content=Content(text="follow up with missing-contact next week"),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
result = await action_under_test.handler(
|
|
155
|
+
runtime,
|
|
156
|
+
message,
|
|
157
|
+
None,
|
|
158
|
+
None,
|
|
159
|
+
None,
|
|
160
|
+
None,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
assert result is not None
|
|
164
|
+
assert result.success is False
|
|
165
|
+
assert len(follow_up_service.calls) == 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.mark.asyncio
|
|
169
|
+
@pytest.mark.parametrize(
|
|
170
|
+
"action_under_test",
|
|
171
|
+
[bootstrap_schedule_follow_up_action, advanced_schedule_follow_up_action],
|
|
172
|
+
)
|
|
173
|
+
async def test_schedule_follow_up_normalizes_priority_and_schedules(
|
|
174
|
+
action_under_test: ActionLike,
|
|
175
|
+
) -> None:
|
|
176
|
+
follow_up_service = FakeFollowUpService()
|
|
177
|
+
contact_id = as_uuid("80000000-0000-0000-0000-000000000001")
|
|
178
|
+
rolodex_service = FakeRolodexService(
|
|
179
|
+
contacts=[SimpleNamespace(entity_id=contact_id, name="known-contact")]
|
|
180
|
+
)
|
|
181
|
+
runtime = FakeRuntime(
|
|
182
|
+
rolodex=rolodex_service,
|
|
183
|
+
follow_up=follow_up_service,
|
|
184
|
+
response=(
|
|
185
|
+
"<response>"
|
|
186
|
+
"<contactName>known-contact</contactName>"
|
|
187
|
+
"<scheduledAt>2026-01-03T09:30:00Z</scheduledAt>"
|
|
188
|
+
"<reason>Status update</reason>"
|
|
189
|
+
"<priority>urgent</priority>"
|
|
190
|
+
"</response>"
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
message = Memory(
|
|
195
|
+
id=as_uuid("80000000-0000-0000-0000-000000000002"),
|
|
196
|
+
room_id=as_uuid("80000000-0000-0000-0000-000000000003"),
|
|
197
|
+
entity_id=None,
|
|
198
|
+
content=Content(text="schedule follow-up with known-contact"),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
result = await action_under_test.handler(
|
|
202
|
+
runtime,
|
|
203
|
+
message,
|
|
204
|
+
None,
|
|
205
|
+
None,
|
|
206
|
+
None,
|
|
207
|
+
None,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert result is not None
|
|
211
|
+
assert result.success is True
|
|
212
|
+
assert len(follow_up_service.calls) == 1
|
|
213
|
+
assert follow_up_service.calls[0].priority == "medium"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@pytest.mark.asyncio
|
|
217
|
+
@pytest.mark.parametrize(
|
|
218
|
+
"action_under_test",
|
|
219
|
+
[bootstrap_schedule_follow_up_action, advanced_schedule_follow_up_action],
|
|
220
|
+
)
|
|
221
|
+
async def test_schedule_follow_up_rejects_invalid_scheduled_at(
|
|
222
|
+
action_under_test: ActionLike,
|
|
223
|
+
) -> None:
|
|
224
|
+
follow_up_service = FakeFollowUpService()
|
|
225
|
+
contact_id = as_uuid("90000000-0000-0000-0000-000000000001")
|
|
226
|
+
rolodex_service = FakeRolodexService(
|
|
227
|
+
contacts=[SimpleNamespace(entity_id=contact_id, name="known-contact")]
|
|
228
|
+
)
|
|
229
|
+
runtime = FakeRuntime(
|
|
230
|
+
rolodex=rolodex_service,
|
|
231
|
+
follow_up=follow_up_service,
|
|
232
|
+
response=(
|
|
233
|
+
"<response>"
|
|
234
|
+
"<contactName>known-contact</contactName>"
|
|
235
|
+
"<scheduledAt>not-a-date</scheduledAt>"
|
|
236
|
+
"<reason>Status update</reason>"
|
|
237
|
+
"<priority>high</priority>"
|
|
238
|
+
"</response>"
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
message = Memory(
|
|
243
|
+
id=as_uuid("90000000-0000-0000-0000-000000000002"),
|
|
244
|
+
room_id=as_uuid("90000000-0000-0000-0000-000000000003"),
|
|
245
|
+
entity_id=None,
|
|
246
|
+
content=Content(text="schedule follow-up with known-contact"),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
result = await action_under_test.handler(
|
|
250
|
+
runtime,
|
|
251
|
+
message,
|
|
252
|
+
None,
|
|
253
|
+
None,
|
|
254
|
+
None,
|
|
255
|
+
None,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
assert result is not None
|
|
259
|
+
assert result.success is False
|
|
260
|
+
assert len(follow_up_service.calls) == 0
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import AsyncMock, MagicMock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from elizaos.advanced_capabilities.actions.send_message import (
|
|
9
|
+
send_message_action as advanced_send_message_action,
|
|
10
|
+
)
|
|
11
|
+
from elizaos.bootstrap.actions.send_message import (
|
|
12
|
+
send_message_action as bootstrap_send_message_action,
|
|
13
|
+
)
|
|
14
|
+
from elizaos.types.memory import Memory
|
|
15
|
+
from elizaos.types.primitives import Content, as_uuid
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _make_runtime() -> MagicMock:
|
|
19
|
+
runtime = MagicMock()
|
|
20
|
+
runtime.agent_id = as_uuid("42345678-1234-1234-1234-123456789001")
|
|
21
|
+
runtime.create_memory = AsyncMock()
|
|
22
|
+
runtime.emit_event = AsyncMock()
|
|
23
|
+
runtime.send_message_to_target = AsyncMock()
|
|
24
|
+
runtime.get_room = AsyncMock(return_value=None)
|
|
25
|
+
runtime.get_rooms = AsyncMock(return_value=[])
|
|
26
|
+
runtime.get_entities_for_room = AsyncMock(return_value=[])
|
|
27
|
+
return runtime
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _make_message() -> Memory:
|
|
31
|
+
return Memory(
|
|
32
|
+
id=as_uuid("42345678-1234-1234-1234-123456789010"),
|
|
33
|
+
entity_id=as_uuid("42345678-1234-1234-1234-123456789011"),
|
|
34
|
+
room_id=as_uuid("42345678-1234-1234-1234-123456789012"),
|
|
35
|
+
content=Content(text="fallback", source="telegram"),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
@pytest.mark.parametrize(
|
|
41
|
+
"action_under_test",
|
|
42
|
+
[bootstrap_send_message_action, advanced_send_message_action],
|
|
43
|
+
)
|
|
44
|
+
async def test_send_message_uses_room_target_parameters(action_under_test: object) -> None:
|
|
45
|
+
runtime = _make_runtime()
|
|
46
|
+
message = _make_message()
|
|
47
|
+
target_room_id = "42345678-1234-1234-1234-123456789099"
|
|
48
|
+
|
|
49
|
+
result = await action_under_test.handler(
|
|
50
|
+
runtime,
|
|
51
|
+
message,
|
|
52
|
+
None,
|
|
53
|
+
SimpleNamespace(
|
|
54
|
+
parameters={
|
|
55
|
+
"targetType": "room",
|
|
56
|
+
"target": target_room_id,
|
|
57
|
+
"source": "discord",
|
|
58
|
+
"text": "ship it",
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
None,
|
|
62
|
+
None,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assert result.success is True
|
|
66
|
+
assert result.values["targetType"] == "room"
|
|
67
|
+
assert result.values["targetRoomId"] == target_room_id
|
|
68
|
+
runtime.create_memory.assert_awaited_once()
|
|
69
|
+
create_kwargs = runtime.create_memory.await_args.kwargs
|
|
70
|
+
assert create_kwargs["room_id"] == as_uuid(target_room_id)
|
|
71
|
+
|
|
72
|
+
runtime.send_message_to_target.assert_awaited_once()
|
|
73
|
+
send_target = runtime.send_message_to_target.await_args.args[0]
|
|
74
|
+
assert str(send_target.room_id) == target_room_id
|
|
75
|
+
assert send_target.source == "discord"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.asyncio
|
|
79
|
+
@pytest.mark.parametrize(
|
|
80
|
+
"action_under_test",
|
|
81
|
+
[bootstrap_send_message_action, advanced_send_message_action],
|
|
82
|
+
)
|
|
83
|
+
async def test_send_message_resolves_user_target_from_room_entities(
|
|
84
|
+
action_under_test: object,
|
|
85
|
+
) -> None:
|
|
86
|
+
runtime = _make_runtime()
|
|
87
|
+
message = _make_message()
|
|
88
|
+
target_entity_id = as_uuid("42345678-1234-1234-1234-123456789088")
|
|
89
|
+
runtime.get_entities_for_room = AsyncMock(
|
|
90
|
+
return_value=[SimpleNamespace(id=target_entity_id, names=["Alice"])]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
result = await action_under_test.handler(
|
|
94
|
+
runtime,
|
|
95
|
+
message,
|
|
96
|
+
None,
|
|
97
|
+
SimpleNamespace(
|
|
98
|
+
parameters={
|
|
99
|
+
"targetType": "user",
|
|
100
|
+
"target": "alice",
|
|
101
|
+
"source": "discord",
|
|
102
|
+
"text": "hello",
|
|
103
|
+
}
|
|
104
|
+
),
|
|
105
|
+
None,
|
|
106
|
+
None,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assert result.success is True
|
|
110
|
+
assert result.values["targetType"] == "user"
|
|
111
|
+
assert result.values["targetEntityId"] == target_entity_id
|
|
112
|
+
runtime.send_message_to_target.assert_awaited_once()
|
|
113
|
+
send_target = runtime.send_message_to_target.await_args.args[0]
|
|
114
|
+
assert str(send_target.entity_id) == target_entity_id
|
|
@@ -78,7 +78,6 @@ class TestSettingsCrypto:
|
|
|
78
78
|
assert migrated.startswith("v2:")
|
|
79
79
|
assert decrypt_string_value(migrated, salt) == plaintext
|
|
80
80
|
|
|
81
|
-
@pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
|
|
82
81
|
def test_runtime_get_setting_decrypts_secret_strings(
|
|
83
82
|
self, monkeypatch: pytest.MonkeyPatch
|
|
84
83
|
) -> None:
|
|
@@ -96,7 +95,6 @@ class TestSettingsCrypto:
|
|
|
96
95
|
runtime = AgentRuntime(character=character)
|
|
97
96
|
assert runtime.get_setting("API_KEY") == "super-secret"
|
|
98
97
|
|
|
99
|
-
@pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
|
|
100
98
|
def test_runtime_get_setting_coerces_true_false_strings(
|
|
101
99
|
self, monkeypatch: pytest.MonkeyPatch
|
|
102
100
|
) -> None:
|