@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,561 @@
1
+ """Tests for dynamic_prompt_exec_from_state and related functionality.
2
+
3
+ This test module validates the dynamic execution engine across:
4
+ 1. SchemaRow and RetryBackoffConfig types
5
+ 2. XML parsing and validation code handling
6
+ 3. The full dynamic_prompt_exec_from_state flow
7
+ 4. Parity with TypeScript and Rust implementations
8
+ """
9
+
10
+ from unittest.mock import MagicMock
11
+
12
+ import pytest
13
+
14
+ from elizaos.runtime import AgentRuntime, DynamicPromptOptions
15
+ from elizaos.types import Character
16
+ from elizaos.types.state import (
17
+ RetryBackoffConfig,
18
+ SchemaRow,
19
+ StreamEvent,
20
+ StreamEventType,
21
+ )
22
+
23
+ # ============================================================================
24
+ # SchemaRow Tests
25
+ # ============================================================================
26
+
27
+
28
+ class TestSchemaRow:
29
+ """Tests for SchemaRow dataclass."""
30
+
31
+ def test_basic_creation(self) -> None:
32
+ """Test basic SchemaRow creation with required fields."""
33
+ row = SchemaRow(field="thought", description="Your reasoning")
34
+ assert row.field == "thought"
35
+ assert row.description == "Your reasoning"
36
+ assert row.required is False
37
+ assert row.validate_field is None
38
+ assert row.stream_field is None
39
+
40
+ def test_required_field(self) -> None:
41
+ """Test SchemaRow with required=True."""
42
+ row = SchemaRow(field="text", description="Response", required=True)
43
+ assert row.required is True
44
+
45
+ def test_validate_field_option(self) -> None:
46
+ """Test SchemaRow with validate_field option."""
47
+ row_with_validation = SchemaRow(field="text", description="Response", validate_field=True)
48
+ assert row_with_validation.validate_field is True
49
+
50
+ row_without_validation = SchemaRow(
51
+ field="thought", description="Reasoning", validate_field=False
52
+ )
53
+ assert row_without_validation.validate_field is False
54
+
55
+ def test_stream_field_option(self) -> None:
56
+ """Test SchemaRow with stream_field option."""
57
+ row_streamed = SchemaRow(field="text", description="Response", stream_field=True)
58
+ assert row_streamed.stream_field is True
59
+
60
+ row_not_streamed = SchemaRow(field="thought", description="Reasoning", stream_field=False)
61
+ assert row_not_streamed.stream_field is False
62
+
63
+
64
+ # ============================================================================
65
+ # RetryBackoffConfig Tests
66
+ # ============================================================================
67
+
68
+
69
+ class TestRetryBackoffConfig:
70
+ """Tests for RetryBackoffConfig dataclass."""
71
+
72
+ def test_default_values(self) -> None:
73
+ """Test default configuration values."""
74
+ config = RetryBackoffConfig()
75
+ assert config.initial_ms == 1000
76
+ assert config.multiplier == 2.0
77
+ assert config.max_ms == 30000
78
+
79
+ def test_custom_values(self) -> None:
80
+ """Test custom configuration values."""
81
+ config = RetryBackoffConfig(initial_ms=500, multiplier=1.5, max_ms=10000)
82
+ assert config.initial_ms == 500
83
+ assert config.multiplier == 1.5
84
+ assert config.max_ms == 10000
85
+
86
+ def test_delay_for_retry_calculation(self) -> None:
87
+ """Test exponential backoff delay calculation."""
88
+ config = RetryBackoffConfig(initial_ms=1000, multiplier=2.0, max_ms=30000)
89
+
90
+ # First retry: 1000 * 2^0 = 1000ms
91
+ assert config.delay_for_retry(1) == 1000
92
+
93
+ # Second retry: 1000 * 2^1 = 2000ms
94
+ assert config.delay_for_retry(2) == 2000
95
+
96
+ # Third retry: 1000 * 2^2 = 4000ms
97
+ assert config.delay_for_retry(3) == 4000
98
+
99
+ # Fourth retry: 1000 * 2^3 = 8000ms
100
+ assert config.delay_for_retry(4) == 8000
101
+
102
+ def test_delay_capped_at_max(self) -> None:
103
+ """Test that delay is capped at max_ms."""
104
+ config = RetryBackoffConfig(initial_ms=1000, multiplier=2.0, max_ms=5000)
105
+
106
+ # Fifth retry would be 1000 * 2^4 = 16000ms, but capped at 5000ms
107
+ assert config.delay_for_retry(5) == 5000
108
+
109
+
110
+ # ============================================================================
111
+ # StreamEvent Tests
112
+ # ============================================================================
113
+
114
+
115
+ class TestStreamEvent:
116
+ """Tests for StreamEvent and StreamEventType."""
117
+
118
+ def test_event_types(self) -> None:
119
+ """Test all StreamEventType values."""
120
+ assert StreamEventType.CHUNK.value == "chunk"
121
+ assert StreamEventType.FIELD_VALIDATED.value == "field_validated"
122
+ assert StreamEventType.RETRY_START.value == "retry_start"
123
+ assert StreamEventType.ERROR.value == "error"
124
+ assert StreamEventType.COMPLETE.value == "complete"
125
+
126
+ def test_chunk_event_factory(self) -> None:
127
+ """Test StreamEvent.chunk_event factory method."""
128
+ event = StreamEvent.chunk_event("text", "Hello world")
129
+ assert event.event_type == StreamEventType.CHUNK
130
+ assert event.field == "text"
131
+ assert event.chunk == "Hello world"
132
+ assert event.timestamp > 0
133
+
134
+ def test_field_validated_event_factory(self) -> None:
135
+ """Test StreamEvent.field_validated_event factory method."""
136
+ event = StreamEvent.field_validated_event("text")
137
+ assert event.event_type == StreamEventType.FIELD_VALIDATED
138
+ assert event.field == "text"
139
+ assert event.timestamp > 0
140
+
141
+ def test_retry_start_event_factory(self) -> None:
142
+ """Test StreamEvent.retry_start_event factory method."""
143
+ event = StreamEvent.retry_start_event(2)
144
+ assert event.event_type == StreamEventType.RETRY_START
145
+ assert event.retry_count == 2
146
+ assert event.timestamp > 0
147
+
148
+ def test_error_event_factory(self) -> None:
149
+ """Test StreamEvent.error_event factory method."""
150
+ event = StreamEvent.error_event("Something went wrong")
151
+ assert event.event_type == StreamEventType.ERROR
152
+ assert event.error == "Something went wrong"
153
+ assert event.timestamp > 0
154
+
155
+ def test_complete_event_factory(self) -> None:
156
+ """Test StreamEvent.complete_event factory method."""
157
+ event = StreamEvent.complete_event()
158
+ assert event.event_type == StreamEventType.COMPLETE
159
+ assert event.timestamp > 0
160
+
161
+
162
+ # ============================================================================
163
+ # XML Parsing Tests
164
+ # ============================================================================
165
+
166
+
167
+ class TestXMLParsing:
168
+ """Tests for XML parsing in dynamic prompt execution."""
169
+
170
+ @pytest.fixture
171
+ def runtime(self) -> AgentRuntime:
172
+ """Create a test runtime."""
173
+ character = Character(name="TestAgent", bio="Test agent")
174
+ return AgentRuntime(character=character)
175
+
176
+ def test_parse_simple_xml(self, runtime: AgentRuntime) -> None:
177
+ """Test parsing simple XML response."""
178
+ xml = """<response>
179
+ <thought>I should respond politely</thought>
180
+ <text>Hello! How can I help you?</text>
181
+ <actions>REPLY</actions>
182
+ </response>"""
183
+
184
+ result = runtime._parse_xml_to_dict(xml)
185
+
186
+ assert result is not None
187
+ assert result.get("thought") == "I should respond politely"
188
+ assert result.get("text") == "Hello! How can I help you?"
189
+ assert result.get("actions") == "REPLY"
190
+
191
+ def test_parse_xml_with_validation_codes(self, runtime: AgentRuntime) -> None:
192
+ """Test parsing XML with validation code fields (with underscores)."""
193
+ xml = """<response>
194
+ <code_text_start>abc12345</code_text_start>
195
+ <text>Hello world</text>
196
+ <code_text_end>abc12345</code_text_end>
197
+ </response>"""
198
+
199
+ result = runtime._parse_xml_to_dict(xml)
200
+
201
+ assert result is not None
202
+ assert result.get("code_text_start") == "abc12345"
203
+ assert result.get("text") == "Hello world"
204
+ assert result.get("code_text_end") == "abc12345"
205
+
206
+ def test_parse_xml_with_checkpoint_codes(self, runtime: AgentRuntime) -> None:
207
+ """Test parsing XML with checkpoint validation codes."""
208
+ xml = """<response>
209
+ <one_initial_code>uuid-1234</one_initial_code>
210
+ <one_middle_code>uuid-5678</one_middle_code>
211
+ <one_end_code>uuid-9abc</one_end_code>
212
+ <text>Response text</text>
213
+ </response>"""
214
+
215
+ result = runtime._parse_xml_to_dict(xml)
216
+
217
+ assert result is not None
218
+ assert result.get("one_initial_code") == "uuid-1234"
219
+ assert result.get("one_middle_code") == "uuid-5678"
220
+ assert result.get("one_end_code") == "uuid-9abc"
221
+ assert result.get("text") == "Response text"
222
+
223
+ def test_parse_nested_xml(self, runtime: AgentRuntime) -> None:
224
+ """Test parsing nested XML structures."""
225
+ xml = """<response>
226
+ <parameters>
227
+ <name>test</name>
228
+ <value>123</value>
229
+ </parameters>
230
+ </response>"""
231
+
232
+ result = runtime._parse_xml_to_dict(xml)
233
+
234
+ assert result is not None
235
+ assert isinstance(result.get("parameters"), dict)
236
+ params = result["parameters"]
237
+ assert params.get("name") == "test"
238
+ assert params.get("value") == "123"
239
+
240
+ def test_parse_xml_with_think_block_removed(self, runtime: AgentRuntime) -> None:
241
+ """Test that think blocks would be removed before parsing."""
242
+ # Note: The _parse_xml_to_dict doesn't remove think blocks
243
+ # That's done in dynamic_prompt_exec_from_state before calling parse
244
+ xml = """<response>
245
+ <text>Clean response</text>
246
+ </response>"""
247
+
248
+ result = runtime._parse_xml_to_dict(xml)
249
+
250
+ assert result is not None
251
+ assert result.get("text") == "Clean response"
252
+
253
+ def test_parse_malformed_xml_fallback(self, runtime: AgentRuntime) -> None:
254
+ """Test fallback regex parsing for malformed XML."""
255
+ # Missing proper nesting but has tags
256
+ xml = """<thought>thinking here</thought>
257
+ <text>response text</text>"""
258
+
259
+ result = runtime._parse_xml_to_dict(xml)
260
+
261
+ assert result is not None
262
+ assert result.get("thought") == "thinking here"
263
+ assert result.get("text") == "response text"
264
+
265
+
266
+ # ============================================================================
267
+ # DynamicPromptOptions Tests
268
+ # ============================================================================
269
+
270
+
271
+ class TestDynamicPromptOptions:
272
+ """Tests for DynamicPromptOptions dataclass."""
273
+
274
+ def test_default_options(self) -> None:
275
+ """Test default options values."""
276
+ options = DynamicPromptOptions()
277
+ assert options.model_size is None
278
+ assert options.model is None
279
+ assert options.force_format is None
280
+ assert options.required_fields is None
281
+ assert options.context_check_level is None
282
+ assert options.max_retries is None
283
+ assert options.retry_backoff is None
284
+
285
+ def test_custom_options(self) -> None:
286
+ """Test custom options values."""
287
+ backoff = RetryBackoffConfig(initial_ms=500)
288
+ options = DynamicPromptOptions(
289
+ model_size="small",
290
+ force_format="xml",
291
+ required_fields=["text", "actions"],
292
+ context_check_level=1,
293
+ max_retries=3,
294
+ retry_backoff=backoff,
295
+ )
296
+
297
+ assert options.model_size == "small"
298
+ assert options.force_format == "xml"
299
+ assert options.required_fields == ["text", "actions"]
300
+ assert options.context_check_level == 1
301
+ assert options.max_retries == 3
302
+ assert options.retry_backoff.initial_ms == 500
303
+
304
+
305
+ # ============================================================================
306
+ # dynamic_prompt_exec_from_state Integration Tests
307
+ # ============================================================================
308
+
309
+
310
+ class TestDynamicPromptExecFromState:
311
+ """Integration tests for dynamic_prompt_exec_from_state."""
312
+
313
+ @pytest.fixture
314
+ def character(self) -> Character:
315
+ """Create a test character."""
316
+ return Character(name="TestAgent", bio="A test agent")
317
+
318
+ @pytest.fixture
319
+ def runtime(self, character: Character) -> AgentRuntime:
320
+ """Create a test runtime with a mock model handler."""
321
+ return AgentRuntime(character=character)
322
+
323
+ @pytest.mark.asyncio
324
+ async def test_basic_execution_with_mock_model(self, runtime: AgentRuntime) -> None:
325
+ """Test basic execution with a mocked model response."""
326
+
327
+ # Register a mock model handler that returns valid XML
328
+ async def mock_model_handler(rt: AgentRuntime, params: dict[str, object]) -> str:
329
+ return """<response>
330
+ <thought>User wants help</thought>
331
+ <text>I can help with that!</text>
332
+ <actions>REPLY</actions>
333
+ </response>"""
334
+
335
+ runtime.register_model("TEXT_LARGE", mock_model_handler, "mock")
336
+
337
+ # Create a mock state
338
+ from elizaos.types.state import State
339
+
340
+ # Use a simple dict-like mock for state
341
+ state = MagicMock(spec=State)
342
+ state.values = {}
343
+
344
+ schema = [
345
+ SchemaRow("thought", "Your reasoning"),
346
+ SchemaRow("text", "Response to user", required=True),
347
+ SchemaRow("actions", "Actions to take"),
348
+ ]
349
+
350
+ result = await runtime.dynamic_prompt_exec_from_state(
351
+ state=state,
352
+ prompt="Test prompt",
353
+ schema=schema,
354
+ options=DynamicPromptOptions(context_check_level=0), # No validation
355
+ )
356
+
357
+ assert result is not None
358
+ assert "thought" in result
359
+ assert "text" in result
360
+ assert "actions" in result
361
+
362
+ @pytest.mark.asyncio
363
+ async def test_validation_level_settings(self, runtime: AgentRuntime) -> None:
364
+ """Test that VALIDATION_LEVEL setting affects behavior."""
365
+ # Test trusted/fast mode
366
+ runtime.set_setting("VALIDATION_LEVEL", "trusted")
367
+
368
+ # The implementation checks this setting and adjusts context_check_level
369
+ # We can verify the setting is retrieved correctly
370
+ assert runtime.get_setting("VALIDATION_LEVEL") == "trusted"
371
+
372
+ @pytest.mark.asyncio
373
+ async def test_required_fields_validation(self, runtime: AgentRuntime) -> None:
374
+ """Test that required fields are validated."""
375
+
376
+ # Register a mock model handler that returns incomplete XML
377
+ async def mock_model_handler(rt: AgentRuntime, params: dict[str, object]) -> str:
378
+ return """<response>
379
+ <thought>User wants help</thought>
380
+ <text></text>
381
+ </response>"""
382
+
383
+ runtime.register_model("TEXT_LARGE", mock_model_handler, "mock")
384
+
385
+ state = MagicMock()
386
+ state.values = {}
387
+
388
+ schema = [
389
+ SchemaRow("thought", "Your reasoning"),
390
+ SchemaRow("text", "Response to user", required=True),
391
+ ]
392
+
393
+ # With max_retries=0, should fail immediately on missing required field
394
+ result = await runtime.dynamic_prompt_exec_from_state(
395
+ state=state,
396
+ prompt="Test prompt",
397
+ schema=schema,
398
+ options=DynamicPromptOptions(
399
+ context_check_level=0, # No validation codes
400
+ max_retries=0,
401
+ required_fields=["text"], # text is required but empty
402
+ ),
403
+ )
404
+
405
+ # Should return None because text is empty and required
406
+ assert result is None
407
+
408
+ @pytest.mark.asyncio
409
+ async def test_callable_prompt(self, runtime: AgentRuntime) -> None:
410
+ """Test that callable prompts work correctly."""
411
+
412
+ async def mock_model_handler(rt: AgentRuntime, params: dict[str, object]) -> str:
413
+ prompt = params.get("prompt", "")
414
+ # Verify the callable was executed with state
415
+ assert "Hello Alice" in str(prompt)
416
+ return """<response>
417
+ <text>Hello back!</text>
418
+ </response>"""
419
+
420
+ runtime.register_model("TEXT_LARGE", mock_model_handler, "mock")
421
+
422
+ state = MagicMock()
423
+ state.values = {"name": "Alice"}
424
+
425
+ def prompt_callable(ctx: dict) -> str:
426
+ return "Hello {{name}}"
427
+
428
+ schema = [SchemaRow("text", "Response")]
429
+
430
+ result = await runtime.dynamic_prompt_exec_from_state(
431
+ state=state,
432
+ prompt=prompt_callable,
433
+ schema=schema,
434
+ options=DynamicPromptOptions(context_check_level=0),
435
+ )
436
+
437
+ assert result is not None
438
+
439
+ @pytest.mark.asyncio
440
+ async def test_json_format(self, runtime: AgentRuntime) -> None:
441
+ """Test JSON format output parsing."""
442
+
443
+ async def mock_model_handler(rt: AgentRuntime, params: dict[str, object]) -> str:
444
+ return '{"thought": "reasoning", "text": "response"}'
445
+
446
+ runtime.register_model("TEXT_LARGE", mock_model_handler, "mock")
447
+
448
+ state = MagicMock()
449
+ state.values = {}
450
+
451
+ schema = [
452
+ SchemaRow("thought", "Your reasoning"),
453
+ SchemaRow("text", "Response"),
454
+ ]
455
+
456
+ result = await runtime.dynamic_prompt_exec_from_state(
457
+ state=state,
458
+ prompt="Test prompt",
459
+ schema=schema,
460
+ options=DynamicPromptOptions(context_check_level=0, force_format="json"),
461
+ )
462
+
463
+ assert result is not None
464
+ assert result.get("thought") == "reasoning"
465
+ assert result.get("text") == "response"
466
+
467
+ @pytest.mark.asyncio
468
+ async def test_retry_on_failure(self, runtime: AgentRuntime) -> None:
469
+ """Test retry behavior on validation failure."""
470
+ call_count = [0]
471
+
472
+ async def mock_model_handler(rt: AgentRuntime, params: dict[str, object]) -> str:
473
+ call_count[0] += 1
474
+ if call_count[0] < 2:
475
+ # First call returns invalid response
476
+ return "<response><text></text></response>"
477
+ # Second call returns valid response
478
+ return "<response><text>Valid response</text></response>"
479
+
480
+ runtime.register_model("TEXT_LARGE", mock_model_handler, "mock")
481
+
482
+ state = MagicMock()
483
+ state.values = {}
484
+
485
+ schema = [SchemaRow("text", "Response", required=True)]
486
+
487
+ result = await runtime.dynamic_prompt_exec_from_state(
488
+ state=state,
489
+ prompt="Test prompt",
490
+ schema=schema,
491
+ options=DynamicPromptOptions(
492
+ context_check_level=0, max_retries=2, required_fields=["text"]
493
+ ),
494
+ )
495
+
496
+ assert result is not None
497
+ assert result.get("text") == "Valid response"
498
+ assert call_count[0] == 2
499
+
500
+
501
+ # ============================================================================
502
+ # Parity Tests (ensure consistency with TypeScript/Rust)
503
+ # ============================================================================
504
+
505
+
506
+ class TestCrossLanguageParity:
507
+ """Tests to ensure parity with TypeScript and Rust implementations."""
508
+
509
+ def test_schema_row_fields_match_typescript(self) -> None:
510
+ """Verify SchemaRow has same fields as TypeScript SchemaRow type."""
511
+ # TypeScript has: field, description, required?, validateField?, streamField?
512
+ row = SchemaRow(
513
+ field="test", description="desc", required=True, validate_field=True, stream_field=False
514
+ )
515
+
516
+ # Python uses snake_case, TypeScript uses camelCase
517
+ # But semantic equivalence should hold
518
+ assert hasattr(row, "field")
519
+ assert hasattr(row, "description")
520
+ assert hasattr(row, "required")
521
+ assert hasattr(row, "validate_field") # = validateField in TS
522
+ assert hasattr(row, "stream_field") # = streamField in TS
523
+
524
+ def test_retry_backoff_config_matches_typescript(self) -> None:
525
+ """Verify RetryBackoffConfig has same fields as TypeScript."""
526
+ config = RetryBackoffConfig(initial_ms=1000, multiplier=2.0, max_ms=30000)
527
+
528
+ # TypeScript has: initialMs, multiplier, maxMs
529
+ assert hasattr(config, "initial_ms") # = initialMs in TS
530
+ assert hasattr(config, "multiplier")
531
+ assert hasattr(config, "max_ms") # = maxMs in TS
532
+
533
+ def test_stream_event_types_match_typescript(self) -> None:
534
+ """Verify StreamEventType enum values match TypeScript."""
535
+ # TypeScript StreamEventType values:
536
+ # "chunk" | "field_validated" | "retry_start" | "error" | "complete"
537
+
538
+ expected_values = {"chunk", "field_validated", "retry_start", "error", "complete"}
539
+ actual_values = {e.value for e in StreamEventType}
540
+
541
+ assert actual_values == expected_values
542
+
543
+ def test_validation_levels_semantics(self) -> None:
544
+ """Verify validation level semantics match across languages."""
545
+ # Level 0: Trusted - no validation codes
546
+ # Level 1: Progressive - per-field validation codes
547
+ # Level 2: Checkpoint - codes at start only
548
+ # Level 3: Full - codes at start and end
549
+
550
+ # These are implicit in the implementation but we can verify
551
+ # the constants/semantics are documented
552
+ levels = {
553
+ 0: "trusted/fast - no validation codes",
554
+ 1: "progressive - per-field validation",
555
+ 2: "checkpoint - first codes",
556
+ 3: "full - first and last codes",
557
+ }
558
+
559
+ # Just verify the levels exist conceptually
560
+ for level in range(4):
561
+ assert level in levels
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ from elizaos.logger import _redaction_processor
6
+
7
+
8
+ def test_redaction_processor_redacts_common_secret_keys(monkeypatch) -> None:
9
+ monkeypatch.setenv("ELIZA_LOG_REDACT", "true")
10
+ monkeypatch.delenv("ELIZA_LOG_REDACT_KEYS", raising=False)
11
+
12
+ event: dict[str, object] = {
13
+ "message": "hello",
14
+ "token": "abc",
15
+ "nested": {"password": "pw", "ok": "yes"},
16
+ "list": [{"apiKey": "k1"}, {"value": "v"}],
17
+ }
18
+
19
+ redacted = _redaction_processor(logging.getLogger("test"), "info", event)
20
+ assert redacted["token"] == "[REDACTED]"
21
+ assert isinstance(redacted["nested"], dict)
22
+ assert redacted["nested"]["password"] == "[REDACTED]"
23
+ assert redacted["nested"]["ok"] == "yes"
24
+ assert isinstance(redacted["list"], list)
25
+ assert redacted["list"][0]["apiKey"] == "[REDACTED]"
26
+
27
+
28
+ def test_redaction_processor_can_be_disabled(monkeypatch) -> None:
29
+ monkeypatch.setenv("ELIZA_LOG_REDACT", "false")
30
+
31
+ event: dict[str, object] = {"token": "abc"}
32
+ out = _redaction_processor(logging.getLogger("test"), "info", event)
33
+ assert out["token"] == "abc"
34
+
35
+
36
+ def test_redaction_processor_supports_custom_keys(monkeypatch) -> None:
37
+ monkeypatch.setenv("ELIZA_LOG_REDACT", "true")
38
+ monkeypatch.setenv("ELIZA_LOG_REDACT_KEYS", "email, phone ")
39
+
40
+ event: dict[str, object] = {"email": "user@example.com", "phone": "555-5555"}
41
+ out = _redaction_processor(logging.getLogger("test"), "info", event)
42
+ assert out["email"] == "[REDACTED]"
43
+ assert out["phone"] == "[REDACTED]"