@elizaos/python 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +239 -0
- package/elizaos/__init__.py +280 -0
- package/elizaos/action_docs.py +149 -0
- package/elizaos/advanced_capabilities/__init__.py +85 -0
- package/elizaos/advanced_capabilities/actions/__init__.py +54 -0
- package/elizaos/advanced_capabilities/actions/add_contact.py +139 -0
- package/elizaos/advanced_capabilities/actions/follow_room.py +151 -0
- package/elizaos/advanced_capabilities/actions/image_generation.py +148 -0
- package/elizaos/advanced_capabilities/actions/mute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/remove_contact.py +145 -0
- package/elizaos/advanced_capabilities/actions/roles.py +207 -0
- package/elizaos/advanced_capabilities/actions/schedule_follow_up.py +154 -0
- package/elizaos/advanced_capabilities/actions/search_contacts.py +145 -0
- package/elizaos/advanced_capabilities/actions/send_message.py +187 -0
- package/elizaos/advanced_capabilities/actions/settings.py +151 -0
- package/elizaos/advanced_capabilities/actions/unfollow_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/unmute_room.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_contact.py +164 -0
- package/elizaos/advanced_capabilities/actions/update_entity.py +161 -0
- package/elizaos/advanced_capabilities/evaluators/__init__.py +18 -0
- package/elizaos/advanced_capabilities/evaluators/reflection.py +134 -0
- package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +203 -0
- package/elizaos/advanced_capabilities/providers/__init__.py +36 -0
- package/elizaos/advanced_capabilities/providers/agent_settings.py +60 -0
- package/elizaos/advanced_capabilities/providers/contacts.py +77 -0
- package/elizaos/advanced_capabilities/providers/facts.py +82 -0
- package/elizaos/advanced_capabilities/providers/follow_ups.py +113 -0
- package/elizaos/advanced_capabilities/providers/knowledge.py +83 -0
- package/elizaos/advanced_capabilities/providers/relationships.py +112 -0
- package/elizaos/advanced_capabilities/providers/roles.py +97 -0
- package/elizaos/advanced_capabilities/providers/settings.py +51 -0
- package/elizaos/advanced_capabilities/services/__init__.py +18 -0
- package/elizaos/advanced_capabilities/services/follow_up.py +138 -0
- package/elizaos/advanced_capabilities/services/rolodex.py +244 -0
- package/elizaos/advanced_memory/__init__.py +3 -0
- package/elizaos/advanced_memory/evaluators.py +97 -0
- package/elizaos/advanced_memory/memory_service.py +556 -0
- package/elizaos/advanced_memory/plugin.py +30 -0
- package/elizaos/advanced_memory/prompts.py +12 -0
- package/elizaos/advanced_memory/providers.py +90 -0
- package/elizaos/advanced_memory/types.py +65 -0
- package/elizaos/advanced_planning/__init__.py +10 -0
- package/elizaos/advanced_planning/actions.py +145 -0
- package/elizaos/advanced_planning/message_classifier.py +127 -0
- package/elizaos/advanced_planning/planning_service.py +712 -0
- package/elizaos/advanced_planning/plugin.py +40 -0
- package/elizaos/advanced_planning/prompts.py +4 -0
- package/elizaos/basic_capabilities/__init__.py +66 -0
- package/elizaos/basic_capabilities/actions/__init__.py +24 -0
- package/elizaos/basic_capabilities/actions/choice.py +140 -0
- package/elizaos/basic_capabilities/actions/ignore.py +66 -0
- package/elizaos/basic_capabilities/actions/none.py +56 -0
- package/elizaos/basic_capabilities/actions/reply.py +120 -0
- package/elizaos/basic_capabilities/providers/__init__.py +54 -0
- package/elizaos/basic_capabilities/providers/action_state.py +113 -0
- package/elizaos/basic_capabilities/providers/actions.py +263 -0
- package/elizaos/basic_capabilities/providers/attachments.py +76 -0
- package/elizaos/basic_capabilities/providers/capabilities.py +62 -0
- package/elizaos/basic_capabilities/providers/character.py +113 -0
- package/elizaos/basic_capabilities/providers/choice.py +73 -0
- package/elizaos/basic_capabilities/providers/context_bench.py +44 -0
- package/elizaos/basic_capabilities/providers/current_time.py +58 -0
- package/elizaos/basic_capabilities/providers/entities.py +99 -0
- package/elizaos/basic_capabilities/providers/evaluators.py +54 -0
- package/elizaos/basic_capabilities/providers/providers_list.py +55 -0
- package/elizaos/basic_capabilities/providers/recent_messages.py +85 -0
- package/elizaos/basic_capabilities/providers/time.py +45 -0
- package/elizaos/basic_capabilities/providers/world.py +93 -0
- package/elizaos/basic_capabilities/services/__init__.py +18 -0
- package/elizaos/basic_capabilities/services/embedding.py +122 -0
- package/elizaos/basic_capabilities/services/task.py +178 -0
- package/elizaos/bootstrap/__init__.py +12 -0
- package/elizaos/bootstrap/actions/__init__.py +68 -0
- package/elizaos/bootstrap/actions/add_contact.py +149 -0
- package/elizaos/bootstrap/actions/choice.py +147 -0
- package/elizaos/bootstrap/actions/follow_room.py +151 -0
- package/elizaos/bootstrap/actions/ignore.py +80 -0
- package/elizaos/bootstrap/actions/image_generation.py +135 -0
- package/elizaos/bootstrap/actions/mute_room.py +151 -0
- package/elizaos/bootstrap/actions/none.py +71 -0
- package/elizaos/bootstrap/actions/remove_contact.py +159 -0
- package/elizaos/bootstrap/actions/reply.py +140 -0
- package/elizaos/bootstrap/actions/roles.py +193 -0
- package/elizaos/bootstrap/actions/schedule_follow_up.py +164 -0
- package/elizaos/bootstrap/actions/search_contacts.py +159 -0
- package/elizaos/bootstrap/actions/send_message.py +173 -0
- package/elizaos/bootstrap/actions/settings.py +165 -0
- package/elizaos/bootstrap/actions/unfollow_room.py +151 -0
- package/elizaos/bootstrap/actions/unmute_room.py +151 -0
- package/elizaos/bootstrap/actions/update_contact.py +178 -0
- package/elizaos/bootstrap/actions/update_entity.py +175 -0
- package/elizaos/bootstrap/autonomy/__init__.py +18 -0
- package/elizaos/bootstrap/autonomy/action.py +197 -0
- package/elizaos/bootstrap/autonomy/providers.py +165 -0
- package/elizaos/bootstrap/autonomy/routes.py +171 -0
- package/elizaos/bootstrap/autonomy/service.py +562 -0
- package/elizaos/bootstrap/autonomy/types.py +18 -0
- package/elizaos/bootstrap/evaluators/__init__.py +19 -0
- package/elizaos/bootstrap/evaluators/reflection.py +118 -0
- package/elizaos/bootstrap/evaluators/relationship_extraction.py +192 -0
- package/elizaos/bootstrap/plugin.py +140 -0
- package/elizaos/bootstrap/providers/__init__.py +80 -0
- package/elizaos/bootstrap/providers/action_state.py +71 -0
- package/elizaos/bootstrap/providers/actions.py +256 -0
- package/elizaos/bootstrap/providers/agent_settings.py +63 -0
- package/elizaos/bootstrap/providers/attachments.py +76 -0
- package/elizaos/bootstrap/providers/capabilities.py +66 -0
- package/elizaos/bootstrap/providers/character.py +128 -0
- package/elizaos/bootstrap/providers/choice.py +77 -0
- package/elizaos/bootstrap/providers/contacts.py +78 -0
- package/elizaos/bootstrap/providers/context_bench.py +49 -0
- package/elizaos/bootstrap/providers/current_time.py +56 -0
- package/elizaos/bootstrap/providers/entities.py +99 -0
- package/elizaos/bootstrap/providers/evaluators.py +58 -0
- package/elizaos/bootstrap/providers/facts.py +86 -0
- package/elizaos/bootstrap/providers/follow_ups.py +116 -0
- package/elizaos/bootstrap/providers/knowledge.py +73 -0
- package/elizaos/bootstrap/providers/providers_list.py +59 -0
- package/elizaos/bootstrap/providers/recent_messages.py +85 -0
- package/elizaos/bootstrap/providers/relationships.py +106 -0
- package/elizaos/bootstrap/providers/roles.py +95 -0
- package/elizaos/bootstrap/providers/settings.py +55 -0
- package/elizaos/bootstrap/providers/time.py +45 -0
- package/elizaos/bootstrap/providers/world.py +97 -0
- package/elizaos/bootstrap/services/__init__.py +26 -0
- package/elizaos/bootstrap/services/embedding.py +122 -0
- package/elizaos/bootstrap/services/follow_up.py +138 -0
- package/elizaos/bootstrap/services/rolodex.py +244 -0
- package/elizaos/bootstrap/services/task.py +585 -0
- package/elizaos/bootstrap/types.py +54 -0
- package/elizaos/bootstrap/utils/__init__.py +7 -0
- package/elizaos/bootstrap/utils/xml.py +69 -0
- package/elizaos/character.py +149 -0
- package/elizaos/logger.py +179 -0
- package/elizaos/media/__init__.py +45 -0
- package/elizaos/media/mime.py +315 -0
- package/elizaos/media/search.py +161 -0
- package/elizaos/media/tests/__init__.py +1 -0
- package/elizaos/media/tests/test_mime.py +117 -0
- package/elizaos/media/tests/test_search.py +156 -0
- package/elizaos/plugin.py +191 -0
- package/elizaos/prompts.py +1071 -0
- package/elizaos/py.typed +0 -0
- package/elizaos/runtime.py +2572 -0
- package/elizaos/services/__init__.py +49 -0
- package/elizaos/services/hook_service.py +511 -0
- package/elizaos/services/message_service.py +1248 -0
- package/elizaos/settings.py +182 -0
- package/elizaos/streaming_context.py +159 -0
- package/elizaos/trajectory_context.py +18 -0
- package/elizaos/types/__init__.py +512 -0
- package/elizaos/types/agent.py +31 -0
- package/elizaos/types/components.py +208 -0
- package/elizaos/types/database.py +64 -0
- package/elizaos/types/environment.py +46 -0
- package/elizaos/types/events.py +47 -0
- package/elizaos/types/memory.py +45 -0
- package/elizaos/types/model.py +393 -0
- package/elizaos/types/plugin.py +188 -0
- package/elizaos/types/primitives.py +100 -0
- package/elizaos/types/runtime.py +460 -0
- package/elizaos/types/service.py +113 -0
- package/elizaos/types/service_interfaces.py +244 -0
- package/elizaos/types/state.py +188 -0
- package/elizaos/types/task.py +29 -0
- package/elizaos/utils/__init__.py +108 -0
- package/elizaos/utils/spec_examples.py +48 -0
- package/elizaos/utils/streaming.py +426 -0
- package/elizaos_atropos_shared/__init__.py +1 -0
- package/elizaos_atropos_shared/canonical_eliza.py +282 -0
- package/package.json +19 -0
- package/pyproject.toml +143 -0
- package/requirements-dev.in +11 -0
- package/requirements-dev.lock +134 -0
- package/requirements.in +9 -0
- package/requirements.lock +64 -0
- package/tests/__init__.py +0 -0
- package/tests/test_action_parameters.py +154 -0
- package/tests/test_actions_provider_examples.py +39 -0
- package/tests/test_advanced_memory_behavior.py +96 -0
- package/tests/test_advanced_memory_flag.py +30 -0
- package/tests/test_advanced_planning_behavior.py +225 -0
- package/tests/test_advanced_planning_flag.py +26 -0
- package/tests/test_autonomy.py +445 -0
- package/tests/test_bootstrap_initialize.py +37 -0
- package/tests/test_character.py +163 -0
- package/tests/test_character_provider.py +231 -0
- package/tests/test_dynamic_prompt_exec.py +561 -0
- package/tests/test_logger_redaction.py +43 -0
- package/tests/test_plugin.py +117 -0
- package/tests/test_runtime.py +422 -0
- package/tests/test_salt_production_enforcement.py +22 -0
- package/tests/test_settings_crypto.py +118 -0
- package/tests/test_streaming.py +295 -0
- package/tests/test_types.py +221 -0
- package/tests/test_uuid_parity.py +46 -0
|
@@ -0,0 +1,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]"
|