@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,112 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from elizaos.types import Provider, ProviderResult
6
+
7
+ if TYPE_CHECKING:
8
+ from elizaos.types import IAgentRuntime, Memory, State
9
+
10
+
11
+ def format_relationship(
12
+ relationship: dict[str, str | int | list[str] | dict[str, str]],
13
+ target_name: str,
14
+ ) -> str:
15
+ tags = relationship.get("tags", [])
16
+ tags_str = (", ".join(tags) if tags else "none") if isinstance(tags, list) else str(tags)
17
+
18
+ interactions = relationship.get("metadata", {})
19
+ interaction_count = interactions.get("interactions", 0) if isinstance(interactions, dict) else 0
20
+
21
+ return f"- {target_name}: tags=[{tags_str}], interactions={interaction_count}"
22
+
23
+
24
+ async def get_relationships(
25
+ runtime: IAgentRuntime,
26
+ message: Memory,
27
+ state: State | None = None,
28
+ ) -> ProviderResult:
29
+ entity_id = message.entity_id
30
+ if not entity_id:
31
+ return ProviderResult(
32
+ text="No relationships found.",
33
+ values={"relationshipCount": 0},
34
+ data={"relationships": []},
35
+ )
36
+
37
+ try:
38
+ relationships = await runtime.get_relationships({"entityId": str(entity_id)})
39
+ except Exception as e:
40
+ runtime.logger.debug(
41
+ f"Failed to get relationships: src=provider:relationships agentId={runtime.agent_id} error={e}"
42
+ )
43
+ relationships = []
44
+
45
+ if not relationships:
46
+ return ProviderResult(
47
+ text="No relationships found.",
48
+ values={"relationshipCount": 0},
49
+ data={"relationships": []},
50
+ )
51
+
52
+ def _get_interactions(r: object) -> int:
53
+ if isinstance(r, dict):
54
+ meta = r.get("metadata", {})
55
+ if isinstance(meta, dict):
56
+ val = meta.get("interactions", 0)
57
+ return int(val) if isinstance(val, (int, float)) else 0
58
+ return 0
59
+
60
+ sorted_relationships = sorted(
61
+ relationships,
62
+ key=_get_interactions,
63
+ reverse=True,
64
+ )[:30]
65
+
66
+ formatted_relationships: list[str] = []
67
+ entity_cache: dict[str, str] = {}
68
+ for rel in sorted_relationships:
69
+ if not isinstance(rel, dict):
70
+ continue
71
+ target_id = rel.get("targetEntityId")
72
+ if not target_id:
73
+ continue
74
+
75
+ target_id_str = str(target_id)
76
+ target_name = entity_cache.get(target_id_str)
77
+ if target_name is None:
78
+ target_entity = await runtime.get_entity(target_id_str)
79
+ target_name = target_entity.name if target_entity else target_id_str[:8]
80
+ entity_cache[target_id_str] = target_name
81
+
82
+ formatted_relationships.append(format_relationship(rel, target_name))
83
+
84
+ if not formatted_relationships:
85
+ return ProviderResult(
86
+ text="No relationships found.",
87
+ values={"relationshipCount": 0},
88
+ data={"relationships": []},
89
+ )
90
+
91
+ sender_name = message.content.sender_name if message.content else "Unknown"
92
+ text = f"# {runtime.character.name} has observed {sender_name} interacting with:\n" + "\n".join(
93
+ formatted_relationships
94
+ )
95
+
96
+ return ProviderResult(
97
+ text=text,
98
+ values={
99
+ "relationshipCount": len(sorted_relationships),
100
+ },
101
+ data={
102
+ "relationships": sorted_relationships,
103
+ },
104
+ )
105
+
106
+
107
+ relationships_provider = Provider(
108
+ name="RELATIONSHIPS",
109
+ description="Relationships between entities observed by the agent",
110
+ get=get_relationships,
111
+ dynamic=True,
112
+ )
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING
5
+
6
+ from elizaos.types import Provider, ProviderResult
7
+
8
+ if TYPE_CHECKING:
9
+ from elizaos.types import IAgentRuntime, Memory, State
10
+
11
+
12
+ def format_role_info(entity_name: str, role: str) -> str:
13
+ return f"- {entity_name}: {role}"
14
+
15
+
16
+ async def get_roles(
17
+ runtime: IAgentRuntime,
18
+ message: Memory,
19
+ state: State | None = None,
20
+ ) -> ProviderResult:
21
+ role_info: list[dict[str, str]] = []
22
+
23
+ world_id = None
24
+ if state and hasattr(state, "world"):
25
+ world = state.world
26
+ if world and hasattr(world, "id"):
27
+ world_id = world.id
28
+
29
+ if not world_id and message.room_id:
30
+ room = await runtime.get_room(message.room_id)
31
+ if room and hasattr(room, "world_id"):
32
+ world_id = room.world_id
33
+
34
+ if world_id:
35
+ world = await runtime.get_world(world_id)
36
+ if world and hasattr(world, "metadata"):
37
+ roles = world.metadata.get("roles", {})
38
+ if isinstance(roles, dict):
39
+ entity_ids = list(roles.keys())
40
+ entities = await asyncio.gather(
41
+ *(runtime.get_entity(entity_id) for entity_id in entity_ids)
42
+ )
43
+ for entity_id, entity, role in zip(
44
+ entity_ids, entities, roles.values(), strict=False
45
+ ):
46
+ entity_name = entity.name if entity else str(entity_id)[:8]
47
+
48
+ role_info.append(
49
+ {
50
+ "entityId": str(entity_id),
51
+ "entityName": entity_name,
52
+ "role": str(role),
53
+ }
54
+ )
55
+
56
+ if message.entity_id:
57
+ entity = await runtime.get_entity(message.entity_id)
58
+ if entity and hasattr(entity, "metadata"):
59
+ sender_role = entity.metadata.get("role")
60
+ if sender_role:
61
+ existing = next(
62
+ (r for r in role_info if r["entityId"] == str(message.entity_id)), None
63
+ )
64
+ if not existing:
65
+ role_info.append(
66
+ {
67
+ "entityId": str(message.entity_id),
68
+ "entityName": entity.name or "Unknown",
69
+ "role": str(sender_role),
70
+ }
71
+ )
72
+
73
+ if not role_info:
74
+ return ProviderResult(text="", values={"roleCount": 0}, data={"roles": []})
75
+
76
+ formatted_roles = "\n".join(format_role_info(r["entityName"], r["role"]) for r in role_info)
77
+
78
+ text = f"# Entity Roles\n{formatted_roles}"
79
+
80
+ return ProviderResult(
81
+ text=text,
82
+ values={
83
+ "roleCount": len(role_info),
84
+ "roles": {r["entityName"]: r["role"] for r in role_info},
85
+ },
86
+ data={
87
+ "roles": role_info,
88
+ },
89
+ )
90
+
91
+
92
+ roles_provider = Provider(
93
+ name="ROLES",
94
+ description="Roles assigned to entities in the current context",
95
+ get=get_roles,
96
+ dynamic=True,
97
+ )
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from elizaos.bootstrap.providers.agent_settings import SENSITIVE_KEY_PATTERNS
6
+ from elizaos.types import Provider, ProviderResult
7
+
8
+ if TYPE_CHECKING:
9
+ from elizaos.types import IAgentRuntime, Memory, State
10
+
11
+
12
+ async def get_settings_context(
13
+ runtime: IAgentRuntime,
14
+ message: Memory,
15
+ state: State | None = None,
16
+ ) -> ProviderResult:
17
+ _ = message, state
18
+
19
+ all_settings = runtime.get_all_settings()
20
+
21
+ safe_settings: dict[str, str] = {}
22
+ for key, value in all_settings.items():
23
+ if not any(pattern in key.lower() for pattern in SENSITIVE_KEY_PATTERNS):
24
+ safe_settings[key] = str(value)
25
+
26
+ lines: list[str] = []
27
+ if safe_settings:
28
+ lines.append("## Current Configuration")
29
+ for key, value in safe_settings.items():
30
+ display_value = value if len(value) <= 50 else value[:50] + "..."
31
+ lines.append(f"- {key}: {display_value}")
32
+
33
+ context_text = "\n".join(lines) if lines else ""
34
+
35
+ return ProviderResult(
36
+ text=context_text,
37
+ values={
38
+ "settings": context_text,
39
+ "settingsCount": len(safe_settings),
40
+ "hasSettings": len(safe_settings) > 0,
41
+ },
42
+ data={"settings": safe_settings},
43
+ )
44
+
45
+
46
+ settings_provider = Provider(
47
+ name="SETTINGS",
48
+ description="Current settings for the agent/server (filtered for security)",
49
+ get=get_settings_context,
50
+ dynamic=True,
51
+ )
@@ -0,0 +1,18 @@
1
+ """Advanced Services - Extended services for agent operation.
2
+
3
+ Services that can be enabled with `advanced_capabilities=True`.
4
+ """
5
+
6
+ from .follow_up import FollowUpService
7
+ from .rolodex import RolodexService
8
+
9
+ __all__ = [
10
+ "FollowUpService",
11
+ "RolodexService",
12
+ "advanced_services",
13
+ ]
14
+
15
+ advanced_services: list[type] = [
16
+ RolodexService,
17
+ FollowUpService,
18
+ ]
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import UTC, datetime
5
+ from typing import TYPE_CHECKING
6
+ from uuid import UUID
7
+
8
+ from elizaos.types import Service, ServiceType
9
+
10
+ if TYPE_CHECKING:
11
+ from elizaos.types import IAgentRuntime
12
+
13
+
14
+ @dataclass
15
+ class FollowUpTask:
16
+ entity_id: UUID
17
+ reason: str
18
+ message: str | None = None
19
+ priority: str = "medium"
20
+ scheduled_at: str = ""
21
+ metadata: dict[str, str | int | float | bool] = field(default_factory=dict)
22
+
23
+
24
+ @dataclass
25
+ class FollowUpSuggestion:
26
+ entity_id: UUID
27
+ entity_name: str
28
+ days_since_last_contact: int
29
+ relationship_strength: float
30
+ suggested_reason: str
31
+
32
+
33
+ class FollowUpService(Service):
34
+ name = "follow_up"
35
+ service_type = ServiceType.TASK
36
+
37
+ @property
38
+ def capability_description(self) -> str:
39
+ return "Follow-up scheduling and reminder management service"
40
+
41
+ def __init__(self) -> None:
42
+ self._follow_ups: dict[UUID, FollowUpTask] = {}
43
+ self._runtime: IAgentRuntime | None = None
44
+
45
+ @classmethod
46
+ async def start(cls, runtime: IAgentRuntime) -> FollowUpService:
47
+ service = cls()
48
+ service._runtime = runtime
49
+ runtime.logger.info(
50
+ "Follow-up service started",
51
+ src="service:follow_up",
52
+ agentId=str(runtime.agent_id),
53
+ )
54
+ return service
55
+
56
+ async def stop(self) -> None:
57
+ if self._runtime:
58
+ self._runtime.logger.info(
59
+ "Follow-up service stopped",
60
+ src="service:follow_up",
61
+ agentId=str(self._runtime.agent_id),
62
+ )
63
+ self._follow_ups.clear()
64
+ self._runtime = None
65
+
66
+ async def schedule_follow_up(
67
+ self,
68
+ entity_id: UUID,
69
+ scheduled_at: datetime,
70
+ reason: str,
71
+ priority: str = "medium",
72
+ message: str | None = None,
73
+ ) -> FollowUpTask:
74
+ task = FollowUpTask(
75
+ entity_id=entity_id,
76
+ reason=reason,
77
+ message=message,
78
+ priority=priority,
79
+ scheduled_at=scheduled_at.isoformat(),
80
+ )
81
+
82
+ self._follow_ups[entity_id] = task
83
+
84
+ if self._runtime:
85
+ self._runtime.logger.info(
86
+ f"Scheduled follow-up with {entity_id}",
87
+ src="service:follow_up",
88
+ scheduled_at=task.scheduled_at,
89
+ )
90
+
91
+ return task
92
+
93
+ async def get_follow_up(self, entity_id: UUID) -> FollowUpTask | None:
94
+ return self._follow_ups.get(entity_id)
95
+
96
+ async def cancel_follow_up(self, entity_id: UUID) -> bool:
97
+ if entity_id in self._follow_ups:
98
+ del self._follow_ups[entity_id]
99
+ return True
100
+ return False
101
+
102
+ async def get_upcoming_follow_ups(
103
+ self,
104
+ days_ahead: int = 7,
105
+ include_overdue: bool = True,
106
+ ) -> list[FollowUpTask]:
107
+ now = datetime.now(UTC)
108
+ results: list[FollowUpTask] = []
109
+
110
+ for task in self._follow_ups.values():
111
+ scheduled = datetime.fromisoformat(task.scheduled_at.replace("Z", "+00:00"))
112
+ days_until = (scheduled - now).days
113
+
114
+ if include_overdue and days_until < 0 or 0 <= days_until <= days_ahead:
115
+ results.append(task)
116
+
117
+ results.sort(key=lambda t: t.scheduled_at)
118
+ return results
119
+
120
+ async def get_overdue_follow_ups(self) -> list[FollowUpTask]:
121
+ now = datetime.now(UTC)
122
+ results: list[FollowUpTask] = []
123
+
124
+ for task in self._follow_ups.values():
125
+ scheduled = datetime.fromisoformat(task.scheduled_at.replace("Z", "+00:00"))
126
+ if scheduled < now:
127
+ results.append(task)
128
+
129
+ return results
130
+
131
+ async def get_follow_up_suggestions(
132
+ self,
133
+ max_suggestions: int = 5,
134
+ ) -> list[FollowUpSuggestion]:
135
+ return []
136
+
137
+ async def complete_follow_up(self, entity_id: UUID) -> bool:
138
+ return await self.cancel_follow_up(entity_id)
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING
7
+ from uuid import UUID
8
+
9
+ from elizaos.types import Service, ServiceType
10
+
11
+ if TYPE_CHECKING:
12
+ from elizaos.types import IAgentRuntime
13
+
14
+
15
+ class ContactCategory(str, Enum):
16
+ FRIEND = "friend"
17
+ FAMILY = "family"
18
+ COLLEAGUE = "colleague"
19
+ ACQUAINTANCE = "acquaintance"
20
+ VIP = "vip"
21
+ BUSINESS = "business"
22
+
23
+
24
+ @dataclass
25
+ class ContactPreferences:
26
+ preferred_channel: str | None = None
27
+ timezone: str | None = None
28
+ language: str | None = None
29
+ contact_frequency: str | None = None
30
+ do_not_disturb: bool = False
31
+ notes: str | None = None
32
+
33
+
34
+ @dataclass
35
+ class ContactInfo:
36
+ entity_id: UUID
37
+ categories: list[str] = field(default_factory=list)
38
+ tags: list[str] = field(default_factory=list)
39
+ preferences: ContactPreferences = field(default_factory=ContactPreferences)
40
+ custom_fields: dict[str, str | int | float | bool] = field(default_factory=dict)
41
+ privacy_level: str = "private"
42
+ last_modified: str = ""
43
+
44
+
45
+ @dataclass
46
+ class RelationshipAnalytics:
47
+ strength: float = 0.0
48
+ interaction_count: int = 0
49
+ last_interaction_at: str | None = None
50
+ average_response_time: float | None = None
51
+ sentiment_score: float | None = None
52
+ topics_discussed: list[str] = field(default_factory=list)
53
+
54
+
55
+ def calculate_relationship_strength(
56
+ interaction_count: int,
57
+ last_interaction_at: str | None = None,
58
+ message_quality: float = 5.0,
59
+ relationship_type: str = "acquaintance",
60
+ ) -> float:
61
+ interaction_score = min(interaction_count * 2, 40)
62
+
63
+ recency_score = 0.0
64
+ if last_interaction_at:
65
+ last_dt = datetime.fromisoformat(last_interaction_at.replace("Z", "+00:00"))
66
+ days_since = (datetime.now(last_dt.tzinfo) - last_dt).days
67
+ if days_since < 1:
68
+ recency_score = 30
69
+ elif days_since < 7:
70
+ recency_score = 25
71
+ elif days_since < 30:
72
+ recency_score = 15
73
+ elif days_since < 90:
74
+ recency_score = 5
75
+
76
+ quality_score = min(message_quality * 2, 20)
77
+
78
+ relationship_bonus = {
79
+ "family": 10,
80
+ "friend": 8,
81
+ "colleague": 6,
82
+ "acquaintance": 4,
83
+ "unknown": 0,
84
+ }
85
+
86
+ total = (
87
+ interaction_score
88
+ + recency_score
89
+ + quality_score
90
+ + relationship_bonus.get(relationship_type, 0)
91
+ )
92
+ return max(0.0, min(100.0, round(total, 1)))
93
+
94
+
95
+ class RolodexService(Service):
96
+ name = "rolodex"
97
+ service_type = ServiceType.UNKNOWN
98
+
99
+ @property
100
+ def capability_description(self) -> str:
101
+ return "Comprehensive contact and relationship management service"
102
+
103
+ def __init__(self) -> None:
104
+ self._contacts: dict[UUID, ContactInfo] = {}
105
+ self._analytics: dict[str, RelationshipAnalytics] = {}
106
+ self._runtime: IAgentRuntime | None = None
107
+
108
+ @classmethod
109
+ async def start(cls, runtime: IAgentRuntime) -> RolodexService:
110
+ service = cls()
111
+ service._runtime = runtime
112
+ runtime.logger.info(
113
+ "Rolodex service started",
114
+ src="service:rolodex",
115
+ agentId=str(runtime.agent_id),
116
+ )
117
+ return service
118
+
119
+ async def stop(self) -> None:
120
+ if self._runtime:
121
+ self._runtime.logger.info(
122
+ "Rolodex service stopped",
123
+ src="service:rolodex",
124
+ agentId=str(self._runtime.agent_id),
125
+ )
126
+ self._contacts.clear()
127
+ self._analytics.clear()
128
+ self._runtime = None
129
+
130
+ async def add_contact(
131
+ self,
132
+ entity_id: UUID,
133
+ categories: list[str] | None = None,
134
+ preferences: ContactPreferences | None = None,
135
+ custom_fields: dict[str, str | int | float | bool] | None = None,
136
+ ) -> ContactInfo:
137
+ contact = ContactInfo(
138
+ entity_id=entity_id,
139
+ categories=categories or ["acquaintance"],
140
+ tags=[],
141
+ preferences=preferences or ContactPreferences(),
142
+ custom_fields=custom_fields or {},
143
+ privacy_level="private",
144
+ last_modified=datetime.utcnow().isoformat(),
145
+ )
146
+
147
+ self._contacts[entity_id] = contact
148
+
149
+ if self._runtime:
150
+ self._runtime.logger.info(
151
+ f"Added contact {entity_id}",
152
+ src="service:rolodex",
153
+ categories=contact.categories,
154
+ )
155
+
156
+ return contact
157
+
158
+ async def get_contact(self, entity_id: UUID) -> ContactInfo | None:
159
+ return self._contacts.get(entity_id)
160
+
161
+ async def update_contact(
162
+ self,
163
+ entity_id: UUID,
164
+ categories: list[str] | None = None,
165
+ tags: list[str] | None = None,
166
+ preferences: ContactPreferences | None = None,
167
+ custom_fields: dict[str, str | int | float | bool] | None = None,
168
+ ) -> ContactInfo | None:
169
+ contact = self._contacts.get(entity_id)
170
+ if not contact:
171
+ return None
172
+
173
+ if categories is not None:
174
+ contact.categories = categories
175
+ if tags is not None:
176
+ contact.tags = tags
177
+ if preferences is not None:
178
+ contact.preferences = preferences
179
+ if custom_fields is not None:
180
+ contact.custom_fields = custom_fields
181
+
182
+ contact.last_modified = datetime.utcnow().isoformat()
183
+
184
+ return contact
185
+
186
+ async def remove_contact(self, entity_id: UUID) -> bool:
187
+ if entity_id in self._contacts:
188
+ del self._contacts[entity_id]
189
+ return True
190
+ return False
191
+
192
+ async def search_contacts(
193
+ self,
194
+ categories: list[str] | None = None,
195
+ tags: list[str] | None = None,
196
+ search_term: str | None = None,
197
+ ) -> list[ContactInfo]:
198
+ results = list(self._contacts.values())
199
+
200
+ if categories:
201
+ results = [c for c in results if any(cat in c.categories for cat in categories)]
202
+
203
+ if tags:
204
+ results = [c for c in results if any(tag in c.tags for tag in tags)]
205
+
206
+ return results
207
+
208
+ async def get_all_contacts(self) -> list[ContactInfo]:
209
+ return list(self._contacts.values())
210
+
211
+ async def get_relationship_analytics(
212
+ self,
213
+ entity_id: UUID,
214
+ ) -> RelationshipAnalytics | None:
215
+ key = str(entity_id)
216
+ return self._analytics.get(key)
217
+
218
+ async def update_relationship_analytics(
219
+ self,
220
+ entity_id: UUID,
221
+ interaction_count: int | None = None,
222
+ last_interaction_at: str | None = None,
223
+ ) -> RelationshipAnalytics:
224
+ key = str(entity_id)
225
+ analytics = self._analytics.get(key) or RelationshipAnalytics()
226
+
227
+ if interaction_count is not None:
228
+ analytics.interaction_count = interaction_count
229
+ if last_interaction_at is not None:
230
+ analytics.last_interaction_at = last_interaction_at
231
+
232
+ contact = self._contacts.get(entity_id)
233
+ relationship_type = "acquaintance"
234
+ if contact and contact.categories:
235
+ relationship_type = contact.categories[0]
236
+
237
+ analytics.strength = calculate_relationship_strength(
238
+ analytics.interaction_count,
239
+ analytics.last_interaction_at,
240
+ relationship_type=relationship_type,
241
+ )
242
+
243
+ self._analytics[key] = analytics
244
+ return analytics
@@ -0,0 +1,3 @@
1
+ from .plugin import advanced_memory_plugin, create_advanced_memory_plugin
2
+
3
+ __all__ = ["advanced_memory_plugin", "create_advanced_memory_plugin"]