@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,164 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING
5
+
6
+ from elizaos.generated.spec_helpers import require_action_spec
7
+ from elizaos.types import Action, ActionExample, ActionResult, Content
8
+
9
+ if TYPE_CHECKING:
10
+ from elizaos.types import HandlerCallback, HandlerOptions, IAgentRuntime, Memory, State
11
+
12
+ # Get text content from centralized specs
13
+ _spec = require_action_spec("MUTE_ROOM")
14
+
15
+
16
+ def _convert_spec_examples() -> list[list[ActionExample]]:
17
+ """Convert spec examples to ActionExample format."""
18
+ spec_examples = _spec.get("examples", [])
19
+ if not isinstance(spec_examples, list):
20
+ return []
21
+ result: list[list[ActionExample]] = []
22
+ for example in spec_examples:
23
+ if not isinstance(example, list):
24
+ continue
25
+ row: list[ActionExample] = []
26
+ for msg in example:
27
+ if not isinstance(msg, dict):
28
+ continue
29
+ content = msg.get("content", {})
30
+ text = ""
31
+ actions: list[str] | None = None
32
+ if isinstance(content, dict):
33
+ text_val = content.get("text", "")
34
+ text = str(text_val) if text_val else ""
35
+ actions_val = content.get("actions")
36
+ if isinstance(actions_val, list) and all(isinstance(a, str) for a in actions_val):
37
+ actions = list(actions_val)
38
+ row.append(
39
+ ActionExample(
40
+ name=str(msg.get("name", "")),
41
+ content=Content(text=text, actions=actions),
42
+ )
43
+ )
44
+ if row:
45
+ result.append(row)
46
+ return result
47
+
48
+
49
+ @dataclass
50
+ class MuteRoomAction:
51
+ name: str = _spec["name"]
52
+ similes: list[str] = field(default_factory=lambda: list(_spec.get("similes", [])))
53
+ description: str = _spec["description"]
54
+
55
+ async def validate(
56
+ self, runtime: IAgentRuntime, message: Memory, _state: State | None = None
57
+ ) -> bool:
58
+ room_id = message.room_id
59
+ if not room_id:
60
+ return False
61
+
62
+ room = await runtime.get_room(room_id)
63
+ if room is None:
64
+ return False
65
+
66
+ world_id = room.world_id
67
+ if world_id:
68
+ world = await runtime.get_world(world_id)
69
+ if world and world.metadata:
70
+ muted_rooms = world.metadata.get("mutedRooms", [])
71
+ if str(room_id) in muted_rooms:
72
+ return False
73
+
74
+ return True
75
+
76
+ async def handler(
77
+ self,
78
+ runtime: IAgentRuntime,
79
+ message: Memory,
80
+ state: State | None = None,
81
+ options: HandlerOptions | None = None,
82
+ callback: HandlerCallback | None = None,
83
+ responses: list[Memory] | None = None,
84
+ ) -> ActionResult:
85
+ room_id = message.room_id
86
+ if not room_id:
87
+ return ActionResult(
88
+ text="No room specified to mute",
89
+ values={"success": False, "error": "no_room_id"},
90
+ data={"actionName": "MUTE_ROOM"},
91
+ success=False,
92
+ )
93
+
94
+ room = await runtime.get_room(room_id)
95
+ if room is None:
96
+ return ActionResult(
97
+ text="Room not found",
98
+ values={"success": False, "error": "room_not_found"},
99
+ data={"actionName": "MUTE_ROOM"},
100
+ success=False,
101
+ )
102
+
103
+ room_name = str(room.name) if room.name else "Unknown Room"
104
+
105
+ world_id = room.world_id
106
+ if world_id:
107
+ world = await runtime.get_world(world_id)
108
+ if world and world.metadata:
109
+ muted_rooms = list(world.metadata.get("mutedRooms", []))
110
+ room_id_str = str(room_id)
111
+
112
+ if room_id_str not in muted_rooms:
113
+ muted_rooms.append(room_id_str)
114
+ world.metadata["mutedRooms"] = muted_rooms
115
+ await runtime.update_world(world)
116
+
117
+ await runtime.create_memory(
118
+ content=Content(
119
+ text=f"Muted room: {room_name}",
120
+ actions=["MUTE_ROOM"],
121
+ ),
122
+ room_id=room_id,
123
+ entity_id=runtime.agent_id,
124
+ memory_type="action",
125
+ metadata={"type": "MUTE_ROOM", "roomName": room_name},
126
+ )
127
+
128
+ response_content = Content(
129
+ text=f"I have muted {room_name}. I won't respond to messages there.",
130
+ actions=["MUTE_ROOM"],
131
+ )
132
+
133
+ if callback:
134
+ await callback(response_content)
135
+
136
+ return ActionResult(
137
+ text=f"Muted room: {room_name}",
138
+ values={
139
+ "success": True,
140
+ "muted": True,
141
+ "roomId": str(room_id),
142
+ "roomName": room_name,
143
+ },
144
+ data={
145
+ "actionName": "MUTE_ROOM",
146
+ "roomId": str(room_id),
147
+ "roomName": room_name,
148
+ },
149
+ success=True,
150
+ )
151
+
152
+ @property
153
+ def examples(self) -> list[list[ActionExample]]:
154
+ return _convert_spec_examples()
155
+
156
+
157
+ mute_room_action = Action(
158
+ name=MuteRoomAction.name,
159
+ similes=MuteRoomAction().similes,
160
+ description=MuteRoomAction.description,
161
+ validate=MuteRoomAction().validate,
162
+ handler=MuteRoomAction().handler,
163
+ examples=MuteRoomAction().examples,
164
+ )
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import TYPE_CHECKING
5
+
6
+ from elizaos.bootstrap.utils.xml import parse_key_value_xml
7
+ from elizaos.generated.spec_helpers import require_action_spec
8
+ from elizaos.prompts import REMOVE_CONTACT_TEMPLATE
9
+ from elizaos.types import (
10
+ Action,
11
+ ActionExample,
12
+ ActionResult,
13
+ Content,
14
+ ModelType,
15
+ )
16
+ from elizaos.utils.spec_examples import convert_spec_examples
17
+
18
+ if TYPE_CHECKING:
19
+ from elizaos.types import (
20
+ HandlerCallback,
21
+ HandlerOptions,
22
+ IAgentRuntime,
23
+ Memory,
24
+ State,
25
+ )
26
+
27
+ # Get text content from centralized specs
28
+ _spec = require_action_spec("REMOVE_CONTACT")
29
+
30
+
31
+ def _convert_spec_examples() -> list[list[ActionExample]]:
32
+ """Convert spec examples to ActionExample format."""
33
+ return convert_spec_examples(_spec)
34
+
35
+
36
+ @dataclass
37
+ class RemoveContactAction:
38
+ name: str = _spec["name"]
39
+ similes: list[str] = field(default_factory=lambda: list(_spec.get("similes", [])))
40
+ description: str = _spec["description"]
41
+
42
+ async def validate(
43
+ self, runtime: IAgentRuntime, _message: Memory, _state: State | None = None
44
+ ) -> bool:
45
+ rolodex_service = runtime.get_service("rolodex")
46
+ return rolodex_service is not None
47
+
48
+ async def handler(
49
+ self,
50
+ runtime: IAgentRuntime,
51
+ message: Memory,
52
+ state: State | None = None,
53
+ options: HandlerOptions | None = None,
54
+ callback: HandlerCallback | None = None,
55
+ responses: list[Memory] | None = None,
56
+ ) -> ActionResult:
57
+ from elizaos.bootstrap.services.rolodex import RolodexService
58
+
59
+ rolodex_service = runtime.get_service("rolodex")
60
+ if not rolodex_service or not isinstance(rolodex_service, RolodexService):
61
+ return ActionResult(
62
+ text="Rolodex service not available",
63
+ success=False,
64
+ values={"error": True},
65
+ data={"error": "RolodexService not available"},
66
+ )
67
+
68
+ state = await runtime.compose_state(message, ["RECENT_MESSAGES", "ENTITIES"])
69
+
70
+ prompt = runtime.compose_prompt_from_state(
71
+ state=state,
72
+ template=REMOVE_CONTACT_TEMPLATE,
73
+ )
74
+
75
+ response = await runtime.use_model(ModelType.TEXT_SMALL, {"prompt": prompt})
76
+ parsed = parse_key_value_xml(response)
77
+
78
+ if not parsed or not parsed.get("contactName"):
79
+ return ActionResult(
80
+ text="Could not determine which contact to remove",
81
+ success=False,
82
+ values={"error": True},
83
+ data={"error": "No contact name provided"},
84
+ )
85
+
86
+ contact_name = str(parsed.get("contactName", ""))
87
+ confirmed = str(parsed.get("confirmed", "no")).lower() == "yes"
88
+
89
+ if not confirmed:
90
+ response_text = (
91
+ f'To remove {contact_name}, please confirm by saying "yes, remove {contact_name}".'
92
+ )
93
+ if callback:
94
+ await callback(Content(text=response_text, actions=["REMOVE_CONTACT"]))
95
+ return ActionResult(
96
+ text=response_text,
97
+ success=True,
98
+ values={"needsConfirmation": True},
99
+ data={"contactName": contact_name},
100
+ )
101
+
102
+ contacts = await rolodex_service.search_contacts(search_term=contact_name)
103
+
104
+ if not contacts:
105
+ return ActionResult(
106
+ text=f"Could not find a contact named '{contact_name}'",
107
+ success=False,
108
+ values={"error": True},
109
+ data={"error": "Contact not found"},
110
+ )
111
+
112
+ contact = contacts[0]
113
+ removed = await rolodex_service.remove_contact(contact.entity_id)
114
+
115
+ if removed:
116
+ response_text = f"I've removed {contact_name} from your contacts."
117
+ if callback:
118
+ await callback(Content(text=response_text, actions=["REMOVE_CONTACT"]))
119
+ return ActionResult(
120
+ text=response_text,
121
+ success=True,
122
+ values={"contactId": str(contact.entity_id)},
123
+ data={"success": True},
124
+ )
125
+ else:
126
+ return ActionResult(
127
+ text="Failed to remove contact",
128
+ success=False,
129
+ values={"error": True},
130
+ data={"error": "Remove operation failed"},
131
+ )
132
+
133
+ @property
134
+ def examples(self) -> list[list[ActionExample]]:
135
+ return _convert_spec_examples()
136
+
137
+
138
+ remove_contact_action = Action(
139
+ name=RemoveContactAction.name,
140
+ similes=RemoveContactAction().similes,
141
+ description=RemoveContactAction.description,
142
+ validate=RemoveContactAction().validate,
143
+ handler=RemoveContactAction().handler,
144
+ examples=RemoveContactAction().examples,
145
+ )
@@ -0,0 +1,207 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING
6
+ from uuid import UUID
7
+
8
+ from elizaos.bootstrap.utils.xml import parse_key_value_xml
9
+ from elizaos.generated.spec_helpers import require_action_spec
10
+ from elizaos.prompts import UPDATE_ROLE_TEMPLATE
11
+ from elizaos.types import Action, ActionExample, ActionResult, Content, ModelType
12
+
13
+ if TYPE_CHECKING:
14
+ from elizaos.types import HandlerCallback, HandlerOptions, IAgentRuntime, Memory, State
15
+
16
+ # Get text content from centralized specs
17
+ _spec = require_action_spec("UPDATE_ROLE")
18
+
19
+
20
+ def _convert_spec_examples() -> list[list[ActionExample]]:
21
+ """Convert spec examples to ActionExample format."""
22
+ spec_examples = _spec.get("examples", [])
23
+ if not isinstance(spec_examples, list):
24
+ return []
25
+ result: list[list[ActionExample]] = []
26
+ for example in spec_examples:
27
+ if not isinstance(example, list):
28
+ continue
29
+ row: list[ActionExample] = []
30
+ for msg in example:
31
+ if not isinstance(msg, dict):
32
+ continue
33
+ content = msg.get("content", {})
34
+ text = ""
35
+ actions: list[str] | None = None
36
+ if isinstance(content, dict):
37
+ text_val = content.get("text", "")
38
+ text = str(text_val) if text_val else ""
39
+ actions_val = content.get("actions")
40
+ if isinstance(actions_val, list) and all(isinstance(a, str) for a in actions_val):
41
+ actions = list(actions_val)
42
+ row.append(
43
+ ActionExample(
44
+ name=str(msg.get("name", "")),
45
+ content=Content(text=text, actions=actions),
46
+ )
47
+ )
48
+ if row:
49
+ result.append(row)
50
+ return result
51
+
52
+
53
+ class Role(str, Enum):
54
+ OWNER = "OWNER"
55
+ ADMIN = "ADMIN"
56
+ MEMBER = "MEMBER"
57
+ GUEST = "GUEST"
58
+ NONE = "NONE"
59
+
60
+
61
+ @dataclass
62
+ class UpdateRoleAction:
63
+ name: str = _spec["name"]
64
+ similes: list[str] = field(default_factory=lambda: list(_spec.get("similes", [])))
65
+ description: str = _spec["description"]
66
+
67
+ async def validate(
68
+ self, runtime: IAgentRuntime, message: Memory, _state: State | None = None
69
+ ) -> bool:
70
+ room_id = message.room_id
71
+ if not room_id:
72
+ return False
73
+
74
+ room = await runtime.get_room(room_id)
75
+ if room is None or room.world_id is None:
76
+ return False
77
+
78
+ world = await runtime.get_world(room.world_id)
79
+ if world is None or world.metadata is None:
80
+ return False
81
+
82
+ roles = world.metadata.get("roles", {})
83
+ agent_role = roles.get(str(runtime.agent_id), Role.NONE.value)
84
+
85
+ return agent_role in (Role.OWNER.value, Role.ADMIN.value)
86
+
87
+ async def handler(
88
+ self,
89
+ runtime: IAgentRuntime,
90
+ message: Memory,
91
+ state: State | None = None,
92
+ options: HandlerOptions | None = None,
93
+ callback: HandlerCallback | None = None,
94
+ responses: list[Memory] | None = None,
95
+ ) -> ActionResult:
96
+ if state is None:
97
+ raise ValueError("State is required for UPDATE_ROLE action")
98
+
99
+ room_id = message.room_id
100
+ if not room_id:
101
+ return ActionResult(
102
+ text="No room context for role update",
103
+ values={"success": False, "error": "no_room_id"},
104
+ data={"actionName": "UPDATE_ROLE"},
105
+ success=False,
106
+ )
107
+
108
+ room = await runtime.get_room(room_id)
109
+ if room is None or room.world_id is None:
110
+ return ActionResult(
111
+ text="Room or world not found",
112
+ values={"success": False, "error": "room_not_found"},
113
+ data={"actionName": "UPDATE_ROLE"},
114
+ success=False,
115
+ )
116
+
117
+ world = await runtime.get_world(room.world_id)
118
+ if world is None or world.metadata is None:
119
+ return ActionResult(
120
+ text="World not found",
121
+ values={"success": False, "error": "world_not_found"},
122
+ data={"actionName": "UPDATE_ROLE"},
123
+ success=False,
124
+ )
125
+
126
+ state = await runtime.compose_state(
127
+ message, ["RECENT_MESSAGES", "ACTION_STATE", "WORLD_INFO"]
128
+ )
129
+
130
+ current_roles = world.metadata.get("roles", {})
131
+ roles_context = "\n".join(
132
+ f"- {entity_id}: {role}" for entity_id, role in current_roles.items()
133
+ )
134
+
135
+ template = (
136
+ runtime.character.templates.get("updateRoleTemplate")
137
+ if runtime.character.templates and "updateRoleTemplate" in runtime.character.templates
138
+ else UPDATE_ROLE_TEMPLATE
139
+ )
140
+ prompt = runtime.compose_prompt(state=state, template=template)
141
+ prompt = prompt.replace("{{roles}}", roles_context)
142
+
143
+ response_text = await runtime.use_model(ModelType.TEXT_LARGE, prompt=prompt)
144
+ parsed_xml = parse_key_value_xml(response_text)
145
+
146
+ if parsed_xml is None:
147
+ raise ValueError("Failed to parse XML response")
148
+
149
+ thought = str(parsed_xml.get("thought", ""))
150
+ entity_id_str = str(parsed_xml.get("entity_id", ""))
151
+ new_role_str = str(parsed_xml.get("new_role", "")).upper()
152
+
153
+ if not entity_id_str:
154
+ raise ValueError("No entity ID provided")
155
+
156
+ if new_role_str not in [r.value for r in Role]:
157
+ raise ValueError(f"Invalid role: {new_role_str}")
158
+
159
+ entity_id = UUID(entity_id_str)
160
+
161
+ roles = dict(world.metadata.get("roles", {}))
162
+ old_role = roles.get(str(entity_id), Role.NONE.value)
163
+ roles[str(entity_id)] = new_role_str
164
+ world.metadata["roles"] = roles
165
+
166
+ await runtime.update_world(world)
167
+
168
+ response_content = Content(
169
+ text=f"Updated role for {entity_id_str}: {old_role} -> {new_role_str}",
170
+ actions=["UPDATE_ROLE"],
171
+ )
172
+
173
+ if callback:
174
+ await callback(response_content)
175
+
176
+ return ActionResult(
177
+ text=f"Role updated: {entity_id_str} is now {new_role_str}",
178
+ values={
179
+ "success": True,
180
+ "roleUpdated": True,
181
+ "entityId": str(entity_id),
182
+ "oldRole": old_role,
183
+ "newRole": new_role_str,
184
+ },
185
+ data={
186
+ "actionName": "UPDATE_ROLE",
187
+ "entityId": str(entity_id),
188
+ "oldRole": old_role,
189
+ "newRole": new_role_str,
190
+ "thought": thought,
191
+ },
192
+ success=True,
193
+ )
194
+
195
+ @property
196
+ def examples(self) -> list[list[ActionExample]]:
197
+ return _convert_spec_examples()
198
+
199
+
200
+ update_role_action = Action(
201
+ name=UpdateRoleAction.name,
202
+ similes=UpdateRoleAction().similes,
203
+ description=UpdateRoleAction.description,
204
+ validate=UpdateRoleAction().validate,
205
+ handler=UpdateRoleAction().handler,
206
+ examples=UpdateRoleAction().examples,
207
+ )
@@ -0,0 +1,154 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import TYPE_CHECKING
6
+
7
+ from elizaos.bootstrap.utils.xml import parse_key_value_xml
8
+ from elizaos.generated.spec_helpers import require_action_spec
9
+ from elizaos.prompts import SCHEDULE_FOLLOW_UP_TEMPLATE
10
+ from elizaos.types import (
11
+ Action,
12
+ ActionExample,
13
+ ActionResult,
14
+ Content,
15
+ ModelType,
16
+ )
17
+ from elizaos.utils.spec_examples import convert_spec_examples
18
+
19
+ if TYPE_CHECKING:
20
+ from elizaos.types import (
21
+ HandlerCallback,
22
+ HandlerOptions,
23
+ IAgentRuntime,
24
+ Memory,
25
+ State,
26
+ )
27
+
28
+ # Get text content from centralized specs
29
+ _spec = require_action_spec("SCHEDULE_FOLLOW_UP")
30
+
31
+
32
+ def _convert_spec_examples() -> list[list[ActionExample]]:
33
+ """Convert spec examples to ActionExample format."""
34
+ return convert_spec_examples(_spec)
35
+
36
+
37
+ @dataclass
38
+ class ScheduleFollowUpAction:
39
+ name: str = _spec["name"]
40
+ similes: list[str] = field(default_factory=lambda: list(_spec.get("similes", [])))
41
+ description: str = _spec["description"]
42
+
43
+ async def validate(
44
+ self, runtime: IAgentRuntime, _message: Memory, _state: State | None = None
45
+ ) -> bool:
46
+ rolodex_service = runtime.get_service("rolodex")
47
+ follow_up_service = runtime.get_service("follow_up")
48
+ return rolodex_service is not None and follow_up_service is not None
49
+
50
+ async def handler(
51
+ self,
52
+ runtime: IAgentRuntime,
53
+ message: Memory,
54
+ state: State | None = None,
55
+ options: HandlerOptions | None = None,
56
+ callback: HandlerCallback | None = None,
57
+ responses: list[Memory] | None = None,
58
+ ) -> ActionResult:
59
+ from elizaos.bootstrap.services.follow_up import FollowUpService
60
+ from elizaos.bootstrap.services.rolodex import RolodexService
61
+
62
+ rolodex_service = runtime.get_service("rolodex")
63
+ follow_up_service = runtime.get_service("follow_up")
64
+
65
+ if not rolodex_service or not isinstance(rolodex_service, RolodexService):
66
+ return ActionResult(
67
+ text="Rolodex service not available",
68
+ success=False,
69
+ values={"error": True},
70
+ data={"error": "RolodexService not available"},
71
+ )
72
+
73
+ if not follow_up_service or not isinstance(follow_up_service, FollowUpService):
74
+ return ActionResult(
75
+ text="Follow-up service not available",
76
+ success=False,
77
+ values={"error": True},
78
+ data={"error": "FollowUpService not available"},
79
+ )
80
+
81
+ state = await runtime.compose_state(message, ["RECENT_MESSAGES", "ENTITIES"])
82
+ state.values["currentDateTime"] = datetime.utcnow().isoformat()
83
+
84
+ prompt = runtime.compose_prompt_from_state(
85
+ state=state,
86
+ template=SCHEDULE_FOLLOW_UP_TEMPLATE,
87
+ )
88
+
89
+ response = await runtime.use_model(ModelType.TEXT_SMALL, {"prompt": prompt})
90
+ parsed = parse_key_value_xml(response)
91
+
92
+ if not parsed or not parsed.get("contactName"):
93
+ return ActionResult(
94
+ text="Could not extract follow-up information",
95
+ success=False,
96
+ values={"error": True},
97
+ data={"error": "Failed to parse follow-up info"},
98
+ )
99
+
100
+ contact_name = str(parsed.get("contactName", ""))
101
+ scheduled_at_str = str(parsed.get("scheduledAt", ""))
102
+ reason = str(parsed.get("reason", "Follow-up"))
103
+ priority = str(parsed.get("priority", "medium"))
104
+ follow_up_message = str(parsed.get("message", ""))
105
+
106
+ scheduled_at = datetime.fromisoformat(scheduled_at_str.replace("Z", "+00:00"))
107
+
108
+ from uuid import UUID as StdUUID
109
+
110
+ entity_id = message.entity_id
111
+ entity_id_uuid = StdUUID(str(entity_id)) if entity_id else None
112
+ if entity_id_uuid:
113
+ await follow_up_service.schedule_follow_up(
114
+ entity_id=entity_id_uuid,
115
+ scheduled_at=scheduled_at,
116
+ reason=reason,
117
+ priority=priority,
118
+ message=follow_up_message,
119
+ )
120
+
121
+ response_text = f"I've scheduled a follow-up with {contact_name} for {scheduled_at.strftime('%B %d, %Y')}. Reason: {reason}"
122
+
123
+ if callback:
124
+ await callback(Content(text=response_text, actions=["SCHEDULE_FOLLOW_UP"]))
125
+
126
+ return ActionResult(
127
+ text=response_text,
128
+ success=True,
129
+ values={
130
+ "contactId": str(entity_id),
131
+ "scheduledAt": scheduled_at.isoformat(),
132
+ },
133
+ data={
134
+ "contactId": str(entity_id),
135
+ "contactName": contact_name,
136
+ "scheduledAt": scheduled_at.isoformat(),
137
+ "reason": reason,
138
+ "priority": priority,
139
+ },
140
+ )
141
+
142
+ @property
143
+ def examples(self) -> list[list[ActionExample]]:
144
+ return _convert_spec_examples()
145
+
146
+
147
+ schedule_follow_up_action = Action(
148
+ name=ScheduleFollowUpAction.name,
149
+ similes=ScheduleFollowUpAction().similes,
150
+ description=ScheduleFollowUpAction.description,
151
+ validate=ScheduleFollowUpAction().validate,
152
+ handler=ScheduleFollowUpAction().handler,
153
+ examples=ScheduleFollowUpAction().examples,
154
+ )