@elizaos/python 2.0.0-alpha.11 → 2.0.0-alpha.26

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 (57) hide show
  1. package/elizaos/advanced_capabilities/__init__.py +6 -41
  2. package/elizaos/advanced_capabilities/actions/__init__.py +1 -21
  3. package/elizaos/advanced_capabilities/actions/add_contact.py +21 -11
  4. package/elizaos/advanced_capabilities/actions/follow_room.py +28 -28
  5. package/elizaos/advanced_capabilities/actions/image_generation.py +13 -26
  6. package/elizaos/advanced_capabilities/actions/mute_room.py +13 -26
  7. package/elizaos/advanced_capabilities/actions/remove_contact.py +16 -2
  8. package/elizaos/advanced_capabilities/actions/roles.py +13 -27
  9. package/elizaos/advanced_capabilities/actions/search_contacts.py +17 -3
  10. package/elizaos/advanced_capabilities/actions/send_message.py +317 -9
  11. package/elizaos/advanced_capabilities/actions/settings.py +16 -2
  12. package/elizaos/advanced_capabilities/actions/unfollow_room.py +13 -26
  13. package/elizaos/advanced_capabilities/actions/unmute_room.py +13 -26
  14. package/elizaos/advanced_capabilities/actions/update_contact.py +16 -2
  15. package/elizaos/advanced_capabilities/actions/update_entity.py +16 -2
  16. package/elizaos/advanced_capabilities/evaluators/__init__.py +2 -9
  17. package/elizaos/advanced_capabilities/evaluators/reflection.py +3 -132
  18. package/elizaos/advanced_capabilities/evaluators/relationship_extraction.py +5 -201
  19. package/elizaos/advanced_capabilities/providers/__init__.py +1 -12
  20. package/elizaos/advanced_capabilities/providers/knowledge.py +24 -3
  21. package/elizaos/advanced_capabilities/services/__init__.py +2 -9
  22. package/elizaos/advanced_memory/actions/reset_session.py +11 -0
  23. package/elizaos/advanced_memory/evaluators/reflection.py +134 -0
  24. package/elizaos/advanced_memory/evaluators/relationship_extraction.py +203 -0
  25. package/elizaos/advanced_memory/test_advanced_memory.py +357 -0
  26. package/elizaos/advanced_planning/actions/schedule_follow_up.py +222 -0
  27. package/elizaos/basic_capabilities/__init__.py +0 -2
  28. package/elizaos/basic_capabilities/providers/__init__.py +0 -3
  29. package/elizaos/basic_capabilities/providers/agent_settings.py +64 -0
  30. package/elizaos/basic_capabilities/providers/contacts.py +79 -0
  31. package/elizaos/basic_capabilities/providers/facts.py +87 -0
  32. package/elizaos/basic_capabilities/providers/follow_ups.py +117 -0
  33. package/elizaos/basic_capabilities/providers/knowledge.py +97 -0
  34. package/elizaos/basic_capabilities/providers/relationships.py +107 -0
  35. package/elizaos/basic_capabilities/providers/roles.py +96 -0
  36. package/elizaos/basic_capabilities/providers/settings.py +56 -0
  37. package/elizaos/bootstrap/autonomy/__init__.py +5 -1
  38. package/elizaos/bootstrap/autonomy/action.py +161 -0
  39. package/elizaos/bootstrap/autonomy/evaluators.py +217 -0
  40. package/elizaos/bootstrap/autonomy/service.py +8 -0
  41. package/elizaos/bootstrap/plugin.py +7 -0
  42. package/elizaos/bootstrap/providers/knowledge.py +26 -3
  43. package/elizaos/bootstrap/services/embedding.py +156 -1
  44. package/elizaos/runtime.py +63 -18
  45. package/elizaos/services/message_service.py +173 -23
  46. package/elizaos/types/generated/eliza/v1/agent_pb2.py +16 -16
  47. package/elizaos/types/generated/eliza/v1/agent_pb2.pyi +2 -4
  48. package/elizaos/types/model.py +27 -0
  49. package/elizaos/types/runtime.py +5 -1
  50. package/elizaos/utils/validation.py +76 -0
  51. package/package.json +2 -2
  52. package/tests/test_actions_provider_examples.py +58 -1
  53. package/tests/test_async_embedding.py +124 -0
  54. package/tests/test_autonomy.py +13 -2
  55. package/tests/test_validation.py +141 -0
  56. package/tests/verify_memory_architecture.py +192 -0
  57. package/elizaos/basic_capabilities/providers/capabilities.py +0 -62
@@ -1,203 +1,7 @@
1
- from __future__ import annotations
1
+ """Compatibility wrapper for relationship extraction evaluator."""
2
2
 
3
- import re
4
- from collections.abc import Awaitable, Callable
5
- from typing import TYPE_CHECKING
6
-
7
- from elizaos.bootstrap.types import EvaluatorResult
8
- from elizaos.generated.spec_helpers import require_evaluator_spec
9
- from elizaos.types import ActionResult, Evaluator, HandlerOptions
10
-
11
- if TYPE_CHECKING:
12
- from elizaos.types import Content, IAgentRuntime, Memory, State
13
-
14
- # Get text content from centralized specs
15
- _spec = require_evaluator_spec("RELATIONSHIP_EXTRACTION")
16
-
17
- X_HANDLE_PATTERN = re.compile(r"@[\w]+")
18
- EMAIL_PATTERN = re.compile(r"[\w.+-]+@[\w.-]+\.\w+")
19
- PHONE_PATTERN = re.compile(r"\+?[\d\s\-()]{10,}")
20
- DISCORD_PATTERN = re.compile(r"[\w]+#\d{4}")
21
-
22
-
23
- def extract_platform_identities(text: str) -> list[dict[str, str | bool | float]]:
24
- identities: list[dict[str, str | bool | float]] = []
25
-
26
- for match in X_HANDLE_PATTERN.finditer(text):
27
- handle = match.group()
28
- if handle.lower() not in ("@here", "@everyone", "@channel"):
29
- identities.append(
30
- {
31
- "platform": "x",
32
- "handle": handle,
33
- "verified": False,
34
- "confidence": 0.7,
35
- }
36
- )
37
-
38
- for match in EMAIL_PATTERN.finditer(text):
39
- identities.append(
40
- {
41
- "platform": "email",
42
- "handle": match.group(),
43
- "verified": False,
44
- "confidence": 0.9,
45
- }
46
- )
47
-
48
- for match in DISCORD_PATTERN.finditer(text):
49
- identities.append(
50
- {
51
- "platform": "discord",
52
- "handle": match.group(),
53
- "verified": False,
54
- "confidence": 0.8,
55
- }
56
- )
57
-
58
- return identities
59
-
60
-
61
- def detect_relationship_indicators(text: str) -> list[dict[str, str | float]]:
62
- indicators: list[dict[str, str | float]] = []
63
-
64
- friend_patterns = [
65
- r"my friend",
66
- r"good friend",
67
- r"best friend",
68
- r"close friend",
69
- r"we're friends",
70
- ]
71
- for pattern in friend_patterns:
72
- if re.search(pattern, text, re.IGNORECASE):
73
- indicators.append(
74
- {
75
- "type": "friend",
76
- "sentiment": "positive",
77
- "confidence": 0.8,
78
- }
79
- )
80
- break
81
-
82
- colleague_patterns = [
83
- r"my colleague",
84
- r"coworker",
85
- r"co-worker",
86
- r"work together",
87
- r"at work",
88
- ]
89
- for pattern in colleague_patterns:
90
- if re.search(pattern, text, re.IGNORECASE):
91
- indicators.append(
92
- {
93
- "type": "colleague",
94
- "sentiment": "neutral",
95
- "confidence": 0.8,
96
- }
97
- )
98
- break
99
-
100
- family_patterns = [
101
- r"my (brother|sister|mom|dad|mother|father|parent|son|daughter|child)",
102
- r"my family",
103
- r"family member",
104
- ]
105
- for pattern in family_patterns:
106
- if re.search(pattern, text, re.IGNORECASE):
107
- indicators.append(
108
- {
109
- "type": "family",
110
- "sentiment": "positive",
111
- "confidence": 0.9,
112
- }
113
- )
114
- break
115
-
116
- return indicators
117
-
118
-
119
- async def evaluate_relationship_extraction(
120
- runtime: IAgentRuntime,
121
- message: Memory,
122
- state: State | None = None,
123
- ) -> EvaluatorResult:
124
- text = message.content.text if message.content else ""
125
-
126
- if not text:
127
- return EvaluatorResult(
128
- score=50,
129
- passed=True,
130
- reason="No text to analyze",
131
- details={"noText": True},
132
- )
133
-
134
- identities = extract_platform_identities(text)
135
-
136
- indicators = detect_relationship_indicators(text)
137
-
138
- if identities and message.entity_id:
139
- entity = await runtime.get_entity(str(message.entity_id))
140
- if entity:
141
- metadata = entity.metadata or {}
142
- existing_identities = metadata.get("platformIdentities", [])
143
- if isinstance(existing_identities, list):
144
- for identity in identities:
145
- exists = any(
146
- i.get("platform") == identity["platform"]
147
- and i.get("handle") == identity["handle"]
148
- for i in existing_identities
149
- if isinstance(i, dict)
150
- )
151
- if not exists:
152
- existing_identities.append(identity)
153
- metadata["platformIdentities"] = existing_identities
154
- entity.metadata = metadata
155
- await runtime.update_entity(entity)
156
-
157
- runtime.logger.info(
158
- f"Completed extraction: src=evaluator:relationship_extraction agentId={runtime.agent_id} identitiesFound={len(identities)} indicatorsFound={len(indicators)}"
159
- )
160
-
161
- return EvaluatorResult(
162
- score=70,
163
- passed=True,
164
- reason=f"Found {len(identities)} identities and {len(indicators)} relationship indicators",
165
- details={
166
- "identitiesCount": len(identities),
167
- "indicatorsCount": len(indicators),
168
- },
169
- )
170
-
171
-
172
- async def validate_relationship_extraction(
173
- runtime: IAgentRuntime,
174
- message: Memory,
175
- _state: State | None = None,
176
- ) -> bool:
177
- return message.content is not None and bool(message.content.text)
178
-
179
-
180
- async def _relationship_extraction_handler(
181
- runtime: IAgentRuntime,
182
- message: Memory,
183
- state: State | None = None,
184
- options: HandlerOptions | None = None,
185
- callback: Callable[[Content], Awaitable[list[Memory]]] | None = None,
186
- responses: list[Memory] | None = None,
187
- ) -> ActionResult | None:
188
- """Wrapper handler that matches the expected signature."""
189
- _ = options, callback, responses # Unused parameters
190
- result = await evaluate_relationship_extraction(runtime, message, state)
191
- # Return None as ActionResult - evaluators don't typically return action results
192
- return ActionResult(text=result.reason, success=result.passed, values={}, data={})
193
-
194
-
195
- relationship_extraction_evaluator = Evaluator(
196
- name=str(_spec["name"]),
197
- description=str(_spec["description"]),
198
- similes=list(_spec.get("similes", [])) if _spec.get("similes") else [],
199
- validate=validate_relationship_extraction,
200
- handler=_relationship_extraction_handler,
201
- always_run=bool(_spec.get("alwaysRun", False)),
202
- examples=[],
3
+ from elizaos.bootstrap.evaluators.relationship_extraction import (
4
+ relationship_extraction_evaluator,
203
5
  )
6
+
7
+ __all__ = ["relationship_extraction_evaluator"]
@@ -4,32 +4,21 @@ Extended providers that can be enabled with `advanced_capabilities=True`.
4
4
  """
5
5
 
6
6
  from .agent_settings import agent_settings_provider
7
- from .contacts import contacts_provider
8
- from .facts import facts_provider
9
- from .follow_ups import follow_ups_provider
10
7
  from .knowledge import knowledge_provider
11
- from .relationships import relationships_provider
12
8
  from .roles import roles_provider
13
9
  from .settings import settings_provider
14
10
 
15
11
  __all__ = [
16
12
  "agent_settings_provider",
17
- "contacts_provider",
18
- "facts_provider",
19
- "follow_ups_provider",
20
13
  "knowledge_provider",
21
- "relationships_provider",
22
14
  "roles_provider",
23
15
  "settings_provider",
24
16
  "advanced_providers",
25
17
  ]
26
18
 
19
+ # Rolodex/contact providers are provided by plugin-rolodex.
27
20
  advanced_providers = [
28
- contacts_provider,
29
- facts_provider,
30
- follow_ups_provider,
31
21
  knowledge_provider,
32
- relationships_provider,
33
22
  roles_provider,
34
23
  agent_settings_provider,
35
24
  settings_provider,
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from elizaos.types import Provider, ProviderResult
6
+ from elizaos.types.database import MemorySearchOptions
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from elizaos.types import IAgentRuntime, Memory, State
@@ -25,11 +26,31 @@ async def get_knowledge_context(
25
26
  text="", values={"knowledgeCount": 0, "hasKnowledge": False}, data={"entries": []}
26
27
  )
27
28
 
28
- relevant_knowledge = await runtime.search_knowledge(
29
- query=query_text,
30
- limit=5,
29
+ # 1. Fetch recent messages to get embeddings
30
+ recent_messages = await runtime.get_memories(
31
+ room_id=message.room_id, limit=5, table_name="messages"
31
32
  )
32
33
 
34
+ # 2. Extract valid embeddings
35
+ embeddings = [m.embedding for m in recent_messages if m and m.embedding]
36
+
37
+ relevant_knowledge = []
38
+ # 3. Search using the most recent embedding if available
39
+ if embeddings:
40
+ primary_embedding = embeddings[0]
41
+ params = MemorySearchOptions(
42
+ table_name="knowledge",
43
+ room_id=message.room_id,
44
+ embedding=primary_embedding,
45
+ match_threshold=0.75,
46
+ match_count=5,
47
+ unique=True,
48
+ )
49
+ relevant_knowledge = await runtime.search_memories(params)
50
+ elif query_text:
51
+ # Fallback skipped for parity with TS/Bootstrap
52
+ pass
53
+
33
54
  for entry in relevant_knowledge:
34
55
  # Handle both dict and object entries
35
56
  if isinstance(entry, dict):
@@ -3,16 +3,9 @@
3
3
  Services that can be enabled with `advanced_capabilities=True`.
4
4
  """
5
5
 
6
- from .follow_up import FollowUpService
7
- from .rolodex import RolodexService
8
-
9
6
  __all__ = [
10
- "FollowUpService",
11
- "RolodexService",
12
7
  "advanced_services",
13
8
  ]
14
9
 
15
- advanced_services: list[type] = [
16
- RolodexService,
17
- FollowUpService,
18
- ]
10
+ # Rolodex/follow-up services are owned by plugin-rolodex.
11
+ advanced_services: list[type] = []
@@ -0,0 +1,11 @@
1
+ from ...types import Action
2
+
3
+ # TODO: Implement reset_session action
4
+ reset_session_action: Action = {
5
+ "name": "RESET_SESSION",
6
+ "similes": [],
7
+ "description": "Reset the session (Implementation TODO)",
8
+ "handler": None,
9
+ "validate": None,
10
+ "examples": [],
11
+ }
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import TYPE_CHECKING
5
+
6
+ from elizaos.bootstrap.types import EvaluatorResult
7
+ from elizaos.bootstrap.utils.xml import parse_key_value_xml
8
+ from elizaos.generated.spec_helpers import require_evaluator_spec
9
+ from elizaos.prompts import REFLECTION_TEMPLATE
10
+ from elizaos.types import ActionResult, Evaluator, HandlerOptions, ModelType
11
+
12
+ if TYPE_CHECKING:
13
+ from elizaos.types import Content, IAgentRuntime, Memory, State
14
+
15
+ # Get text content from centralized specs
16
+ _spec = require_evaluator_spec("REFLECTION")
17
+
18
+
19
+ async def evaluate_reflection(
20
+ runtime: IAgentRuntime,
21
+ message: Memory,
22
+ state: State | None = None,
23
+ ) -> EvaluatorResult:
24
+ if state is None:
25
+ return EvaluatorResult(
26
+ score=50,
27
+ passed=True,
28
+ reason="No state for reflection",
29
+ details={},
30
+ )
31
+
32
+ recent_interactions: list[str] = []
33
+ room_id = message.room_id
34
+
35
+ if room_id:
36
+ recent_messages = await runtime.get_memories(
37
+ room_id=room_id,
38
+ limit=10,
39
+ order_by="created_at",
40
+ order_direction="desc",
41
+ )
42
+
43
+ for msg in recent_messages:
44
+ if msg.content and msg.content.text:
45
+ sender = "Unknown"
46
+ if msg.entity_id:
47
+ if str(msg.entity_id) == str(runtime.agent_id):
48
+ sender = runtime.character.name
49
+ else:
50
+ entity = await runtime.get_entity(msg.entity_id)
51
+ if entity and entity.name:
52
+ sender = entity.name
53
+ recent_interactions.append(f"{sender}: {msg.content.text}")
54
+
55
+ if not recent_interactions:
56
+ return EvaluatorResult(
57
+ score=50,
58
+ passed=True,
59
+ reason="No recent interactions to reflect on",
60
+ details={"noInteractions": True},
61
+ )
62
+
63
+ interactions_text = "\n".join(recent_interactions)
64
+
65
+ template = (
66
+ runtime.character.templates.get("reflectionTemplate")
67
+ if runtime.character.templates and "reflectionTemplate" in runtime.character.templates
68
+ else REFLECTION_TEMPLATE
69
+ )
70
+ prompt = runtime.compose_prompt(state=state, template=template)
71
+ prompt = prompt.replace("{{recentInteractions}}", interactions_text)
72
+
73
+ response_text = await runtime.use_model(ModelType.TEXT_LARGE, prompt=prompt)
74
+ parsed_xml = parse_key_value_xml(response_text)
75
+
76
+ if parsed_xml is None:
77
+ raise ValueError("Failed to parse reflection response")
78
+
79
+ quality_str = str(parsed_xml.get("quality_score", "50"))
80
+ quality_score = max(0, min(100, int(quality_str)))
81
+
82
+ thought = str(parsed_xml.get("thought", ""))
83
+ strengths = str(parsed_xml.get("strengths", ""))
84
+ improvements = str(parsed_xml.get("improvements", ""))
85
+ learnings = str(parsed_xml.get("learnings", ""))
86
+
87
+ passed = quality_score >= 50
88
+
89
+ return EvaluatorResult(
90
+ score=quality_score,
91
+ passed=passed,
92
+ reason=f"Strengths: {strengths}\nImprovements: {improvements}",
93
+ details={
94
+ "thought": thought,
95
+ "strengths": strengths,
96
+ "improvements": improvements,
97
+ "learnings": learnings,
98
+ "interactionCount": len(recent_interactions),
99
+ },
100
+ )
101
+
102
+
103
+ async def validate_reflection(
104
+ runtime: IAgentRuntime,
105
+ message: Memory,
106
+ _state: State | None = None,
107
+ ) -> bool:
108
+ return True
109
+
110
+
111
+ async def _reflection_handler(
112
+ runtime: IAgentRuntime,
113
+ message: Memory,
114
+ state: State | None = None,
115
+ options: HandlerOptions | None = None,
116
+ callback: Callable[[Content], Awaitable[list[Memory]]] | None = None,
117
+ responses: list[Memory] | None = None,
118
+ ) -> ActionResult | None:
119
+ """Wrapper handler that matches the expected signature."""
120
+ _ = options, callback, responses # Unused parameters
121
+ result = await evaluate_reflection(runtime, message, state)
122
+ # Return ActionResult - evaluators don't typically return action results
123
+ return ActionResult(text=result.reason, success=result.passed, values={}, data={})
124
+
125
+
126
+ reflection_evaluator = Evaluator(
127
+ name=str(_spec["name"]),
128
+ description=str(_spec["description"]),
129
+ similes=list(_spec.get("similes", [])) if _spec.get("similes") else [],
130
+ validate=validate_reflection,
131
+ handler=_reflection_handler,
132
+ always_run=bool(_spec.get("alwaysRun", False)),
133
+ examples=[],
134
+ )
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import TYPE_CHECKING
6
+
7
+ from elizaos.bootstrap.types import EvaluatorResult
8
+ from elizaos.generated.spec_helpers import require_evaluator_spec
9
+ from elizaos.types import ActionResult, Evaluator, HandlerOptions
10
+
11
+ if TYPE_CHECKING:
12
+ from elizaos.types import Content, IAgentRuntime, Memory, State
13
+
14
+ # Get text content from centralized specs
15
+ _spec = require_evaluator_spec("RELATIONSHIP_EXTRACTION")
16
+
17
+ X_HANDLE_PATTERN = re.compile(r"@[\w]+")
18
+ EMAIL_PATTERN = re.compile(r"[\w.+-]+@[\w.-]+\.\w+")
19
+ PHONE_PATTERN = re.compile(r"\+?[\d\s\-()]{10,}")
20
+ DISCORD_PATTERN = re.compile(r"[\w]+#\d{4}")
21
+
22
+
23
+ def extract_platform_identities(text: str) -> list[dict[str, str | bool | float]]:
24
+ identities: list[dict[str, str | bool | float]] = []
25
+
26
+ for match in X_HANDLE_PATTERN.finditer(text):
27
+ handle = match.group()
28
+ if handle.lower() not in ("@here", "@everyone", "@channel"):
29
+ identities.append(
30
+ {
31
+ "platform": "x",
32
+ "handle": handle,
33
+ "verified": False,
34
+ "confidence": 0.7,
35
+ }
36
+ )
37
+
38
+ for match in EMAIL_PATTERN.finditer(text):
39
+ identities.append(
40
+ {
41
+ "platform": "email",
42
+ "handle": match.group(),
43
+ "verified": False,
44
+ "confidence": 0.9,
45
+ }
46
+ )
47
+
48
+ for match in DISCORD_PATTERN.finditer(text):
49
+ identities.append(
50
+ {
51
+ "platform": "discord",
52
+ "handle": match.group(),
53
+ "verified": False,
54
+ "confidence": 0.8,
55
+ }
56
+ )
57
+
58
+ return identities
59
+
60
+
61
+ def detect_relationship_indicators(text: str) -> list[dict[str, str | float]]:
62
+ indicators: list[dict[str, str | float]] = []
63
+
64
+ friend_patterns = [
65
+ r"my friend",
66
+ r"good friend",
67
+ r"best friend",
68
+ r"close friend",
69
+ r"we're friends",
70
+ ]
71
+ for pattern in friend_patterns:
72
+ if re.search(pattern, text, re.IGNORECASE):
73
+ indicators.append(
74
+ {
75
+ "type": "friend",
76
+ "sentiment": "positive",
77
+ "confidence": 0.8,
78
+ }
79
+ )
80
+ break
81
+
82
+ colleague_patterns = [
83
+ r"my colleague",
84
+ r"coworker",
85
+ r"co-worker",
86
+ r"work together",
87
+ r"at work",
88
+ ]
89
+ for pattern in colleague_patterns:
90
+ if re.search(pattern, text, re.IGNORECASE):
91
+ indicators.append(
92
+ {
93
+ "type": "colleague",
94
+ "sentiment": "neutral",
95
+ "confidence": 0.8,
96
+ }
97
+ )
98
+ break
99
+
100
+ family_patterns = [
101
+ r"my (brother|sister|mom|dad|mother|father|parent|son|daughter|child)",
102
+ r"my family",
103
+ r"family member",
104
+ ]
105
+ for pattern in family_patterns:
106
+ if re.search(pattern, text, re.IGNORECASE):
107
+ indicators.append(
108
+ {
109
+ "type": "family",
110
+ "sentiment": "positive",
111
+ "confidence": 0.9,
112
+ }
113
+ )
114
+ break
115
+
116
+ return indicators
117
+
118
+
119
+ async def evaluate_relationship_extraction(
120
+ runtime: IAgentRuntime,
121
+ message: Memory,
122
+ state: State | None = None,
123
+ ) -> EvaluatorResult:
124
+ text = message.content.text if message.content else ""
125
+
126
+ if not text:
127
+ return EvaluatorResult(
128
+ score=50,
129
+ passed=True,
130
+ reason="No text to analyze",
131
+ details={"noText": True},
132
+ )
133
+
134
+ identities = extract_platform_identities(text)
135
+
136
+ indicators = detect_relationship_indicators(text)
137
+
138
+ if identities and message.entity_id:
139
+ entity = await runtime.get_entity(str(message.entity_id))
140
+ if entity:
141
+ metadata = entity.metadata or {}
142
+ existing_identities = metadata.get("platformIdentities", [])
143
+ if isinstance(existing_identities, list):
144
+ for identity in identities:
145
+ exists = any(
146
+ i.get("platform") == identity["platform"]
147
+ and i.get("handle") == identity["handle"]
148
+ for i in existing_identities
149
+ if isinstance(i, dict)
150
+ )
151
+ if not exists:
152
+ existing_identities.append(identity)
153
+ metadata["platformIdentities"] = existing_identities
154
+ entity.metadata = metadata
155
+ await runtime.update_entity(entity)
156
+
157
+ runtime.logger.info(
158
+ f"Completed extraction: src=evaluator:relationship_extraction agentId={runtime.agent_id} identitiesFound={len(identities)} indicatorsFound={len(indicators)}"
159
+ )
160
+
161
+ return EvaluatorResult(
162
+ score=70,
163
+ passed=True,
164
+ reason=f"Found {len(identities)} identities and {len(indicators)} relationship indicators",
165
+ details={
166
+ "identitiesCount": len(identities),
167
+ "indicatorsCount": len(indicators),
168
+ },
169
+ )
170
+
171
+
172
+ async def validate_relationship_extraction(
173
+ runtime: IAgentRuntime,
174
+ message: Memory,
175
+ _state: State | None = None,
176
+ ) -> bool:
177
+ return message.content is not None and bool(message.content.text)
178
+
179
+
180
+ async def _relationship_extraction_handler(
181
+ runtime: IAgentRuntime,
182
+ message: Memory,
183
+ state: State | None = None,
184
+ options: HandlerOptions | None = None,
185
+ callback: Callable[[Content], Awaitable[list[Memory]]] | None = None,
186
+ responses: list[Memory] | None = None,
187
+ ) -> ActionResult | None:
188
+ """Wrapper handler that matches the expected signature."""
189
+ _ = options, callback, responses # Unused parameters
190
+ result = await evaluate_relationship_extraction(runtime, message, state)
191
+ # Return None as ActionResult - evaluators don't typically return action results
192
+ return ActionResult(text=result.reason, success=result.passed, values={}, data={})
193
+
194
+
195
+ relationship_extraction_evaluator = Evaluator(
196
+ name=str(_spec["name"]),
197
+ description=str(_spec["description"]),
198
+ similes=list(_spec.get("similes", [])) if _spec.get("similes") else [],
199
+ validate=validate_relationship_extraction,
200
+ handler=_relationship_extraction_handler,
201
+ always_run=bool(_spec.get("alwaysRun", False)),
202
+ examples=[],
203
+ )