@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.
Files changed (197) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +239 -0
  3. package/elizaos/__init__.py +280 -0
  4. package/elizaos/action_docs.py +149 -0
  5. package/elizaos/advanced_capabilities/__init__.py +85 -0
  6. package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
  7. package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
  8. package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
  9. package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
  10. package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
  11. package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
  12. package/elizaos/advanced_capabilities/actions/roles.py +207 -0
  13. package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
  14. package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
  15. package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
  16. package/elizaos/advanced_capabilities/actions/settings.py +151 -0
  17. package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
  18. package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
  19. package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
  20. package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
  21. package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
  22. package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
  23. package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
  24. package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
  25. package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
  26. package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
  27. package/elizaos/advanced_capabilities/providers/facts.py +82 -0
  28. package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
  29. package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
  30. package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
  31. package/elizaos/advanced_capabilities/providers/roles.py +97 -0
  32. package/elizaos/advanced_capabilities/providers/settings.py +51 -0
  33. package/elizaos/advanced_capabilities/services/__init__.py +18 -0
  34. package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
  35. package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
  36. package/elizaos/advanced_memory/__init__.py +3 -0
  37. package/elizaos/advanced_memory/evaluators.py +97 -0
  38. package/elizaos/advanced_memory/memory_service.py +556 -0
  39. package/elizaos/advanced_memory/plugin.py +30 -0
  40. package/elizaos/advanced_memory/prompts.py +12 -0
  41. package/elizaos/advanced_memory/providers.py +90 -0
  42. package/elizaos/advanced_memory/types.py +65 -0
  43. package/elizaos/advanced_planning/__init__.py +10 -0
  44. package/elizaos/advanced_planning/actions.py +145 -0
  45. package/elizaos/advanced_planning/message_classifier.py +127 -0
  46. package/elizaos/advanced_planning/planning_service.py +712 -0
  47. package/elizaos/advanced_planning/plugin.py +40 -0
  48. package/elizaos/advanced_planning/prompts.py +4 -0
  49. package/elizaos/basic_capabilities/__init__.py +66 -0
  50. package/elizaos/basic_capabilities/actions/__init__.py +24 -0
  51. package/elizaos/basic_capabilities/actions/choice.py +140 -0
  52. package/elizaos/basic_capabilities/actions/ignore.py +66 -0
  53. package/elizaos/basic_capabilities/actions/none.py +56 -0
  54. package/elizaos/basic_capabilities/actions/reply.py +120 -0
  55. package/elizaos/basic_capabilities/providers/__init__.py +54 -0
  56. package/elizaos/basic_capabilities/providers/action_state.py +113 -0
  57. package/elizaos/basic_capabilities/providers/actions.py +263 -0
  58. package/elizaos/basic_capabilities/providers/attachments.py +76 -0
  59. package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
  60. package/elizaos/basic_capabilities/providers/character.py +113 -0
  61. package/elizaos/basic_capabilities/providers/choice.py +73 -0
  62. package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
  63. package/elizaos/basic_capabilities/providers/current_time.py +58 -0
  64. package/elizaos/basic_capabilities/providers/entities.py +99 -0
  65. package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
  66. package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
  67. package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
  68. package/elizaos/basic_capabilities/providers/time.py +45 -0
  69. package/elizaos/basic_capabilities/providers/world.py +93 -0
  70. package/elizaos/basic_capabilities/services/__init__.py +18 -0
  71. package/elizaos/basic_capabilities/services/embedding.py +122 -0
  72. package/elizaos/basic_capabilities/services/task.py +178 -0
  73. package/elizaos/bootstrap/__init__.py +12 -0
  74. package/elizaos/bootstrap/actions/__init__.py +68 -0
  75. package/elizaos/bootstrap/actions/add_contact.py +149 -0
  76. package/elizaos/bootstrap/actions/choice.py +147 -0
  77. package/elizaos/bootstrap/actions/follow_room.py +151 -0
  78. package/elizaos/bootstrap/actions/ignore.py +80 -0
  79. package/elizaos/bootstrap/actions/image_generation.py +135 -0
  80. package/elizaos/bootstrap/actions/mute_room.py +151 -0
  81. package/elizaos/bootstrap/actions/none.py +71 -0
  82. package/elizaos/bootstrap/actions/remove_contact.py +159 -0
  83. package/elizaos/bootstrap/actions/reply.py +140 -0
  84. package/elizaos/bootstrap/actions/roles.py +193 -0
  85. package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
  86. package/elizaos/bootstrap/actions/search_contacts.py +159 -0
  87. package/elizaos/bootstrap/actions/send_message.py +173 -0
  88. package/elizaos/bootstrap/actions/settings.py +165 -0
  89. package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
  90. package/elizaos/bootstrap/actions/unmute_room.py +151 -0
  91. package/elizaos/bootstrap/actions/update_contact.py +178 -0
  92. package/elizaos/bootstrap/actions/update_entity.py +175 -0
  93. package/elizaos/bootstrap/autonomy/__init__.py +18 -0
  94. package/elizaos/bootstrap/autonomy/action.py +197 -0
  95. package/elizaos/bootstrap/autonomy/providers.py +165 -0
  96. package/elizaos/bootstrap/autonomy/routes.py +171 -0
  97. package/elizaos/bootstrap/autonomy/service.py +562 -0
  98. package/elizaos/bootstrap/autonomy/types.py +18 -0
  99. package/elizaos/bootstrap/evaluators/__init__.py +19 -0
  100. package/elizaos/bootstrap/evaluators/reflection.py +118 -0
  101. package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
  102. package/elizaos/bootstrap/plugin.py +140 -0
  103. package/elizaos/bootstrap/providers/__init__.py +80 -0
  104. package/elizaos/bootstrap/providers/action_state.py +71 -0
  105. package/elizaos/bootstrap/providers/actions.py +256 -0
  106. package/elizaos/bootstrap/providers/agent_settings.py +63 -0
  107. package/elizaos/bootstrap/providers/attachments.py +76 -0
  108. package/elizaos/bootstrap/providers/capabilities.py +66 -0
  109. package/elizaos/bootstrap/providers/character.py +128 -0
  110. package/elizaos/bootstrap/providers/choice.py +77 -0
  111. package/elizaos/bootstrap/providers/contacts.py +78 -0
  112. package/elizaos/bootstrap/providers/context_bench.py +49 -0
  113. package/elizaos/bootstrap/providers/current_time.py +56 -0
  114. package/elizaos/bootstrap/providers/entities.py +99 -0
  115. package/elizaos/bootstrap/providers/evaluators.py +58 -0
  116. package/elizaos/bootstrap/providers/facts.py +86 -0
  117. package/elizaos/bootstrap/providers/follow_ups.py +116 -0
  118. package/elizaos/bootstrap/providers/knowledge.py +73 -0
  119. package/elizaos/bootstrap/providers/providers_list.py +59 -0
  120. package/elizaos/bootstrap/providers/recent_messages.py +85 -0
  121. package/elizaos/bootstrap/providers/relationships.py +106 -0
  122. package/elizaos/bootstrap/providers/roles.py +95 -0
  123. package/elizaos/bootstrap/providers/settings.py +55 -0
  124. package/elizaos/bootstrap/providers/time.py +45 -0
  125. package/elizaos/bootstrap/providers/world.py +97 -0
  126. package/elizaos/bootstrap/services/__init__.py +26 -0
  127. package/elizaos/bootstrap/services/embedding.py +122 -0
  128. package/elizaos/bootstrap/services/follow_up.py +138 -0
  129. package/elizaos/bootstrap/services/rolodex.py +244 -0
  130. package/elizaos/bootstrap/services/task.py +585 -0
  131. package/elizaos/bootstrap/types.py +54 -0
  132. package/elizaos/bootstrap/utils/__init__.py +7 -0
  133. package/elizaos/bootstrap/utils/xml.py +69 -0
  134. package/elizaos/character.py +149 -0
  135. package/elizaos/logger.py +179 -0
  136. package/elizaos/media/__init__.py +45 -0
  137. package/elizaos/media/mime.py +315 -0
  138. package/elizaos/media/search.py +161 -0
  139. package/elizaos/media/tests/__init__.py +1 -0
  140. package/elizaos/media/tests/test_mime.py +117 -0
  141. package/elizaos/media/tests/test_search.py +156 -0
  142. package/elizaos/plugin.py +191 -0
  143. package/elizaos/prompts.py +1071 -0
  144. package/elizaos/py.typed +0 -0
  145. package/elizaos/runtime.py +2572 -0
  146. package/elizaos/services/__init__.py +49 -0
  147. package/elizaos/services/hook_service.py +511 -0
  148. package/elizaos/services/message_service.py +1248 -0
  149. package/elizaos/settings.py +182 -0
  150. package/elizaos/streaming_context.py +159 -0
  151. package/elizaos/trajectory_context.py +18 -0
  152. package/elizaos/types/__init__.py +512 -0
  153. package/elizaos/types/agent.py +31 -0
  154. package/elizaos/types/components.py +208 -0
  155. package/elizaos/types/database.py +64 -0
  156. package/elizaos/types/environment.py +46 -0
  157. package/elizaos/types/events.py +47 -0
  158. package/elizaos/types/memory.py +45 -0
  159. package/elizaos/types/model.py +393 -0
  160. package/elizaos/types/plugin.py +188 -0
  161. package/elizaos/types/primitives.py +100 -0
  162. package/elizaos/types/runtime.py +460 -0
  163. package/elizaos/types/service.py +113 -0
  164. package/elizaos/types/service_interfaces.py +244 -0
  165. package/elizaos/types/state.py +188 -0
  166. package/elizaos/types/task.py +29 -0
  167. package/elizaos/utils/__init__.py +108 -0
  168. package/elizaos/utils/spec_examples.py +48 -0
  169. package/elizaos/utils/streaming.py +426 -0
  170. package/elizaos_atropos_shared/__init__.py +1 -0
  171. package/elizaos_atropos_shared/canonical_eliza.py +282 -0
  172. package/package.json +19 -0
  173. package/pyproject.toml +143 -0
  174. package/requirements-dev.in +11 -0
  175. package/requirements-dev.lock +134 -0
  176. package/requirements.in +9 -0
  177. package/requirements.lock +64 -0
  178. package/tests/__init__.py +0 -0
  179. package/tests/test_action_parameters.py +154 -0
  180. package/tests/test_actions_provider_examples.py +39 -0
  181. package/tests/test_advanced_memory_behavior.py +96 -0
  182. package/tests/test_advanced_memory_flag.py +30 -0
  183. package/tests/test_advanced_planning_behavior.py +225 -0
  184. package/tests/test_advanced_planning_flag.py +26 -0
  185. package/tests/test_autonomy.py +445 -0
  186. package/tests/test_bootstrap_initialize.py +37 -0
  187. package/tests/test_character.py +163 -0
  188. package/tests/test_character_provider.py +231 -0
  189. package/tests/test_dynamic_prompt_exec.py +561 -0
  190. package/tests/test_logger_redaction.py +43 -0
  191. package/tests/test_plugin.py +117 -0
  192. package/tests/test_runtime.py +422 -0
  193. package/tests/test_salt_production_enforcement.py +22 -0
  194. package/tests/test_settings_crypto.py +118 -0
  195. package/tests/test_streaming.py +295 -0
  196. package/tests/test_types.py +221 -0
  197. package/tests/test_uuid_parity.py +46 -0
@@ -0,0 +1,117 @@
1
+ import pytest
2
+
3
+ from elizaos.plugin import (
4
+ PluginLoadError,
5
+ resolve_plugin_dependencies,
6
+ )
7
+ from elizaos.types import Plugin
8
+
9
+
10
+ class TestResolveDependencies:
11
+ def test_no_dependencies(self) -> None:
12
+ plugins = [
13
+ Plugin(name="plugin-a", description="Plugin A"),
14
+ Plugin(name="plugin-b", description="Plugin B"),
15
+ ]
16
+ result = resolve_plugin_dependencies(plugins)
17
+ assert len(result) == 2
18
+
19
+ def test_simple_dependency(self) -> None:
20
+ plugins = [
21
+ Plugin(
22
+ name="plugin-b",
23
+ description="Plugin B",
24
+ dependencies=["plugin-a"],
25
+ ),
26
+ Plugin(name="plugin-a", description="Plugin A"),
27
+ ]
28
+ result = resolve_plugin_dependencies(plugins)
29
+ assert result[0].name == "plugin-a"
30
+ assert result[1].name == "plugin-b"
31
+
32
+ def test_chain_dependency(self) -> None:
33
+ plugins = [
34
+ Plugin(
35
+ name="plugin-c",
36
+ description="Plugin C",
37
+ dependencies=["plugin-b"],
38
+ ),
39
+ Plugin(
40
+ name="plugin-b",
41
+ description="Plugin B",
42
+ dependencies=["plugin-a"],
43
+ ),
44
+ Plugin(name="plugin-a", description="Plugin A"),
45
+ ]
46
+ result = resolve_plugin_dependencies(plugins)
47
+ names = [p.name for p in result]
48
+ assert names.index("plugin-a") < names.index("plugin-b")
49
+ assert names.index("plugin-b") < names.index("plugin-c")
50
+
51
+ def test_circular_dependency(self) -> None:
52
+ plugins = [
53
+ Plugin(
54
+ name="plugin-a",
55
+ description="Plugin A",
56
+ dependencies=["plugin-b"],
57
+ ),
58
+ Plugin(
59
+ name="plugin-b",
60
+ description="Plugin B",
61
+ dependencies=["plugin-a"],
62
+ ),
63
+ ]
64
+ with pytest.raises(PluginLoadError, match="Circular dependency"):
65
+ resolve_plugin_dependencies(plugins)
66
+
67
+ def test_missing_dependency_handled(self) -> None:
68
+ plugins = [
69
+ Plugin(
70
+ name="plugin-a",
71
+ description="Plugin A",
72
+ dependencies=["external-plugin"],
73
+ ),
74
+ ]
75
+ result = resolve_plugin_dependencies(plugins)
76
+ assert len(result) == 1
77
+
78
+ def test_multiple_dependencies(self) -> None:
79
+ plugins = [
80
+ Plugin(
81
+ name="plugin-c",
82
+ description="Plugin C",
83
+ dependencies=["plugin-a", "plugin-b"],
84
+ ),
85
+ Plugin(name="plugin-a", description="Plugin A"),
86
+ Plugin(name="plugin-b", description="Plugin B"),
87
+ ]
88
+ result = resolve_plugin_dependencies(plugins)
89
+ names = [p.name for p in result]
90
+ assert names.index("plugin-a") < names.index("plugin-c")
91
+ assert names.index("plugin-b") < names.index("plugin-c")
92
+
93
+ def test_diamond_dependency(self) -> None:
94
+ plugins = [
95
+ Plugin(name="plugin-a", description="Plugin A"),
96
+ Plugin(
97
+ name="plugin-b",
98
+ description="Plugin B",
99
+ dependencies=["plugin-a"],
100
+ ),
101
+ Plugin(
102
+ name="plugin-c",
103
+ description="Plugin C",
104
+ dependencies=["plugin-a"],
105
+ ),
106
+ Plugin(
107
+ name="plugin-d",
108
+ description="Plugin D",
109
+ dependencies=["plugin-b", "plugin-c"],
110
+ ),
111
+ ]
112
+ result = resolve_plugin_dependencies(plugins)
113
+ names = [p.name for p in result]
114
+ assert names.index("plugin-a") < names.index("plugin-b")
115
+ assert names.index("plugin-a") < names.index("plugin-c")
116
+ assert names.index("plugin-b") < names.index("plugin-d")
117
+ assert names.index("plugin-c") < names.index("plugin-d")
@@ -0,0 +1,422 @@
1
+ from typing import Any
2
+
3
+ import pytest
4
+
5
+ from elizaos.runtime import AgentRuntime
6
+ from elizaos.types import (
7
+ Action,
8
+ ActionResult,
9
+ Character,
10
+ Evaluator,
11
+ HandlerOptions,
12
+ IAgentRuntime,
13
+ LLMMode,
14
+ Memory,
15
+ ModelType,
16
+ Plugin,
17
+ Provider,
18
+ ProviderResult,
19
+ State,
20
+ as_uuid,
21
+ )
22
+
23
+
24
+ @pytest.fixture
25
+ def character() -> Character:
26
+ return Character(
27
+ name="TestAgent",
28
+ bio=["A test agent for unit testing."],
29
+ system="You are a helpful test agent.",
30
+ )
31
+
32
+
33
+ @pytest.fixture
34
+ def runtime(character: Character) -> AgentRuntime:
35
+ return AgentRuntime(character=character)
36
+
37
+
38
+ class TestAgentRuntimeInit:
39
+ def test_runtime_creation(self, character: Character) -> None:
40
+ runtime = AgentRuntime(character=character)
41
+ assert runtime.character.name == "TestAgent"
42
+ assert runtime.agent_id is not None
43
+
44
+ def test_runtime_with_agent_id(self, character: Character) -> None:
45
+ agent_id = as_uuid("12345678-1234-1234-1234-123456789012")
46
+ runtime = AgentRuntime(character=character, agent_id=agent_id)
47
+ assert runtime.agent_id == agent_id
48
+
49
+ def test_runtime_with_settings(self, character: Character) -> None:
50
+ runtime = AgentRuntime(
51
+ character=character,
52
+ settings={"custom_setting": "value"},
53
+ )
54
+ assert runtime.get_setting("custom_setting") == "value"
55
+
56
+
57
+ class TestAgentRuntimeSettings:
58
+ def test_get_setting_from_runtime(self, runtime: AgentRuntime) -> None:
59
+ runtime.set_setting("test_key", "test_value")
60
+ assert runtime.get_setting("test_key") == "test_value"
61
+
62
+ @pytest.mark.skip(reason="CharacterSettings proto doesn't support arbitrary fields")
63
+ def test_get_setting_from_character(self) -> None:
64
+ character = Character(
65
+ name="Test",
66
+ bio=["Test"],
67
+ settings={"char_setting": "char_value"},
68
+ )
69
+ runtime = AgentRuntime(character=character)
70
+ assert runtime.get_setting("char_setting") == "char_value"
71
+
72
+ @pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
73
+ def test_get_setting_from_secrets(self) -> None:
74
+ character = Character(
75
+ name="Test",
76
+ bio=["Test"],
77
+ secrets={"API_KEY": "secret_key"},
78
+ )
79
+ runtime = AgentRuntime(character=character)
80
+ assert runtime.get_setting("API_KEY") == "secret_key"
81
+
82
+ def test_get_nonexistent_setting(self, runtime: AgentRuntime) -> None:
83
+ assert runtime.get_setting("nonexistent") is None
84
+
85
+
86
+ class TestAgentRuntimeProviders:
87
+ @pytest.mark.asyncio
88
+ async def test_register_provider(self, runtime: AgentRuntime) -> None:
89
+ async def get_data(rt: IAgentRuntime, msg: Memory, state: State) -> ProviderResult:
90
+ return ProviderResult(text="Provider data")
91
+
92
+ provider = Provider(
93
+ name="test-provider",
94
+ description="A test provider",
95
+ get=get_data,
96
+ )
97
+ runtime.register_provider(provider)
98
+ assert len(runtime.providers) == 1
99
+ assert runtime.providers[0].name == "test-provider"
100
+
101
+
102
+ class TestAgentRuntimeActions:
103
+ @pytest.mark.asyncio
104
+ async def test_register_action(self, runtime: AgentRuntime) -> None:
105
+ async def validate(rt: IAgentRuntime, msg: Memory, state: State | None) -> bool:
106
+ return True
107
+
108
+ async def handler(
109
+ rt: IAgentRuntime,
110
+ msg: Memory,
111
+ state: State | None,
112
+ options: HandlerOptions | None,
113
+ callback: Any,
114
+ responses: list[Memory] | None,
115
+ ) -> ActionResult | None:
116
+ return ActionResult(success=True)
117
+
118
+ action = Action(
119
+ name="TEST_ACTION",
120
+ description="A test action",
121
+ validate=validate,
122
+ handler=handler,
123
+ )
124
+ runtime.register_action(action)
125
+ assert len(runtime.actions) == 1
126
+ assert runtime.actions[0].name == "TEST_ACTION"
127
+
128
+
129
+ class TestAgentRuntimeEvaluators:
130
+ @pytest.mark.asyncio
131
+ async def test_register_evaluator(self, runtime: AgentRuntime) -> None:
132
+ async def validate(rt: IAgentRuntime, msg: Memory, state: State | None) -> bool:
133
+ return True
134
+
135
+ async def handler(
136
+ rt: IAgentRuntime,
137
+ msg: Memory,
138
+ state: State | None,
139
+ options: HandlerOptions | None,
140
+ callback: Any,
141
+ responses: list[Memory] | None,
142
+ ) -> ActionResult | None:
143
+ return ActionResult(success=True)
144
+
145
+ evaluator = Evaluator(
146
+ name="test-evaluator",
147
+ description="A test evaluator",
148
+ examples=[],
149
+ validate=validate,
150
+ handler=handler,
151
+ )
152
+ runtime.register_evaluator(evaluator)
153
+ assert len(runtime.evaluators) == 1
154
+ assert runtime.evaluators[0].name == "test-evaluator"
155
+
156
+
157
+ class TestAgentRuntimePlugins:
158
+ @pytest.mark.asyncio
159
+ async def test_register_plugin(self, runtime: AgentRuntime) -> None:
160
+ async def get_data(rt: IAgentRuntime, msg: Memory, state: State) -> ProviderResult:
161
+ return ProviderResult(text="Plugin provider data")
162
+
163
+ plugin = Plugin(
164
+ name="test-plugin",
165
+ description="A test plugin",
166
+ providers=[
167
+ Provider(
168
+ name="plugin-provider",
169
+ get=get_data,
170
+ )
171
+ ],
172
+ )
173
+ await runtime.register_plugin(plugin)
174
+ assert len(runtime.plugins) == 1
175
+ assert runtime.plugins[0].name == "test-plugin"
176
+ assert len(runtime.providers) == 1
177
+
178
+
179
+ class TestAgentRuntimeEvents:
180
+ @pytest.mark.asyncio
181
+ async def test_register_event_handler(self, runtime: AgentRuntime) -> None:
182
+ events_received: list[str] = []
183
+
184
+ async def handler(params: dict[str, object]) -> None:
185
+ events_received.append("event_received")
186
+
187
+ runtime.register_event("TEST_EVENT", handler)
188
+ await runtime.emit_event("TEST_EVENT", {"data": "test"})
189
+
190
+ assert len(events_received) == 1
191
+ assert events_received[0] == "event_received"
192
+
193
+ @pytest.mark.asyncio
194
+ async def test_multiple_event_handlers(self, runtime: AgentRuntime) -> None:
195
+ count = [0]
196
+
197
+ async def handler1(params: dict[str, object]) -> None:
198
+ count[0] += 1
199
+
200
+ async def handler2(params: dict[str, object]) -> None:
201
+ count[0] += 1
202
+
203
+ runtime.register_event("MULTI_EVENT", handler1)
204
+ runtime.register_event("MULTI_EVENT", handler2)
205
+ await runtime.emit_event("MULTI_EVENT", {})
206
+
207
+ assert count[0] == 2
208
+
209
+
210
+ class TestAgentRuntimeModels:
211
+ @pytest.mark.asyncio
212
+ async def test_register_model(self, runtime: AgentRuntime) -> None:
213
+ async def model_handler(rt: IAgentRuntime, params: dict[str, Any]) -> Any:
214
+ return f"Generated: {params.get('prompt', '')}"
215
+
216
+ runtime.register_model(
217
+ model_type="TEXT_LARGE",
218
+ handler=model_handler,
219
+ provider="test-provider",
220
+ )
221
+
222
+ result = await runtime.use_model("TEXT_LARGE", {"prompt": "Hello"})
223
+ assert result == "Generated: Hello"
224
+
225
+ @pytest.mark.asyncio
226
+ async def test_model_priority(self, runtime: AgentRuntime) -> None:
227
+ async def low_priority_handler(rt: IAgentRuntime, params: dict[str, Any]) -> Any:
228
+ return "low"
229
+
230
+ async def high_priority_handler(rt: IAgentRuntime, params: dict[str, Any]) -> Any:
231
+ return "high"
232
+
233
+ runtime.register_model(
234
+ model_type="TEXT_LARGE",
235
+ handler=low_priority_handler,
236
+ provider="low",
237
+ priority=0,
238
+ )
239
+ runtime.register_model(
240
+ model_type="TEXT_LARGE",
241
+ handler=high_priority_handler,
242
+ provider="high",
243
+ priority=10,
244
+ )
245
+
246
+ result = await runtime.use_model("TEXT_LARGE", {})
247
+ assert result == "high"
248
+
249
+
250
+ class TestAgentRuntimeRunTracking:
251
+ def test_create_run_id(self, runtime: AgentRuntime) -> None:
252
+ run_id = runtime.create_run_id()
253
+ assert run_id is not None
254
+ assert len(run_id) == 36
255
+
256
+ def test_start_and_end_run(self, runtime: AgentRuntime) -> None:
257
+ room_id = as_uuid("12345678-1234-1234-1234-123456789012")
258
+ run_id = runtime.start_run(room_id)
259
+ assert run_id == runtime.get_current_run_id()
260
+
261
+ runtime.end_run()
262
+ new_run_id = runtime.get_current_run_id()
263
+ assert new_run_id != run_id
264
+
265
+
266
+ class TestAgentRuntimeServices:
267
+ def test_has_service_empty(self, runtime: AgentRuntime) -> None:
268
+ assert runtime.has_service("test-service") is False
269
+
270
+ def test_get_service_empty(self, runtime: AgentRuntime) -> None:
271
+ assert runtime.get_service("test-service") is None
272
+
273
+ def test_get_registered_service_types_empty(self, runtime: AgentRuntime) -> None:
274
+ assert runtime.get_registered_service_types() == []
275
+
276
+
277
+ class TestAgentRuntimeLogLevel:
278
+ def test_default_log_level_is_error(self, character: Character) -> None:
279
+ runtime = AgentRuntime(character=character)
280
+ assert runtime.logger is not None
281
+
282
+ def test_custom_log_level_info(self, character: Character) -> None:
283
+ runtime = AgentRuntime(character=character, log_level="INFO")
284
+ assert runtime.logger is not None
285
+
286
+ def test_custom_log_level_debug(self, character: Character) -> None:
287
+ runtime = AgentRuntime(character=character, log_level="DEBUG")
288
+ assert runtime.logger is not None
289
+
290
+ def test_custom_log_level_warning(self, character: Character) -> None:
291
+ runtime = AgentRuntime(character=character, log_level="WARNING")
292
+ assert runtime.logger is not None
293
+
294
+
295
+ class TestAgentRuntimeLLMMode:
296
+ def test_default_llm_mode_is_default(self, character: Character) -> None:
297
+ runtime = AgentRuntime(character=character)
298
+ assert runtime.get_llm_mode() == LLMMode.DEFAULT
299
+
300
+ def test_constructor_option_small(self, character: Character) -> None:
301
+ runtime = AgentRuntime(character=character, llm_mode=LLMMode.SMALL)
302
+ assert runtime.get_llm_mode() == LLMMode.SMALL
303
+
304
+ def test_constructor_option_large(self, character: Character) -> None:
305
+ runtime = AgentRuntime(character=character, llm_mode=LLMMode.LARGE)
306
+ assert runtime.get_llm_mode() == LLMMode.LARGE
307
+
308
+ @pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
309
+ def test_character_setting_small(self) -> None:
310
+ character = Character(
311
+ name="Test",
312
+ bio=["Test"],
313
+ settings={"LLM_MODE": "SMALL"},
314
+ )
315
+ runtime = AgentRuntime(character=character)
316
+ assert runtime.get_llm_mode() == LLMMode.SMALL
317
+
318
+ @pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
319
+ def test_constructor_option_takes_precedence(self) -> None:
320
+ character = Character(
321
+ name="Test",
322
+ bio=["Test"],
323
+ settings={"LLM_MODE": "SMALL"},
324
+ )
325
+ runtime = AgentRuntime(character=character, llm_mode=LLMMode.LARGE)
326
+ assert runtime.get_llm_mode() == LLMMode.LARGE
327
+
328
+ @pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
329
+ def test_case_insensitive_character_setting(self) -> None:
330
+ character = Character(
331
+ name="Test",
332
+ bio=["Test"],
333
+ settings={"LLM_MODE": "small"},
334
+ )
335
+ runtime = AgentRuntime(character=character)
336
+ assert runtime.get_llm_mode() == LLMMode.SMALL
337
+
338
+ @pytest.mark.skip(reason="CharacterSettings proto doesn't have LLM_MODE field")
339
+ def test_invalid_setting_defaults_to_default(self) -> None:
340
+ character = Character(
341
+ name="Test",
342
+ bio=["Test"],
343
+ settings={"LLM_MODE": "invalid"},
344
+ )
345
+ runtime = AgentRuntime(character=character)
346
+ assert runtime.get_llm_mode() == LLMMode.DEFAULT
347
+
348
+ @pytest.mark.asyncio
349
+ async def test_use_model_override_small(self, character: Character) -> None:
350
+ runtime = AgentRuntime(character=character, llm_mode=LLMMode.SMALL)
351
+
352
+ async def small_handler(rt: IAgentRuntime, params: dict[str, Any]) -> Any:
353
+ return "small response"
354
+
355
+ async def large_handler(rt: IAgentRuntime, params: dict[str, Any]) -> Any:
356
+ return "large response"
357
+
358
+ runtime.register_model(ModelType.TEXT_SMALL, small_handler, "test")
359
+ runtime.register_model(ModelType.TEXT_LARGE, large_handler, "test")
360
+
361
+ result = await runtime.use_model(ModelType.TEXT_LARGE, {"prompt": "test"})
362
+ assert result == "small response"
363
+
364
+ @pytest.mark.asyncio
365
+ async def test_use_model_override_large(self, character: Character) -> None:
366
+ runtime = AgentRuntime(character=character, llm_mode=LLMMode.LARGE)
367
+
368
+ async def small_handler(rt: IAgentRuntime, params: dict[str, Any]) -> Any:
369
+ return "small response"
370
+
371
+ async def large_handler(rt: IAgentRuntime, params: dict[str, Any]) -> Any:
372
+ return "large response"
373
+
374
+ runtime.register_model(ModelType.TEXT_SMALL, small_handler, "test")
375
+ runtime.register_model(ModelType.TEXT_LARGE, large_handler, "test")
376
+
377
+ result = await runtime.use_model(ModelType.TEXT_SMALL, {"prompt": "test"})
378
+ assert result == "large response"
379
+
380
+
381
+ class TestAgentRuntimeCheckShouldRespond:
382
+ def test_default_is_true(self, character: Character) -> None:
383
+ runtime = AgentRuntime(character=character)
384
+ assert runtime.is_check_should_respond_enabled() is True
385
+
386
+ def test_constructor_option_false(self, character: Character) -> None:
387
+ runtime = AgentRuntime(character=character, check_should_respond=False)
388
+ assert runtime.is_check_should_respond_enabled() is False
389
+
390
+ def test_constructor_option_true(self, character: Character) -> None:
391
+ runtime = AgentRuntime(character=character, check_should_respond=True)
392
+ assert runtime.is_check_should_respond_enabled() is True
393
+
394
+ @pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
395
+ def test_character_setting_false(self) -> None:
396
+ character = Character(
397
+ name="Test",
398
+ bio=["Test"],
399
+ settings={"CHECK_SHOULD_RESPOND": "false"},
400
+ )
401
+ runtime = AgentRuntime(character=character)
402
+ assert runtime.is_check_should_respond_enabled() is False
403
+
404
+ @pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
405
+ def test_constructor_option_takes_precedence(self) -> None:
406
+ character = Character(
407
+ name="Test",
408
+ bio=["Test"],
409
+ settings={"CHECK_SHOULD_RESPOND": "false"},
410
+ )
411
+ runtime = AgentRuntime(character=character, check_should_respond=True)
412
+ assert runtime.is_check_should_respond_enabled() is True
413
+
414
+ @pytest.mark.skip(reason="CharacterSettings proto doesn't have CHECK_SHOULD_RESPOND field")
415
+ def test_non_false_string_defaults_to_true(self) -> None:
416
+ character = Character(
417
+ name="Test",
418
+ bio=["Test"],
419
+ settings={"CHECK_SHOULD_RESPOND": "yes"},
420
+ )
421
+ runtime = AgentRuntime(character=character)
422
+ assert runtime.is_check_should_respond_enabled() is True
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from elizaos.settings import get_salt
6
+
7
+
8
+ def test_get_salt_throws_in_production_when_default(monkeypatch: pytest.MonkeyPatch) -> None:
9
+ monkeypatch.setenv("NODE_ENV", "production")
10
+ monkeypatch.delenv("SECRET_SALT", raising=False)
11
+ monkeypatch.delenv("ELIZA_ALLOW_DEFAULT_SECRET_SALT", raising=False)
12
+
13
+ with pytest.raises(RuntimeError, match="SECRET_SALT must be set"):
14
+ get_salt()
15
+
16
+
17
+ def test_get_salt_allows_override_in_production(monkeypatch: pytest.MonkeyPatch) -> None:
18
+ monkeypatch.setenv("NODE_ENV", "production")
19
+ monkeypatch.delenv("SECRET_SALT", raising=False)
20
+ monkeypatch.setenv("ELIZA_ALLOW_DEFAULT_SECRET_SALT", "true")
21
+
22
+ assert get_salt() == "secretsalt"
@@ -0,0 +1,118 @@
1
+ import secrets
2
+
3
+ import pytest
4
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
5
+ from cryptography.hazmat.primitives.padding import PKCS7
6
+
7
+ from elizaos import AgentRuntime, Character
8
+ from elizaos.settings import (
9
+ _derive_key,
10
+ decrypt_string_value,
11
+ encrypt_string_value,
12
+ get_salt,
13
+ migrate_encrypted_string_value,
14
+ )
15
+
16
+
17
+ class TestSettingsCrypto:
18
+ def test_get_salt_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
19
+ monkeypatch.delenv("SECRET_SALT", raising=False)
20
+ assert get_salt() == "secretsalt"
21
+
22
+ def test_encrypt_decrypt_roundtrip(self) -> None:
23
+ salt = "test-salt-value"
24
+ plaintext = "sensitive-data"
25
+
26
+ encrypted = encrypt_string_value(plaintext, salt)
27
+ assert isinstance(encrypted, str)
28
+ assert encrypted != plaintext
29
+ assert ":" in encrypted
30
+
31
+ decrypted = decrypt_string_value(encrypted, salt)
32
+ assert decrypted == plaintext
33
+
34
+ def test_encrypt_is_idempotent_for_encrypted_values(self) -> None:
35
+ salt = "test-salt-value"
36
+ plaintext = "hello"
37
+ encrypted = encrypt_string_value(plaintext, salt)
38
+ assert isinstance(encrypted, str)
39
+ encrypted2 = encrypt_string_value(encrypted, salt)
40
+ assert encrypted2 == encrypted
41
+
42
+ def test_decrypt_non_encrypted_returns_original(self) -> None:
43
+ salt = "test-salt-value"
44
+ assert decrypt_string_value("not-encrypted", salt) == "not-encrypted"
45
+
46
+ def test_decrypt_legacy_v1_aes_cbc_value(self) -> None:
47
+ salt = "test-salt-value"
48
+ plaintext = "legacy-secret"
49
+
50
+ key = _derive_key(salt)
51
+ iv = secrets.token_bytes(16)
52
+
53
+ padder = PKCS7(128).padder()
54
+ padded = padder.update(plaintext.encode("utf-8")) + padder.finalize()
55
+
56
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
57
+ encryptor = cipher.encryptor()
58
+ ciphertext = encryptor.update(padded) + encryptor.finalize()
59
+
60
+ legacy = f"{iv.hex()}:{ciphertext.hex()}"
61
+ assert decrypt_string_value(legacy, salt) == plaintext
62
+
63
+ def test_migrate_legacy_v1_to_v2(self) -> None:
64
+ salt = "test-salt-value"
65
+ plaintext = "legacy-migrate"
66
+
67
+ key = _derive_key(salt)
68
+ iv = secrets.token_bytes(16)
69
+ padder = PKCS7(128).padder()
70
+ padded = padder.update(plaintext.encode("utf-8")) + padder.finalize()
71
+ cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
72
+ encryptor = cipher.encryptor()
73
+ ciphertext = encryptor.update(padded) + encryptor.finalize()
74
+ legacy = f"{iv.hex()}:{ciphertext.hex()}"
75
+
76
+ migrated = migrate_encrypted_string_value(legacy, salt)
77
+ assert isinstance(migrated, str)
78
+ assert migrated.startswith("v2:")
79
+ assert decrypt_string_value(migrated, salt) == plaintext
80
+
81
+ @pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
82
+ def test_runtime_get_setting_decrypts_secret_strings(
83
+ self, monkeypatch: pytest.MonkeyPatch
84
+ ) -> None:
85
+ salt = "test-salt-value"
86
+ monkeypatch.setenv("SECRET_SALT", salt)
87
+
88
+ encrypted_api_key = encrypt_string_value("super-secret", salt)
89
+ assert isinstance(encrypted_api_key, str)
90
+
91
+ character = Character(
92
+ name="TestAgent",
93
+ bio=["Test"],
94
+ secrets={"API_KEY": encrypted_api_key},
95
+ )
96
+ runtime = AgentRuntime(character=character)
97
+ assert runtime.get_setting("API_KEY") == "super-secret"
98
+
99
+ @pytest.mark.skip(reason="Runtime get_setting from secrets not yet implemented")
100
+ def test_runtime_get_setting_coerces_true_false_strings(
101
+ self, monkeypatch: pytest.MonkeyPatch
102
+ ) -> None:
103
+ salt = "test-salt-value"
104
+ monkeypatch.setenv("SECRET_SALT", salt)
105
+
106
+ encrypted_true = encrypt_string_value("true", salt)
107
+ encrypted_false = encrypt_string_value("false", salt)
108
+ assert isinstance(encrypted_true, str)
109
+ assert isinstance(encrypted_false, str)
110
+
111
+ character = Character(
112
+ name="TestAgent",
113
+ bio=["Test"],
114
+ secrets={"FLAG_TRUE": encrypted_true, "FLAG_FALSE": encrypted_false},
115
+ )
116
+ runtime = AgentRuntime(character=character)
117
+ assert runtime.get_setting("FLAG_TRUE") is True
118
+ assert runtime.get_setting("FLAG_FALSE") is False