agno 2.3.24__py3-none-any.whl → 2.3.26__py3-none-any.whl
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.
- agno/agent/agent.py +357 -28
- agno/db/base.py +214 -0
- agno/db/dynamo/dynamo.py +47 -0
- agno/db/firestore/firestore.py +47 -0
- agno/db/gcs_json/gcs_json_db.py +47 -0
- agno/db/in_memory/in_memory_db.py +47 -0
- agno/db/json/json_db.py +47 -0
- agno/db/mongo/async_mongo.py +229 -0
- agno/db/mongo/mongo.py +47 -0
- agno/db/mongo/schemas.py +16 -0
- agno/db/mysql/async_mysql.py +47 -0
- agno/db/mysql/mysql.py +47 -0
- agno/db/postgres/async_postgres.py +231 -0
- agno/db/postgres/postgres.py +239 -0
- agno/db/postgres/schemas.py +19 -0
- agno/db/redis/redis.py +47 -0
- agno/db/singlestore/singlestore.py +47 -0
- agno/db/sqlite/async_sqlite.py +242 -0
- agno/db/sqlite/schemas.py +18 -0
- agno/db/sqlite/sqlite.py +239 -0
- agno/db/surrealdb/surrealdb.py +47 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +62 -2
- agno/knowledge/chunking/strategy.py +14 -0
- agno/knowledge/knowledge.py +7 -1
- agno/knowledge/reader/arxiv_reader.py +1 -0
- agno/knowledge/reader/csv_reader.py +1 -0
- agno/knowledge/reader/docx_reader.py +1 -0
- agno/knowledge/reader/firecrawl_reader.py +1 -0
- agno/knowledge/reader/json_reader.py +1 -0
- agno/knowledge/reader/markdown_reader.py +1 -0
- agno/knowledge/reader/pdf_reader.py +1 -0
- agno/knowledge/reader/pptx_reader.py +1 -0
- agno/knowledge/reader/s3_reader.py +1 -0
- agno/knowledge/reader/tavily_reader.py +1 -0
- agno/knowledge/reader/text_reader.py +1 -0
- agno/knowledge/reader/web_search_reader.py +1 -0
- agno/knowledge/reader/website_reader.py +1 -0
- agno/knowledge/reader/wikipedia_reader.py +1 -0
- agno/knowledge/reader/youtube_reader.py +1 -0
- agno/knowledge/utils.py +1 -0
- agno/learn/__init__.py +65 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +690 -0
- agno/learn/schemas.py +1043 -0
- agno/learn/stores/__init__.py +35 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/models/base.py +59 -0
- agno/os/routers/agents/router.py +4 -4
- agno/os/routers/knowledge/knowledge.py +7 -0
- agno/os/routers/teams/router.py +3 -3
- agno/os/routers/workflows/router.py +5 -5
- agno/os/utils.py +55 -3
- agno/team/team.py +131 -0
- agno/tools/browserbase.py +78 -6
- agno/tools/google_bigquery.py +11 -2
- agno/utils/agent.py +30 -1
- agno/workflow/workflow.py +198 -0
- {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/METADATA +24 -2
- {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/RECORD +70 -56
- {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/WHEEL +0 -0
- {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,3275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Entity Memory Store
|
|
3
|
+
===================
|
|
4
|
+
Storage backend for Entity Memory learning type.
|
|
5
|
+
|
|
6
|
+
Stores knowledge about external entities - people, companies, projects, products,
|
|
7
|
+
concepts, systems, and any other things the agent interacts with that aren't the
|
|
8
|
+
user themselves.
|
|
9
|
+
|
|
10
|
+
Think of it as:
|
|
11
|
+
- UserProfile = what you know about THE USER
|
|
12
|
+
- EntityMemory = what you know about EVERYTHING ELSE
|
|
13
|
+
|
|
14
|
+
Key Features:
|
|
15
|
+
- Entity-scoped storage (entity_id + entity_type)
|
|
16
|
+
- Three types of memory per entity:
|
|
17
|
+
- Facts (semantic): Timeless truths ("Acme uses PostgreSQL")
|
|
18
|
+
- Events (episodic): Time-bound occurrences ("Acme launched v2 on Jan 15")
|
|
19
|
+
- Relationships (graph): Connections to other entities ("Bob is CEO of Acme")
|
|
20
|
+
- Namespace-based sharing control
|
|
21
|
+
- Agent tools for CRUD operations
|
|
22
|
+
- Background extraction from conversations
|
|
23
|
+
|
|
24
|
+
Scoping:
|
|
25
|
+
- entity_id: Unique identifier (e.g., "acme_corp", "bob_smith")
|
|
26
|
+
- entity_type: Category (e.g., "company", "person", "project", "product")
|
|
27
|
+
- namespace: Sharing scope:
|
|
28
|
+
- "user": Private to current user
|
|
29
|
+
- "global": Shared with everyone (default)
|
|
30
|
+
- "<custom>": Custom grouping (e.g., "sales_team")
|
|
31
|
+
|
|
32
|
+
Supported Modes:
|
|
33
|
+
- ALWAYS: Automatic extraction of entity info from conversations
|
|
34
|
+
- AGENTIC: Agent calls tools directly to manage entity info
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from copy import deepcopy
|
|
38
|
+
from dataclasses import dataclass, field
|
|
39
|
+
from datetime import datetime, timezone
|
|
40
|
+
from os import getenv
|
|
41
|
+
from textwrap import dedent
|
|
42
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
43
|
+
|
|
44
|
+
from agno.learn.config import EntityMemoryConfig, LearningMode
|
|
45
|
+
from agno.learn.schemas import EntityMemory
|
|
46
|
+
from agno.learn.stores.protocol import LearningStore
|
|
47
|
+
from agno.utils.log import (
|
|
48
|
+
log_debug,
|
|
49
|
+
log_warning,
|
|
50
|
+
set_log_level_to_debug,
|
|
51
|
+
set_log_level_to_info,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
56
|
+
from agno.models.message import Message
|
|
57
|
+
except ImportError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class EntityMemoryStore(LearningStore):
|
|
63
|
+
"""Storage backend for Entity Memory learning type.
|
|
64
|
+
|
|
65
|
+
Stores knowledge about external entities with three types of memory:
|
|
66
|
+
- **Facts**: Semantic memory - timeless truths about the entity
|
|
67
|
+
- **Events**: Episodic memory - time-bound occurrences
|
|
68
|
+
- **Relationships**: Graph edges - connections to other entities
|
|
69
|
+
|
|
70
|
+
Each entity is identified by entity_id + entity_type, with namespace for sharing.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
config: EntityMemoryConfig with all settings including db and model.
|
|
74
|
+
debug_mode: Enable debug logging.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
config: EntityMemoryConfig = field(default_factory=EntityMemoryConfig)
|
|
78
|
+
debug_mode: bool = False
|
|
79
|
+
|
|
80
|
+
# State tracking (internal)
|
|
81
|
+
entity_updated: bool = field(default=False, init=False)
|
|
82
|
+
_schema: Any = field(default=None, init=False)
|
|
83
|
+
|
|
84
|
+
def __post_init__(self):
|
|
85
|
+
self._schema = self.config.schema or EntityMemory
|
|
86
|
+
|
|
87
|
+
if self.config.mode == LearningMode.PROPOSE:
|
|
88
|
+
log_warning("EntityMemoryStore does not support PROPOSE mode. Falling back to ALWAYS mode.")
|
|
89
|
+
elif self.config.mode == LearningMode.HITL:
|
|
90
|
+
log_warning("EntityMemoryStore does not support HITL mode. Falling back to ALWAYS mode.")
|
|
91
|
+
|
|
92
|
+
# =========================================================================
|
|
93
|
+
# LearningStore Protocol Implementation
|
|
94
|
+
# =========================================================================
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def learning_type(self) -> str:
|
|
98
|
+
"""Unique identifier for this learning type."""
|
|
99
|
+
return "entity_memory"
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def schema(self) -> Any:
|
|
103
|
+
"""Schema class used for entities."""
|
|
104
|
+
return self._schema
|
|
105
|
+
|
|
106
|
+
def recall(
|
|
107
|
+
self,
|
|
108
|
+
entity_id: Optional[str] = None,
|
|
109
|
+
entity_type: Optional[str] = None,
|
|
110
|
+
user_id: Optional[str] = None,
|
|
111
|
+
namespace: Optional[str] = None,
|
|
112
|
+
**kwargs,
|
|
113
|
+
) -> Optional[Any]:
|
|
114
|
+
"""Retrieve entity memory from storage.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
entity_id: The entity to retrieve (required with entity_type).
|
|
118
|
+
entity_type: The type of entity (required with entity_id).
|
|
119
|
+
user_id: User ID for "user" namespace scoping.
|
|
120
|
+
namespace: Filter by namespace.
|
|
121
|
+
**kwargs: Additional context (ignored).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Entity memory, or None if not found.
|
|
125
|
+
"""
|
|
126
|
+
if not entity_id or not entity_type:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
effective_namespace = namespace or self.config.namespace
|
|
130
|
+
if effective_namespace == "user" and not user_id:
|
|
131
|
+
log_warning("EntityMemoryStore.process: namespace='user' requires user_id")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
return self.get(
|
|
135
|
+
entity_id=entity_id,
|
|
136
|
+
entity_type=entity_type,
|
|
137
|
+
user_id=user_id,
|
|
138
|
+
namespace=effective_namespace,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async def arecall(
|
|
142
|
+
self,
|
|
143
|
+
entity_id: Optional[str] = None,
|
|
144
|
+
entity_type: Optional[str] = None,
|
|
145
|
+
user_id: Optional[str] = None,
|
|
146
|
+
namespace: Optional[str] = None,
|
|
147
|
+
**kwargs,
|
|
148
|
+
) -> Optional[Any]:
|
|
149
|
+
"""Async version of recall."""
|
|
150
|
+
if not entity_id or not entity_type:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
effective_namespace = namespace or self.config.namespace
|
|
154
|
+
if effective_namespace == "user" and not user_id:
|
|
155
|
+
log_warning("EntityMemoryStore.arecall: namespace='user' requires user_id")
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
return await self.aget(
|
|
159
|
+
entity_id=entity_id,
|
|
160
|
+
entity_type=entity_type,
|
|
161
|
+
user_id=user_id,
|
|
162
|
+
namespace=effective_namespace,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def process(
|
|
166
|
+
self,
|
|
167
|
+
messages: List[Any],
|
|
168
|
+
user_id: Optional[str] = None,
|
|
169
|
+
agent_id: Optional[str] = None,
|
|
170
|
+
team_id: Optional[str] = None,
|
|
171
|
+
namespace: Optional[str] = None,
|
|
172
|
+
**kwargs,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Extract entity information from messages.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
messages: Conversation messages to analyze.
|
|
178
|
+
user_id: User context (for "user" namespace scoping).
|
|
179
|
+
agent_id: Agent context (stored for audit).
|
|
180
|
+
team_id: Team context (stored for audit).
|
|
181
|
+
namespace: Namespace to save entities to.
|
|
182
|
+
**kwargs: Additional context (ignored).
|
|
183
|
+
"""
|
|
184
|
+
if self.config.mode == LearningMode.AGENTIC:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
if not messages:
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
effective_namespace = namespace or self.config.namespace
|
|
191
|
+
self.extract_and_save(
|
|
192
|
+
messages=messages,
|
|
193
|
+
user_id=user_id,
|
|
194
|
+
agent_id=agent_id,
|
|
195
|
+
team_id=team_id,
|
|
196
|
+
namespace=effective_namespace,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
async def aprocess(
|
|
200
|
+
self,
|
|
201
|
+
messages: List[Any],
|
|
202
|
+
user_id: Optional[str] = None,
|
|
203
|
+
agent_id: Optional[str] = None,
|
|
204
|
+
team_id: Optional[str] = None,
|
|
205
|
+
namespace: Optional[str] = None,
|
|
206
|
+
**kwargs,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Async version of process."""
|
|
209
|
+
if self.config.mode == LearningMode.AGENTIC:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if not messages:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
effective_namespace = namespace or self.config.namespace
|
|
216
|
+
await self.aextract_and_save(
|
|
217
|
+
messages=messages,
|
|
218
|
+
user_id=user_id,
|
|
219
|
+
agent_id=agent_id,
|
|
220
|
+
team_id=team_id,
|
|
221
|
+
namespace=effective_namespace,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def build_context(self, data: Any) -> str:
|
|
225
|
+
"""Build context for the agent.
|
|
226
|
+
|
|
227
|
+
Formats entity memory for injection into the agent's system prompt.
|
|
228
|
+
Entity memory provides knowledge about external things - people, companies,
|
|
229
|
+
projects, products - distinct from knowledge about the user themselves.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
data: Entity memory data from recall() - single entity or list.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Context string to inject into the agent's system prompt.
|
|
236
|
+
"""
|
|
237
|
+
if not data:
|
|
238
|
+
if self._should_expose_tools:
|
|
239
|
+
return dedent("""\
|
|
240
|
+
<entity_memory_system>
|
|
241
|
+
You have access to entity memory - a knowledge base about people, companies,
|
|
242
|
+
projects, products, and other external entities relevant to your work.
|
|
243
|
+
|
|
244
|
+
**Available Tools:**
|
|
245
|
+
- `search_entities`: Find stored information about entities
|
|
246
|
+
- `create_entity`: Store a new entity with its facts
|
|
247
|
+
- `add_fact`: Add a timeless truth about an entity
|
|
248
|
+
- `add_event`: Record a time-bound occurrence
|
|
249
|
+
- `add_relationship`: Capture connections between entities
|
|
250
|
+
|
|
251
|
+
**When to use entity memory:**
|
|
252
|
+
- You learn something substantive about a company, person, or project
|
|
253
|
+
- Information would be useful to recall in future conversations
|
|
254
|
+
- Facts are stable enough to be worth storing
|
|
255
|
+
|
|
256
|
+
**Entity memory vs other memory types:**
|
|
257
|
+
- User memory = about THE USER (their preferences, role, context)
|
|
258
|
+
- Entity memory = about EXTERNAL THINGS (companies, people, projects)
|
|
259
|
+
- Learned knowledge = reusable TASK insights (patterns, approaches)
|
|
260
|
+
</entity_memory_system>""")
|
|
261
|
+
return ""
|
|
262
|
+
|
|
263
|
+
# Handle single entity or list
|
|
264
|
+
entities = data if isinstance(data, list) else [data]
|
|
265
|
+
if not entities:
|
|
266
|
+
return ""
|
|
267
|
+
|
|
268
|
+
# Use schema's get_context_text
|
|
269
|
+
formatted_parts = []
|
|
270
|
+
for entity in entities:
|
|
271
|
+
if hasattr(entity, "get_context_text"):
|
|
272
|
+
formatted_parts.append(entity.get_context_text())
|
|
273
|
+
else:
|
|
274
|
+
formatted_parts.append(self._format_entity_basic(entity=entity))
|
|
275
|
+
|
|
276
|
+
formatted = "\n\n---\n\n".join(formatted_parts)
|
|
277
|
+
|
|
278
|
+
context = dedent(f"""\
|
|
279
|
+
<entity_memory>
|
|
280
|
+
**Known information about relevant entities:**
|
|
281
|
+
|
|
282
|
+
{formatted}
|
|
283
|
+
|
|
284
|
+
<entity_memory_guidelines>
|
|
285
|
+
Use this knowledge naturally in your responses:
|
|
286
|
+
- Reference stored facts without citing "entity memory"
|
|
287
|
+
- Treat this as background knowledge you simply have
|
|
288
|
+
- Current conversation takes precedence if there's conflicting information
|
|
289
|
+
- Update entity memory if you learn something new and substantive
|
|
290
|
+
</entity_memory_guidelines>
|
|
291
|
+
""")
|
|
292
|
+
|
|
293
|
+
if self._should_expose_tools:
|
|
294
|
+
context += dedent("""
|
|
295
|
+
Entity memory tools are available to search, create, or update entities.
|
|
296
|
+
</entity_memory>""")
|
|
297
|
+
else:
|
|
298
|
+
context += "</entity_memory>"
|
|
299
|
+
|
|
300
|
+
return context
|
|
301
|
+
|
|
302
|
+
def get_tools(
|
|
303
|
+
self,
|
|
304
|
+
user_id: Optional[str] = None,
|
|
305
|
+
agent_id: Optional[str] = None,
|
|
306
|
+
team_id: Optional[str] = None,
|
|
307
|
+
namespace: Optional[str] = None,
|
|
308
|
+
**kwargs,
|
|
309
|
+
) -> List[Callable]:
|
|
310
|
+
"""Get tools to expose to agent.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
user_id: User context (for "user" namespace scoping).
|
|
314
|
+
agent_id: Agent context (stored for audit).
|
|
315
|
+
team_id: Team context (stored for audit).
|
|
316
|
+
namespace: Default namespace for operations.
|
|
317
|
+
**kwargs: Additional context (ignored).
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
List of callable tools (empty if enable_agent_tools=False).
|
|
321
|
+
"""
|
|
322
|
+
if not self._should_expose_tools:
|
|
323
|
+
return []
|
|
324
|
+
return self.get_agent_tools(
|
|
325
|
+
user_id=user_id,
|
|
326
|
+
agent_id=agent_id,
|
|
327
|
+
team_id=team_id,
|
|
328
|
+
namespace=namespace,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
async def aget_tools(
|
|
332
|
+
self,
|
|
333
|
+
user_id: Optional[str] = None,
|
|
334
|
+
agent_id: Optional[str] = None,
|
|
335
|
+
team_id: Optional[str] = None,
|
|
336
|
+
namespace: Optional[str] = None,
|
|
337
|
+
**kwargs,
|
|
338
|
+
) -> List[Callable]:
|
|
339
|
+
"""Async version of get_tools."""
|
|
340
|
+
if not self._should_expose_tools:
|
|
341
|
+
return []
|
|
342
|
+
return await self.aget_agent_tools(
|
|
343
|
+
user_id=user_id,
|
|
344
|
+
agent_id=agent_id,
|
|
345
|
+
team_id=team_id,
|
|
346
|
+
namespace=namespace,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def was_updated(self) -> bool:
|
|
351
|
+
"""Check if entity was updated in last operation."""
|
|
352
|
+
return self.entity_updated
|
|
353
|
+
|
|
354
|
+
@property
|
|
355
|
+
def _should_expose_tools(self) -> bool:
|
|
356
|
+
"""Check if tools should be exposed to the agent.
|
|
357
|
+
|
|
358
|
+
Returns True if either:
|
|
359
|
+
- mode is AGENTIC (tools are the primary way to manage entities), OR
|
|
360
|
+
- enable_agent_tools is explicitly True
|
|
361
|
+
"""
|
|
362
|
+
return self.config.mode == LearningMode.AGENTIC or self.config.enable_agent_tools
|
|
363
|
+
|
|
364
|
+
# =========================================================================
|
|
365
|
+
# Properties
|
|
366
|
+
# =========================================================================
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def db(self) -> Optional[Union["BaseDb", "AsyncBaseDb"]]:
|
|
370
|
+
"""Database backend."""
|
|
371
|
+
return self.config.db
|
|
372
|
+
|
|
373
|
+
@property
|
|
374
|
+
def model(self):
|
|
375
|
+
"""Model for extraction."""
|
|
376
|
+
return self.config.model
|
|
377
|
+
|
|
378
|
+
# =========================================================================
|
|
379
|
+
# Debug/Logging
|
|
380
|
+
# =========================================================================
|
|
381
|
+
|
|
382
|
+
def set_log_level(self):
|
|
383
|
+
"""Set log level based on debug_mode or environment variable."""
|
|
384
|
+
if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true":
|
|
385
|
+
self.debug_mode = True
|
|
386
|
+
set_log_level_to_debug()
|
|
387
|
+
else:
|
|
388
|
+
set_log_level_to_info()
|
|
389
|
+
|
|
390
|
+
# =========================================================================
|
|
391
|
+
# Agent Tools
|
|
392
|
+
# =========================================================================
|
|
393
|
+
|
|
394
|
+
def get_agent_tools(
|
|
395
|
+
self,
|
|
396
|
+
user_id: Optional[str] = None,
|
|
397
|
+
agent_id: Optional[str] = None,
|
|
398
|
+
team_id: Optional[str] = None,
|
|
399
|
+
namespace: Optional[str] = None,
|
|
400
|
+
) -> List[Callable]:
|
|
401
|
+
"""Get the tools to expose to the agent.
|
|
402
|
+
|
|
403
|
+
Tools are included based on config settings:
|
|
404
|
+
- search_entities (agent_can_search_entities)
|
|
405
|
+
- create_entity (agent_can_create_entity)
|
|
406
|
+
- update_entity (agent_can_update_entity)
|
|
407
|
+
- add_fact, update_fact, delete_fact
|
|
408
|
+
- add_event
|
|
409
|
+
- add_relationship
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
user_id: User context (for "user" namespace scoping).
|
|
413
|
+
agent_id: Agent context (stored for audit).
|
|
414
|
+
team_id: Team context (stored for audit).
|
|
415
|
+
namespace: Default namespace for operations.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
List of callable tools.
|
|
419
|
+
"""
|
|
420
|
+
tools = []
|
|
421
|
+
effective_namespace = namespace or self.config.namespace
|
|
422
|
+
|
|
423
|
+
if self.config.agent_can_search_entities:
|
|
424
|
+
tools.append(
|
|
425
|
+
self._create_search_entities_tool(
|
|
426
|
+
user_id=user_id,
|
|
427
|
+
namespace=effective_namespace,
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if self.config.agent_can_create_entity:
|
|
432
|
+
tools.append(
|
|
433
|
+
self._create_create_entity_tool(
|
|
434
|
+
user_id=user_id,
|
|
435
|
+
agent_id=agent_id,
|
|
436
|
+
team_id=team_id,
|
|
437
|
+
namespace=effective_namespace,
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
if self.config.agent_can_update_entity:
|
|
442
|
+
tools.append(
|
|
443
|
+
self._create_update_entity_tool(
|
|
444
|
+
user_id=user_id,
|
|
445
|
+
agent_id=agent_id,
|
|
446
|
+
team_id=team_id,
|
|
447
|
+
namespace=effective_namespace,
|
|
448
|
+
)
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if self.config.enable_add_fact:
|
|
452
|
+
tools.append(
|
|
453
|
+
self._create_add_fact_tool(
|
|
454
|
+
user_id=user_id,
|
|
455
|
+
agent_id=agent_id,
|
|
456
|
+
team_id=team_id,
|
|
457
|
+
namespace=effective_namespace,
|
|
458
|
+
)
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
if self.config.enable_update_fact:
|
|
462
|
+
tools.append(
|
|
463
|
+
self._create_update_fact_tool(
|
|
464
|
+
user_id=user_id,
|
|
465
|
+
agent_id=agent_id,
|
|
466
|
+
team_id=team_id,
|
|
467
|
+
namespace=effective_namespace,
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
if self.config.enable_delete_fact:
|
|
472
|
+
tools.append(
|
|
473
|
+
self._create_delete_fact_tool(
|
|
474
|
+
user_id=user_id,
|
|
475
|
+
agent_id=agent_id,
|
|
476
|
+
team_id=team_id,
|
|
477
|
+
namespace=effective_namespace,
|
|
478
|
+
)
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
if self.config.enable_add_event:
|
|
482
|
+
tools.append(
|
|
483
|
+
self._create_add_event_tool(
|
|
484
|
+
user_id=user_id,
|
|
485
|
+
agent_id=agent_id,
|
|
486
|
+
team_id=team_id,
|
|
487
|
+
namespace=effective_namespace,
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
if self.config.enable_add_relationship:
|
|
492
|
+
tools.append(
|
|
493
|
+
self._create_add_relationship_tool(
|
|
494
|
+
user_id=user_id,
|
|
495
|
+
agent_id=agent_id,
|
|
496
|
+
team_id=team_id,
|
|
497
|
+
namespace=effective_namespace,
|
|
498
|
+
)
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
return tools
|
|
502
|
+
|
|
503
|
+
async def aget_agent_tools(
|
|
504
|
+
self,
|
|
505
|
+
user_id: Optional[str] = None,
|
|
506
|
+
agent_id: Optional[str] = None,
|
|
507
|
+
team_id: Optional[str] = None,
|
|
508
|
+
namespace: Optional[str] = None,
|
|
509
|
+
) -> List[Callable]:
|
|
510
|
+
"""Async version of get_agent_tools."""
|
|
511
|
+
tools = []
|
|
512
|
+
effective_namespace = namespace or self.config.namespace
|
|
513
|
+
|
|
514
|
+
if self.config.agent_can_search_entities:
|
|
515
|
+
tools.append(
|
|
516
|
+
self._create_async_search_entities_tool(
|
|
517
|
+
user_id=user_id,
|
|
518
|
+
namespace=effective_namespace,
|
|
519
|
+
)
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
if self.config.agent_can_create_entity:
|
|
523
|
+
tools.append(
|
|
524
|
+
self._create_async_create_entity_tool(
|
|
525
|
+
user_id=user_id,
|
|
526
|
+
agent_id=agent_id,
|
|
527
|
+
team_id=team_id,
|
|
528
|
+
namespace=effective_namespace,
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
if self.config.agent_can_update_entity:
|
|
533
|
+
tools.append(
|
|
534
|
+
self._create_async_update_entity_tool(
|
|
535
|
+
user_id=user_id,
|
|
536
|
+
agent_id=agent_id,
|
|
537
|
+
team_id=team_id,
|
|
538
|
+
namespace=effective_namespace,
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
if self.config.enable_add_fact:
|
|
543
|
+
tools.append(
|
|
544
|
+
self._create_async_add_fact_tool(
|
|
545
|
+
user_id=user_id,
|
|
546
|
+
agent_id=agent_id,
|
|
547
|
+
team_id=team_id,
|
|
548
|
+
namespace=effective_namespace,
|
|
549
|
+
)
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
if self.config.enable_update_fact:
|
|
553
|
+
tools.append(
|
|
554
|
+
self._create_async_update_fact_tool(
|
|
555
|
+
user_id=user_id,
|
|
556
|
+
agent_id=agent_id,
|
|
557
|
+
team_id=team_id,
|
|
558
|
+
namespace=effective_namespace,
|
|
559
|
+
)
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
if self.config.enable_delete_fact:
|
|
563
|
+
tools.append(
|
|
564
|
+
self._create_async_delete_fact_tool(
|
|
565
|
+
user_id=user_id,
|
|
566
|
+
agent_id=agent_id,
|
|
567
|
+
team_id=team_id,
|
|
568
|
+
namespace=effective_namespace,
|
|
569
|
+
)
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
if self.config.enable_add_event:
|
|
573
|
+
tools.append(
|
|
574
|
+
self._create_async_add_event_tool(
|
|
575
|
+
user_id=user_id,
|
|
576
|
+
agent_id=agent_id,
|
|
577
|
+
team_id=team_id,
|
|
578
|
+
namespace=effective_namespace,
|
|
579
|
+
)
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
if self.config.enable_add_relationship:
|
|
583
|
+
tools.append(
|
|
584
|
+
self._create_async_add_relationship_tool(
|
|
585
|
+
user_id=user_id,
|
|
586
|
+
agent_id=agent_id,
|
|
587
|
+
team_id=team_id,
|
|
588
|
+
namespace=effective_namespace,
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
return tools
|
|
593
|
+
|
|
594
|
+
# =========================================================================
|
|
595
|
+
# Tool: search_entities
|
|
596
|
+
# =========================================================================
|
|
597
|
+
|
|
598
|
+
def _create_search_entities_tool(
|
|
599
|
+
self,
|
|
600
|
+
user_id: Optional[str] = None,
|
|
601
|
+
namespace: Optional[str] = None,
|
|
602
|
+
) -> Callable:
|
|
603
|
+
"""Create the search_entities tool."""
|
|
604
|
+
|
|
605
|
+
def search_entities(
|
|
606
|
+
query: str,
|
|
607
|
+
entity_type: Optional[str] = None,
|
|
608
|
+
limit: int = 5,
|
|
609
|
+
) -> str:
|
|
610
|
+
"""Search for entities in the knowledge base.
|
|
611
|
+
|
|
612
|
+
Use this to recall information about people, companies, projects, products,
|
|
613
|
+
or other entities that have been stored. Searches across names, facts,
|
|
614
|
+
events, and relationships.
|
|
615
|
+
|
|
616
|
+
**Good times to search:**
|
|
617
|
+
- Before discussing a company/person that might have stored context
|
|
618
|
+
- When the user references an entity by name
|
|
619
|
+
- To recall details about a project or product
|
|
620
|
+
- To find relationships between entities
|
|
621
|
+
|
|
622
|
+
**Search tips:**
|
|
623
|
+
- Search by name: "Acme Corp", "Jane Smith"
|
|
624
|
+
- Search by attribute: "PostgreSQL", "San Francisco"
|
|
625
|
+
- Search by relationship: "CEO", "competitor"
|
|
626
|
+
- Combine with entity_type to narrow results
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
query: What to search for. Can be a name, fact content, relationship,
|
|
630
|
+
or any text that might appear in entity records.
|
|
631
|
+
Examples: "Acme", "uses PostgreSQL", "VP Engineering"
|
|
632
|
+
entity_type: Optional filter - "person", "company", "project", "product", etc.
|
|
633
|
+
limit: Maximum results (default: 5)
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Formatted list of matching entities with their facts, events, and relationships.
|
|
637
|
+
"""
|
|
638
|
+
results = self.search(
|
|
639
|
+
query=query,
|
|
640
|
+
entity_type=entity_type,
|
|
641
|
+
user_id=user_id,
|
|
642
|
+
namespace=namespace,
|
|
643
|
+
limit=limit,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
if not results:
|
|
647
|
+
return "No matching entities found."
|
|
648
|
+
|
|
649
|
+
formatted = self._format_entities_list(entities=results)
|
|
650
|
+
return f"Found {len(results)} entity/entities:\n\n{formatted}"
|
|
651
|
+
|
|
652
|
+
return search_entities
|
|
653
|
+
|
|
654
|
+
def _create_async_search_entities_tool(
|
|
655
|
+
self,
|
|
656
|
+
user_id: Optional[str] = None,
|
|
657
|
+
namespace: Optional[str] = None,
|
|
658
|
+
) -> Callable:
|
|
659
|
+
"""Create the async search_entities tool."""
|
|
660
|
+
|
|
661
|
+
async def search_entities(
|
|
662
|
+
query: str,
|
|
663
|
+
entity_type: Optional[str] = None,
|
|
664
|
+
limit: int = 5,
|
|
665
|
+
) -> str:
|
|
666
|
+
"""Search for entities in the knowledge base.
|
|
667
|
+
|
|
668
|
+
Use this to recall information about people, companies, projects, products,
|
|
669
|
+
or other entities that have been stored. Searches across names, facts,
|
|
670
|
+
events, and relationships.
|
|
671
|
+
|
|
672
|
+
**Good times to search:**
|
|
673
|
+
- Before discussing a company/person that might have stored context
|
|
674
|
+
- When the user references an entity by name
|
|
675
|
+
- To recall details about a project or product
|
|
676
|
+
- To find relationships between entities
|
|
677
|
+
|
|
678
|
+
**Search tips:**
|
|
679
|
+
- Search by name: "Acme Corp", "Jane Smith"
|
|
680
|
+
- Search by attribute: "PostgreSQL", "San Francisco"
|
|
681
|
+
- Search by relationship: "CEO", "competitor"
|
|
682
|
+
- Combine with entity_type to narrow results
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
query: What to search for. Can be a name, fact content, relationship,
|
|
686
|
+
or any text that might appear in entity records.
|
|
687
|
+
Examples: "Acme", "uses PostgreSQL", "VP Engineering"
|
|
688
|
+
entity_type: Optional filter - "person", "company", "project", "product", etc.
|
|
689
|
+
limit: Maximum results (default: 5)
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
Formatted list of matching entities with their facts, events, and relationships.
|
|
693
|
+
"""
|
|
694
|
+
results = await self.asearch(
|
|
695
|
+
query=query,
|
|
696
|
+
entity_type=entity_type,
|
|
697
|
+
user_id=user_id,
|
|
698
|
+
namespace=namespace,
|
|
699
|
+
limit=limit,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if not results:
|
|
703
|
+
return "No matching entities found."
|
|
704
|
+
|
|
705
|
+
formatted = self._format_entities_list(entities=results)
|
|
706
|
+
return f"Found {len(results)} entity/entities:\n\n{formatted}"
|
|
707
|
+
|
|
708
|
+
return search_entities
|
|
709
|
+
|
|
710
|
+
# =========================================================================
|
|
711
|
+
# Tool: create_entity
|
|
712
|
+
# =========================================================================
|
|
713
|
+
|
|
714
|
+
def _create_create_entity_tool(
|
|
715
|
+
self,
|
|
716
|
+
user_id: Optional[str] = None,
|
|
717
|
+
agent_id: Optional[str] = None,
|
|
718
|
+
team_id: Optional[str] = None,
|
|
719
|
+
namespace: Optional[str] = None,
|
|
720
|
+
) -> Callable:
|
|
721
|
+
"""Create the create_entity tool."""
|
|
722
|
+
|
|
723
|
+
def create_entity(
|
|
724
|
+
entity_id: str,
|
|
725
|
+
entity_type: str,
|
|
726
|
+
name: str,
|
|
727
|
+
description: Optional[str] = None,
|
|
728
|
+
properties: Optional[Dict[str, str]] = None,
|
|
729
|
+
) -> str:
|
|
730
|
+
"""Create a new entity in the knowledge base.
|
|
731
|
+
|
|
732
|
+
Use this when you encounter a person, company, project, or other entity
|
|
733
|
+
worth remembering. Create the entity first, then add facts/events/relationships.
|
|
734
|
+
|
|
735
|
+
**When to create an entity:**
|
|
736
|
+
- A company, person, or project is discussed with substantive details
|
|
737
|
+
- Information would be useful to recall in future conversations
|
|
738
|
+
- The entity has a specific identity (not just "a company")
|
|
739
|
+
|
|
740
|
+
**When NOT to create:**
|
|
741
|
+
- For the user themselves (use user memory)
|
|
742
|
+
- For generic concepts without specific identity
|
|
743
|
+
- For one-off mentions with no useful details
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
entity_id: Unique identifier using lowercase and underscores.
|
|
747
|
+
Convention: descriptive name like "acme_corp", "jane_smith", "project_atlas"
|
|
748
|
+
Bad: "company1", "entity_123", "c"
|
|
749
|
+
entity_type: Category of entity. Common types:
|
|
750
|
+
- "person": Individual people
|
|
751
|
+
- "company": Businesses, organizations
|
|
752
|
+
- "project": Specific initiatives or projects
|
|
753
|
+
- "product": Software, services, offerings
|
|
754
|
+
- "system": Technical systems, platforms
|
|
755
|
+
- "concept": Domain-specific concepts worth tracking
|
|
756
|
+
name: Human-readable display name (e.g., "Acme Corporation", "Jane Smith")
|
|
757
|
+
description: Brief description of what/who this entity is.
|
|
758
|
+
Good: "Enterprise SaaS startup in the fintech space, potential client"
|
|
759
|
+
Bad: "A company" (too vague)
|
|
760
|
+
properties: Optional key-value metadata (e.g., {"industry": "fintech", "stage": "Series A"})
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
Confirmation message.
|
|
764
|
+
"""
|
|
765
|
+
success = self.create_entity(
|
|
766
|
+
entity_id=entity_id,
|
|
767
|
+
entity_type=entity_type,
|
|
768
|
+
name=name,
|
|
769
|
+
description=description,
|
|
770
|
+
properties=properties,
|
|
771
|
+
user_id=user_id,
|
|
772
|
+
agent_id=agent_id,
|
|
773
|
+
team_id=team_id,
|
|
774
|
+
namespace=namespace,
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
if success:
|
|
778
|
+
self.entity_updated = True
|
|
779
|
+
return f"Entity created: {entity_type}/{entity_id} ({name})"
|
|
780
|
+
return "Failed to create entity (may already exist)"
|
|
781
|
+
|
|
782
|
+
return create_entity
|
|
783
|
+
|
|
784
|
+
def _create_async_create_entity_tool(
|
|
785
|
+
self,
|
|
786
|
+
user_id: Optional[str] = None,
|
|
787
|
+
agent_id: Optional[str] = None,
|
|
788
|
+
team_id: Optional[str] = None,
|
|
789
|
+
namespace: Optional[str] = None,
|
|
790
|
+
) -> Callable:
|
|
791
|
+
"""Create the async create_entity tool."""
|
|
792
|
+
|
|
793
|
+
async def create_entity(
|
|
794
|
+
entity_id: str,
|
|
795
|
+
entity_type: str,
|
|
796
|
+
name: str,
|
|
797
|
+
description: Optional[str] = None,
|
|
798
|
+
properties: Optional[Dict[str, str]] = None,
|
|
799
|
+
) -> str:
|
|
800
|
+
"""Create a new entity in the knowledge base.
|
|
801
|
+
|
|
802
|
+
Use this when you encounter a person, company, project, or other entity
|
|
803
|
+
worth remembering. Create the entity first, then add facts/events/relationships.
|
|
804
|
+
|
|
805
|
+
**When to create an entity:**
|
|
806
|
+
- A company, person, or project is discussed with substantive details
|
|
807
|
+
- Information would be useful to recall in future conversations
|
|
808
|
+
- The entity has a specific identity (not just "a company")
|
|
809
|
+
|
|
810
|
+
**When NOT to create:**
|
|
811
|
+
- For the user themselves (use user memory)
|
|
812
|
+
- For generic concepts without specific identity
|
|
813
|
+
- For one-off mentions with no useful details
|
|
814
|
+
|
|
815
|
+
Args:
|
|
816
|
+
entity_id: Unique identifier using lowercase and underscores.
|
|
817
|
+
Convention: descriptive name like "acme_corp", "jane_smith", "project_atlas"
|
|
818
|
+
Bad: "company1", "entity_123", "c"
|
|
819
|
+
entity_type: Category of entity. Common types:
|
|
820
|
+
- "person": Individual people
|
|
821
|
+
- "company": Businesses, organizations
|
|
822
|
+
- "project": Specific initiatives or projects
|
|
823
|
+
- "product": Software, services, offerings
|
|
824
|
+
- "system": Technical systems, platforms
|
|
825
|
+
- "concept": Domain-specific concepts worth tracking
|
|
826
|
+
name: Human-readable display name (e.g., "Acme Corporation", "Jane Smith")
|
|
827
|
+
description: Brief description of what/who this entity is.
|
|
828
|
+
Good: "Enterprise SaaS startup in the fintech space, potential client"
|
|
829
|
+
Bad: "A company" (too vague)
|
|
830
|
+
properties: Optional key-value metadata (e.g., {"industry": "fintech", "stage": "Series A"})
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
Confirmation message.
|
|
834
|
+
"""
|
|
835
|
+
success = await self.acreate_entity(
|
|
836
|
+
entity_id=entity_id,
|
|
837
|
+
entity_type=entity_type,
|
|
838
|
+
name=name,
|
|
839
|
+
description=description,
|
|
840
|
+
properties=properties,
|
|
841
|
+
user_id=user_id,
|
|
842
|
+
agent_id=agent_id,
|
|
843
|
+
team_id=team_id,
|
|
844
|
+
namespace=namespace,
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
if success:
|
|
848
|
+
self.entity_updated = True
|
|
849
|
+
return f"Entity created: {entity_type}/{entity_id} ({name})"
|
|
850
|
+
return "Failed to create entity (may already exist)"
|
|
851
|
+
|
|
852
|
+
return create_entity
|
|
853
|
+
|
|
854
|
+
# =========================================================================
|
|
855
|
+
# Tool: update_entity
|
|
856
|
+
# =========================================================================
|
|
857
|
+
|
|
858
|
+
def _create_update_entity_tool(
|
|
859
|
+
self,
|
|
860
|
+
user_id: Optional[str] = None,
|
|
861
|
+
agent_id: Optional[str] = None,
|
|
862
|
+
team_id: Optional[str] = None,
|
|
863
|
+
namespace: Optional[str] = None,
|
|
864
|
+
) -> Callable:
|
|
865
|
+
"""Create the update_entity tool."""
|
|
866
|
+
|
|
867
|
+
def update_entity(
|
|
868
|
+
entity_id: str,
|
|
869
|
+
entity_type: str,
|
|
870
|
+
name: Optional[str] = None,
|
|
871
|
+
description: Optional[str] = None,
|
|
872
|
+
properties: Optional[Dict[str, str]] = None,
|
|
873
|
+
) -> str:
|
|
874
|
+
"""Update an existing entity's core properties.
|
|
875
|
+
|
|
876
|
+
Use this to modify the entity's identity information. Only provided
|
|
877
|
+
fields will be updated - omitted fields remain unchanged.
|
|
878
|
+
|
|
879
|
+
**When to update:**
|
|
880
|
+
- Name change: Company rebranded, person changed name
|
|
881
|
+
- Description evolved: Better understanding of what entity is
|
|
882
|
+
- Properties changed: New metadata to add
|
|
883
|
+
|
|
884
|
+
**Note:** To update facts, events, or relationships, use the specific
|
|
885
|
+
tools (update_fact, add_event, add_relationship) instead.
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
entity_id: The entity's identifier
|
|
889
|
+
entity_type: Type of entity
|
|
890
|
+
name: New display name (only if changed)
|
|
891
|
+
description: New description (only if you have better info)
|
|
892
|
+
properties: Properties to add/update (merged with existing)
|
|
893
|
+
Existing properties not in this dict are preserved
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
Confirmation message.
|
|
897
|
+
"""
|
|
898
|
+
success = self.update_entity(
|
|
899
|
+
entity_id=entity_id,
|
|
900
|
+
entity_type=entity_type,
|
|
901
|
+
name=name,
|
|
902
|
+
description=description,
|
|
903
|
+
properties=properties,
|
|
904
|
+
user_id=user_id,
|
|
905
|
+
agent_id=agent_id,
|
|
906
|
+
team_id=team_id,
|
|
907
|
+
namespace=namespace,
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
if success:
|
|
911
|
+
self.entity_updated = True
|
|
912
|
+
return f"Entity updated: {entity_type}/{entity_id}"
|
|
913
|
+
return f"Entity not found: {entity_type}/{entity_id}"
|
|
914
|
+
|
|
915
|
+
return update_entity
|
|
916
|
+
|
|
917
|
+
def _create_async_update_entity_tool(
|
|
918
|
+
self,
|
|
919
|
+
user_id: Optional[str] = None,
|
|
920
|
+
agent_id: Optional[str] = None,
|
|
921
|
+
team_id: Optional[str] = None,
|
|
922
|
+
namespace: Optional[str] = None,
|
|
923
|
+
) -> Callable:
|
|
924
|
+
"""Create the async update_entity tool."""
|
|
925
|
+
|
|
926
|
+
async def update_entity(
|
|
927
|
+
entity_id: str,
|
|
928
|
+
entity_type: str,
|
|
929
|
+
name: Optional[str] = None,
|
|
930
|
+
description: Optional[str] = None,
|
|
931
|
+
properties: Optional[Dict[str, str]] = None,
|
|
932
|
+
) -> str:
|
|
933
|
+
"""Update an existing entity's core properties.
|
|
934
|
+
|
|
935
|
+
Use this to modify the entity's identity information. Only provided
|
|
936
|
+
fields will be updated - omitted fields remain unchanged.
|
|
937
|
+
|
|
938
|
+
**When to update:**
|
|
939
|
+
- Name change: Company rebranded, person changed name
|
|
940
|
+
- Description evolved: Better understanding of what entity is
|
|
941
|
+
- Properties changed: New metadata to add
|
|
942
|
+
|
|
943
|
+
**Note:** To update facts, events, or relationships, use the specific
|
|
944
|
+
tools (update_fact, add_event, add_relationship) instead.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
entity_id: The entity's identifier
|
|
948
|
+
entity_type: Type of entity
|
|
949
|
+
name: New display name (only if changed)
|
|
950
|
+
description: New description (only if you have better info)
|
|
951
|
+
properties: Properties to add/update (merged with existing)
|
|
952
|
+
Existing properties not in this dict are preserved
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
Confirmation message.
|
|
956
|
+
"""
|
|
957
|
+
success = await self.aupdate_entity(
|
|
958
|
+
entity_id=entity_id,
|
|
959
|
+
entity_type=entity_type,
|
|
960
|
+
name=name,
|
|
961
|
+
description=description,
|
|
962
|
+
properties=properties,
|
|
963
|
+
user_id=user_id,
|
|
964
|
+
agent_id=agent_id,
|
|
965
|
+
team_id=team_id,
|
|
966
|
+
namespace=namespace,
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
if success:
|
|
970
|
+
self.entity_updated = True
|
|
971
|
+
return f"Entity updated: {entity_type}/{entity_id}"
|
|
972
|
+
return f"Entity not found: {entity_type}/{entity_id}"
|
|
973
|
+
|
|
974
|
+
return update_entity
|
|
975
|
+
|
|
976
|
+
# =========================================================================
|
|
977
|
+
# Tool: add_fact
|
|
978
|
+
# =========================================================================
|
|
979
|
+
|
|
980
|
+
def _create_add_fact_tool(
|
|
981
|
+
self,
|
|
982
|
+
user_id: Optional[str] = None,
|
|
983
|
+
agent_id: Optional[str] = None,
|
|
984
|
+
team_id: Optional[str] = None,
|
|
985
|
+
namespace: Optional[str] = None,
|
|
986
|
+
) -> Callable:
|
|
987
|
+
"""Create the add_fact tool."""
|
|
988
|
+
|
|
989
|
+
def add_fact(
|
|
990
|
+
entity_id: str,
|
|
991
|
+
entity_type: str,
|
|
992
|
+
fact: str,
|
|
993
|
+
) -> str:
|
|
994
|
+
"""Add a fact to an entity.
|
|
995
|
+
|
|
996
|
+
Facts are **timeless truths** about an entity (semantic memory).
|
|
997
|
+
They describe what IS, not what HAPPENED.
|
|
998
|
+
|
|
999
|
+
**Good facts (timeless, descriptive):**
|
|
1000
|
+
- "Uses PostgreSQL and Redis for their data layer"
|
|
1001
|
+
- "Headquarters in San Francisco, engineering team in Austin"
|
|
1002
|
+
- "Founded by ex-Google engineers in 2019"
|
|
1003
|
+
- "Main product is a B2B analytics platform"
|
|
1004
|
+
- "Prefers async communication via Slack"
|
|
1005
|
+
|
|
1006
|
+
**Not facts (use events instead):**
|
|
1007
|
+
- "Launched v2.0 last month" → This is an EVENT (time-bound)
|
|
1008
|
+
- "Just closed Series B" → This is an EVENT
|
|
1009
|
+
- "Had a meeting yesterday" → This is an EVENT
|
|
1010
|
+
|
|
1011
|
+
**Not facts (too vague):**
|
|
1012
|
+
- "It's a good company" → Subjective, not useful
|
|
1013
|
+
- "They do tech stuff" → Too vague
|
|
1014
|
+
|
|
1015
|
+
Args:
|
|
1016
|
+
entity_id: The entity's identifier (e.g., "acme_corp")
|
|
1017
|
+
entity_type: Type of entity (e.g., "company")
|
|
1018
|
+
fact: The fact to add - should be specific and timeless
|
|
1019
|
+
|
|
1020
|
+
Returns:
|
|
1021
|
+
Confirmation message with fact ID.
|
|
1022
|
+
"""
|
|
1023
|
+
fact_id = self.add_fact(
|
|
1024
|
+
entity_id=entity_id,
|
|
1025
|
+
entity_type=entity_type,
|
|
1026
|
+
fact=fact,
|
|
1027
|
+
user_id=user_id,
|
|
1028
|
+
agent_id=agent_id,
|
|
1029
|
+
team_id=team_id,
|
|
1030
|
+
namespace=namespace,
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
if fact_id:
|
|
1034
|
+
self.entity_updated = True
|
|
1035
|
+
return f"Fact added to {entity_type}/{entity_id} (id: {fact_id})"
|
|
1036
|
+
return "Failed to add fact (entity may not exist)"
|
|
1037
|
+
|
|
1038
|
+
return add_fact
|
|
1039
|
+
|
|
1040
|
+
def _create_async_add_fact_tool(
|
|
1041
|
+
self,
|
|
1042
|
+
user_id: Optional[str] = None,
|
|
1043
|
+
agent_id: Optional[str] = None,
|
|
1044
|
+
team_id: Optional[str] = None,
|
|
1045
|
+
namespace: Optional[str] = None,
|
|
1046
|
+
) -> Callable:
|
|
1047
|
+
"""Create the async add_fact tool."""
|
|
1048
|
+
|
|
1049
|
+
async def add_fact(
|
|
1050
|
+
entity_id: str,
|
|
1051
|
+
entity_type: str,
|
|
1052
|
+
fact: str,
|
|
1053
|
+
) -> str:
|
|
1054
|
+
"""Add a fact to an entity.
|
|
1055
|
+
|
|
1056
|
+
Facts are **timeless truths** about an entity (semantic memory).
|
|
1057
|
+
They describe what IS, not what HAPPENED.
|
|
1058
|
+
|
|
1059
|
+
**Good facts (timeless, descriptive):**
|
|
1060
|
+
- "Uses PostgreSQL and Redis for their data layer"
|
|
1061
|
+
- "Headquarters in San Francisco, engineering team in Austin"
|
|
1062
|
+
- "Founded by ex-Google engineers in 2019"
|
|
1063
|
+
- "Main product is a B2B analytics platform"
|
|
1064
|
+
- "Prefers async communication via Slack"
|
|
1065
|
+
|
|
1066
|
+
**Not facts (use events instead):**
|
|
1067
|
+
- "Launched v2.0 last month" → This is an EVENT (time-bound)
|
|
1068
|
+
- "Just closed Series B" → This is an EVENT
|
|
1069
|
+
- "Had a meeting yesterday" → This is an EVENT
|
|
1070
|
+
|
|
1071
|
+
**Not facts (too vague):**
|
|
1072
|
+
- "It's a good company" → Subjective, not useful
|
|
1073
|
+
- "They do tech stuff" → Too vague
|
|
1074
|
+
|
|
1075
|
+
Args:
|
|
1076
|
+
entity_id: The entity's identifier (e.g., "acme_corp")
|
|
1077
|
+
entity_type: Type of entity (e.g., "company")
|
|
1078
|
+
fact: The fact to add - should be specific and timeless
|
|
1079
|
+
|
|
1080
|
+
Returns:
|
|
1081
|
+
Confirmation message with fact ID.
|
|
1082
|
+
"""
|
|
1083
|
+
fact_id = await self.aadd_fact(
|
|
1084
|
+
entity_id=entity_id,
|
|
1085
|
+
entity_type=entity_type,
|
|
1086
|
+
fact=fact,
|
|
1087
|
+
user_id=user_id,
|
|
1088
|
+
agent_id=agent_id,
|
|
1089
|
+
team_id=team_id,
|
|
1090
|
+
namespace=namespace,
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
if fact_id:
|
|
1094
|
+
self.entity_updated = True
|
|
1095
|
+
return f"Fact added to {entity_type}/{entity_id} (id: {fact_id})"
|
|
1096
|
+
return "Failed to add fact (entity may not exist)"
|
|
1097
|
+
|
|
1098
|
+
return add_fact
|
|
1099
|
+
|
|
1100
|
+
# =========================================================================
|
|
1101
|
+
# Tool: update_fact
|
|
1102
|
+
# =========================================================================
|
|
1103
|
+
|
|
1104
|
+
def _create_update_fact_tool(
|
|
1105
|
+
self,
|
|
1106
|
+
user_id: Optional[str] = None,
|
|
1107
|
+
agent_id: Optional[str] = None,
|
|
1108
|
+
team_id: Optional[str] = None,
|
|
1109
|
+
namespace: Optional[str] = None,
|
|
1110
|
+
) -> Callable:
|
|
1111
|
+
"""Create the update_fact tool."""
|
|
1112
|
+
|
|
1113
|
+
def update_fact(
|
|
1114
|
+
entity_id: str,
|
|
1115
|
+
entity_type: str,
|
|
1116
|
+
fact_id: str,
|
|
1117
|
+
fact: str,
|
|
1118
|
+
) -> str:
|
|
1119
|
+
"""Update an existing fact on an entity.
|
|
1120
|
+
|
|
1121
|
+
Use this when a fact needs correction or has become more specific.
|
|
1122
|
+
The new fact completely replaces the old one.
|
|
1123
|
+
|
|
1124
|
+
**When to update:**
|
|
1125
|
+
- Correction: Original fact was wrong
|
|
1126
|
+
- More detail: "Uses PostgreSQL" → "Uses PostgreSQL 15 with TimescaleDB extension"
|
|
1127
|
+
- Changed reality: "50 employees" → "75 employees after recent hiring"
|
|
1128
|
+
|
|
1129
|
+
**When to delete instead:**
|
|
1130
|
+
- Fact is no longer true and shouldn't be replaced
|
|
1131
|
+
- Fact was a misunderstanding
|
|
1132
|
+
|
|
1133
|
+
Args:
|
|
1134
|
+
entity_id: The entity's identifier
|
|
1135
|
+
entity_type: Type of entity
|
|
1136
|
+
fact_id: ID of the fact to update (from search_entities results)
|
|
1137
|
+
fact: New fact content - complete replacement, not a diff
|
|
1138
|
+
|
|
1139
|
+
Returns:
|
|
1140
|
+
Confirmation message.
|
|
1141
|
+
"""
|
|
1142
|
+
success = self.update_fact(
|
|
1143
|
+
entity_id=entity_id,
|
|
1144
|
+
entity_type=entity_type,
|
|
1145
|
+
fact_id=fact_id,
|
|
1146
|
+
fact=fact,
|
|
1147
|
+
user_id=user_id,
|
|
1148
|
+
agent_id=agent_id,
|
|
1149
|
+
team_id=team_id,
|
|
1150
|
+
namespace=namespace,
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
if success:
|
|
1154
|
+
self.entity_updated = True
|
|
1155
|
+
return f"Fact updated on {entity_type}/{entity_id}"
|
|
1156
|
+
return f"Fact not found: {fact_id}"
|
|
1157
|
+
|
|
1158
|
+
return update_fact
|
|
1159
|
+
|
|
1160
|
+
def _create_async_update_fact_tool(
|
|
1161
|
+
self,
|
|
1162
|
+
user_id: Optional[str] = None,
|
|
1163
|
+
agent_id: Optional[str] = None,
|
|
1164
|
+
team_id: Optional[str] = None,
|
|
1165
|
+
namespace: Optional[str] = None,
|
|
1166
|
+
) -> Callable:
|
|
1167
|
+
"""Create the async update_fact tool."""
|
|
1168
|
+
|
|
1169
|
+
async def update_fact(
|
|
1170
|
+
entity_id: str,
|
|
1171
|
+
entity_type: str,
|
|
1172
|
+
fact_id: str,
|
|
1173
|
+
fact: str,
|
|
1174
|
+
) -> str:
|
|
1175
|
+
"""Update an existing fact on an entity.
|
|
1176
|
+
|
|
1177
|
+
Use this when a fact needs correction or has become more specific.
|
|
1178
|
+
The new fact completely replaces the old one.
|
|
1179
|
+
|
|
1180
|
+
**When to update:**
|
|
1181
|
+
- Correction: Original fact was wrong
|
|
1182
|
+
- More detail: "Uses PostgreSQL" → "Uses PostgreSQL 15 with TimescaleDB extension"
|
|
1183
|
+
- Changed reality: "50 employees" → "75 employees after recent hiring"
|
|
1184
|
+
|
|
1185
|
+
**When to delete instead:**
|
|
1186
|
+
- Fact is no longer true and shouldn't be replaced
|
|
1187
|
+
- Fact was a misunderstanding
|
|
1188
|
+
|
|
1189
|
+
Args:
|
|
1190
|
+
entity_id: The entity's identifier
|
|
1191
|
+
entity_type: Type of entity
|
|
1192
|
+
fact_id: ID of the fact to update (from search_entities results)
|
|
1193
|
+
fact: New fact content - complete replacement, not a diff
|
|
1194
|
+
|
|
1195
|
+
Returns:
|
|
1196
|
+
Confirmation message.
|
|
1197
|
+
"""
|
|
1198
|
+
success = await self.aupdate_fact(
|
|
1199
|
+
entity_id=entity_id,
|
|
1200
|
+
entity_type=entity_type,
|
|
1201
|
+
fact_id=fact_id,
|
|
1202
|
+
fact=fact,
|
|
1203
|
+
user_id=user_id,
|
|
1204
|
+
agent_id=agent_id,
|
|
1205
|
+
team_id=team_id,
|
|
1206
|
+
namespace=namespace,
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
if success:
|
|
1210
|
+
self.entity_updated = True
|
|
1211
|
+
return f"Fact updated on {entity_type}/{entity_id}"
|
|
1212
|
+
return f"Fact not found: {fact_id}"
|
|
1213
|
+
|
|
1214
|
+
return update_fact
|
|
1215
|
+
|
|
1216
|
+
# =========================================================================
|
|
1217
|
+
# Tool: delete_fact
|
|
1218
|
+
# =========================================================================
|
|
1219
|
+
|
|
1220
|
+
def _create_delete_fact_tool(
|
|
1221
|
+
self,
|
|
1222
|
+
user_id: Optional[str] = None,
|
|
1223
|
+
agent_id: Optional[str] = None,
|
|
1224
|
+
team_id: Optional[str] = None,
|
|
1225
|
+
namespace: Optional[str] = None,
|
|
1226
|
+
) -> Callable:
|
|
1227
|
+
"""Create the delete_fact tool."""
|
|
1228
|
+
|
|
1229
|
+
def delete_fact(
|
|
1230
|
+
entity_id: str,
|
|
1231
|
+
entity_type: str,
|
|
1232
|
+
fact_id: str,
|
|
1233
|
+
) -> str:
|
|
1234
|
+
"""Delete a fact from an entity.
|
|
1235
|
+
|
|
1236
|
+
Use this when a fact is no longer accurate and shouldn't be replaced
|
|
1237
|
+
with updated information.
|
|
1238
|
+
|
|
1239
|
+
**When to delete:**
|
|
1240
|
+
- Fact was incorrect/misunderstood
|
|
1241
|
+
- Fact is no longer true (and no replacement makes sense)
|
|
1242
|
+
- Duplicate of another fact
|
|
1243
|
+
- Too vague to be useful
|
|
1244
|
+
|
|
1245
|
+
**When to update instead:**
|
|
1246
|
+
- Fact needs correction but the topic is still relevant
|
|
1247
|
+
- Fact needs more detail
|
|
1248
|
+
|
|
1249
|
+
Args:
|
|
1250
|
+
entity_id: The entity's identifier
|
|
1251
|
+
entity_type: Type of entity
|
|
1252
|
+
fact_id: ID of the fact to delete (from search_entities results)
|
|
1253
|
+
|
|
1254
|
+
Returns:
|
|
1255
|
+
Confirmation message.
|
|
1256
|
+
"""
|
|
1257
|
+
success = self.delete_fact(
|
|
1258
|
+
entity_id=entity_id,
|
|
1259
|
+
entity_type=entity_type,
|
|
1260
|
+
fact_id=fact_id,
|
|
1261
|
+
user_id=user_id,
|
|
1262
|
+
agent_id=agent_id,
|
|
1263
|
+
team_id=team_id,
|
|
1264
|
+
namespace=namespace,
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
if success:
|
|
1268
|
+
self.entity_updated = True
|
|
1269
|
+
return f"Fact deleted from {entity_type}/{entity_id}"
|
|
1270
|
+
return f"Fact not found: {fact_id}"
|
|
1271
|
+
|
|
1272
|
+
return delete_fact
|
|
1273
|
+
|
|
1274
|
+
def _create_async_delete_fact_tool(
|
|
1275
|
+
self,
|
|
1276
|
+
user_id: Optional[str] = None,
|
|
1277
|
+
agent_id: Optional[str] = None,
|
|
1278
|
+
team_id: Optional[str] = None,
|
|
1279
|
+
namespace: Optional[str] = None,
|
|
1280
|
+
) -> Callable:
|
|
1281
|
+
"""Create the async delete_fact tool."""
|
|
1282
|
+
|
|
1283
|
+
async def delete_fact(
|
|
1284
|
+
entity_id: str,
|
|
1285
|
+
entity_type: str,
|
|
1286
|
+
fact_id: str,
|
|
1287
|
+
) -> str:
|
|
1288
|
+
"""Delete a fact from an entity.
|
|
1289
|
+
|
|
1290
|
+
Use this when a fact is no longer accurate and shouldn't be replaced
|
|
1291
|
+
with updated information.
|
|
1292
|
+
|
|
1293
|
+
**When to delete:**
|
|
1294
|
+
- Fact was incorrect/misunderstood
|
|
1295
|
+
- Fact is no longer true (and no replacement makes sense)
|
|
1296
|
+
- Duplicate of another fact
|
|
1297
|
+
- Too vague to be useful
|
|
1298
|
+
|
|
1299
|
+
**When to update instead:**
|
|
1300
|
+
- Fact needs correction but the topic is still relevant
|
|
1301
|
+
- Fact needs more detail
|
|
1302
|
+
|
|
1303
|
+
Args:
|
|
1304
|
+
entity_id: The entity's identifier
|
|
1305
|
+
entity_type: Type of entity
|
|
1306
|
+
fact_id: ID of the fact to delete (from search_entities results)
|
|
1307
|
+
|
|
1308
|
+
Returns:
|
|
1309
|
+
Confirmation message.
|
|
1310
|
+
"""
|
|
1311
|
+
success = await self.adelete_fact(
|
|
1312
|
+
entity_id=entity_id,
|
|
1313
|
+
entity_type=entity_type,
|
|
1314
|
+
fact_id=fact_id,
|
|
1315
|
+
user_id=user_id,
|
|
1316
|
+
agent_id=agent_id,
|
|
1317
|
+
team_id=team_id,
|
|
1318
|
+
namespace=namespace,
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
if success:
|
|
1322
|
+
self.entity_updated = True
|
|
1323
|
+
return f"Fact deleted from {entity_type}/{entity_id}"
|
|
1324
|
+
return f"Fact not found: {fact_id}"
|
|
1325
|
+
|
|
1326
|
+
return delete_fact
|
|
1327
|
+
|
|
1328
|
+
# =========================================================================
|
|
1329
|
+
# Tool: add_event
|
|
1330
|
+
# =========================================================================
|
|
1331
|
+
|
|
1332
|
+
def _create_add_event_tool(
|
|
1333
|
+
self,
|
|
1334
|
+
user_id: Optional[str] = None,
|
|
1335
|
+
agent_id: Optional[str] = None,
|
|
1336
|
+
team_id: Optional[str] = None,
|
|
1337
|
+
namespace: Optional[str] = None,
|
|
1338
|
+
) -> Callable:
|
|
1339
|
+
"""Create the add_event tool."""
|
|
1340
|
+
|
|
1341
|
+
def add_event(
|
|
1342
|
+
entity_id: str,
|
|
1343
|
+
entity_type: str,
|
|
1344
|
+
event: str,
|
|
1345
|
+
date: Optional[str] = None,
|
|
1346
|
+
) -> str:
|
|
1347
|
+
"""Add an event to an entity.
|
|
1348
|
+
|
|
1349
|
+
Events are **time-bound occurrences** (episodic memory).
|
|
1350
|
+
They describe what HAPPENED, not what IS.
|
|
1351
|
+
|
|
1352
|
+
**Good events (specific, time-bound):**
|
|
1353
|
+
- "Launched v2.0 with new ML features" (date: "2025-01-15")
|
|
1354
|
+
- "Closed $50M Series B led by Sequoia" (date: "2024-Q3")
|
|
1355
|
+
- "Had 4-hour outage affecting payment processing" (date: "2024-12-20")
|
|
1356
|
+
- "CEO announced pivot to enterprise market" (date: "2024-11")
|
|
1357
|
+
- "Initial discovery call - interested in our analytics product"
|
|
1358
|
+
|
|
1359
|
+
**Not events (use facts instead):**
|
|
1360
|
+
- "Uses PostgreSQL" → This is a FACT (timeless truth)
|
|
1361
|
+
- "Based in San Francisco" → This is a FACT
|
|
1362
|
+
- "Has 50 employees" → This is a FACT
|
|
1363
|
+
|
|
1364
|
+
**Include dates when known** - even approximate dates help:
|
|
1365
|
+
- Exact: "2025-01-15"
|
|
1366
|
+
- Month: "January 2025" or "2025-01"
|
|
1367
|
+
- Quarter: "Q1 2025"
|
|
1368
|
+
- Relative: "early 2024", "last week"
|
|
1369
|
+
|
|
1370
|
+
Args:
|
|
1371
|
+
entity_id: The entity's identifier (e.g., "acme_corp")
|
|
1372
|
+
entity_type: Type of entity (e.g., "company")
|
|
1373
|
+
event: Description of what happened - be specific
|
|
1374
|
+
date: When it happened (ISO format, natural language, or approximate)
|
|
1375
|
+
|
|
1376
|
+
Returns:
|
|
1377
|
+
Confirmation message with event ID.
|
|
1378
|
+
"""
|
|
1379
|
+
event_id = self.add_event(
|
|
1380
|
+
entity_id=entity_id,
|
|
1381
|
+
entity_type=entity_type,
|
|
1382
|
+
event=event,
|
|
1383
|
+
date=date,
|
|
1384
|
+
user_id=user_id,
|
|
1385
|
+
agent_id=agent_id,
|
|
1386
|
+
team_id=team_id,
|
|
1387
|
+
namespace=namespace,
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
if event_id:
|
|
1391
|
+
self.entity_updated = True
|
|
1392
|
+
return f"Event added to {entity_type}/{entity_id} (id: {event_id})"
|
|
1393
|
+
return "Failed to add event (entity may not exist)"
|
|
1394
|
+
|
|
1395
|
+
return add_event
|
|
1396
|
+
|
|
1397
|
+
def _create_async_add_event_tool(
|
|
1398
|
+
self,
|
|
1399
|
+
user_id: Optional[str] = None,
|
|
1400
|
+
agent_id: Optional[str] = None,
|
|
1401
|
+
team_id: Optional[str] = None,
|
|
1402
|
+
namespace: Optional[str] = None,
|
|
1403
|
+
) -> Callable:
|
|
1404
|
+
"""Create the async add_event tool."""
|
|
1405
|
+
|
|
1406
|
+
async def add_event(
|
|
1407
|
+
entity_id: str,
|
|
1408
|
+
entity_type: str,
|
|
1409
|
+
event: str,
|
|
1410
|
+
date: Optional[str] = None,
|
|
1411
|
+
) -> str:
|
|
1412
|
+
"""Add an event to an entity.
|
|
1413
|
+
|
|
1414
|
+
Events are **time-bound occurrences** (episodic memory).
|
|
1415
|
+
They describe what HAPPENED, not what IS.
|
|
1416
|
+
|
|
1417
|
+
**Good events (specific, time-bound):**
|
|
1418
|
+
- "Launched v2.0 with new ML features" (date: "2025-01-15")
|
|
1419
|
+
- "Closed $50M Series B led by Sequoia" (date: "2024-Q3")
|
|
1420
|
+
- "Had 4-hour outage affecting payment processing" (date: "2024-12-20")
|
|
1421
|
+
- "CEO announced pivot to enterprise market" (date: "2024-11")
|
|
1422
|
+
- "Initial discovery call - interested in our analytics product"
|
|
1423
|
+
|
|
1424
|
+
**Not events (use facts instead):**
|
|
1425
|
+
- "Uses PostgreSQL" → This is a FACT (timeless truth)
|
|
1426
|
+
- "Based in San Francisco" → This is a FACT
|
|
1427
|
+
- "Has 50 employees" → This is a FACT
|
|
1428
|
+
|
|
1429
|
+
**Include dates when known** - even approximate dates help:
|
|
1430
|
+
- Exact: "2025-01-15"
|
|
1431
|
+
- Month: "January 2025" or "2025-01"
|
|
1432
|
+
- Quarter: "Q1 2025"
|
|
1433
|
+
- Relative: "early 2024", "last week"
|
|
1434
|
+
|
|
1435
|
+
Args:
|
|
1436
|
+
entity_id: The entity's identifier (e.g., "acme_corp")
|
|
1437
|
+
entity_type: Type of entity (e.g., "company")
|
|
1438
|
+
event: Description of what happened - be specific
|
|
1439
|
+
date: When it happened (ISO format, natural language, or approximate)
|
|
1440
|
+
|
|
1441
|
+
Returns:
|
|
1442
|
+
Confirmation message with event ID.
|
|
1443
|
+
"""
|
|
1444
|
+
event_id = await self.aadd_event(
|
|
1445
|
+
entity_id=entity_id,
|
|
1446
|
+
entity_type=entity_type,
|
|
1447
|
+
event=event,
|
|
1448
|
+
date=date,
|
|
1449
|
+
user_id=user_id,
|
|
1450
|
+
agent_id=agent_id,
|
|
1451
|
+
team_id=team_id,
|
|
1452
|
+
namespace=namespace,
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
if event_id:
|
|
1456
|
+
self.entity_updated = True
|
|
1457
|
+
return f"Event added to {entity_type}/{entity_id} (id: {event_id})"
|
|
1458
|
+
return "Failed to add event (entity may not exist)"
|
|
1459
|
+
|
|
1460
|
+
return add_event
|
|
1461
|
+
|
|
1462
|
+
# =========================================================================
|
|
1463
|
+
# Tool: add_relationship
|
|
1464
|
+
# =========================================================================
|
|
1465
|
+
|
|
1466
|
+
def _create_add_relationship_tool(
|
|
1467
|
+
self,
|
|
1468
|
+
user_id: Optional[str] = None,
|
|
1469
|
+
agent_id: Optional[str] = None,
|
|
1470
|
+
team_id: Optional[str] = None,
|
|
1471
|
+
namespace: Optional[str] = None,
|
|
1472
|
+
) -> Callable:
|
|
1473
|
+
"""Create the add_relationship tool."""
|
|
1474
|
+
|
|
1475
|
+
def add_relationship(
|
|
1476
|
+
entity_id: str,
|
|
1477
|
+
entity_type: str,
|
|
1478
|
+
related_entity_id: str,
|
|
1479
|
+
relation: str,
|
|
1480
|
+
direction: str = "outgoing",
|
|
1481
|
+
) -> str:
|
|
1482
|
+
"""Add a relationship between two entities.
|
|
1483
|
+
|
|
1484
|
+
Relationships are **graph edges** connecting entities - they capture
|
|
1485
|
+
how entities relate to each other.
|
|
1486
|
+
|
|
1487
|
+
**Common relationship patterns:**
|
|
1488
|
+
|
|
1489
|
+
People → Companies:
|
|
1490
|
+
- "jane_smith" --[CEO]--> "acme_corp"
|
|
1491
|
+
- "bob_jones" --[engineer_at]--> "acme_corp"
|
|
1492
|
+
- "sarah_chen" --[founder]--> "startup_xyz"
|
|
1493
|
+
|
|
1494
|
+
Companies → Companies:
|
|
1495
|
+
- "acme_corp" --[competitor_of]--> "beta_inc"
|
|
1496
|
+
- "acme_corp" --[acquired]--> "small_startup"
|
|
1497
|
+
- "acme_corp" --[partner_of]--> "big_vendor"
|
|
1498
|
+
|
|
1499
|
+
Projects → Other entities:
|
|
1500
|
+
- "project_atlas" --[uses]--> "postgresql"
|
|
1501
|
+
- "project_atlas" --[owned_by]--> "acme_corp"
|
|
1502
|
+
- "project_atlas" --[led_by]--> "jane_smith"
|
|
1503
|
+
|
|
1504
|
+
**Direction matters:**
|
|
1505
|
+
- "outgoing": This entity → Related entity (default)
|
|
1506
|
+
"jane_smith" --[CEO]--> "acme_corp" means Jane IS CEO OF Acme
|
|
1507
|
+
- "incoming": Related entity → This entity
|
|
1508
|
+
"acme_corp" with incoming "CEO" from "jane_smith" means Acme HAS CEO Jane
|
|
1509
|
+
|
|
1510
|
+
Args:
|
|
1511
|
+
entity_id: The source entity's identifier
|
|
1512
|
+
entity_type: Type of source entity
|
|
1513
|
+
related_entity_id: The target entity's identifier (must exist or will be created)
|
|
1514
|
+
relation: Type of relationship - use clear, consistent labels:
|
|
1515
|
+
For roles: "CEO", "CTO", "engineer_at", "founder"
|
|
1516
|
+
For ownership: "owns", "owned_by", "part_of"
|
|
1517
|
+
For competition: "competitor_of", "partner_of"
|
|
1518
|
+
For technical: "uses", "depends_on", "integrates_with"
|
|
1519
|
+
direction: "outgoing" (source → target) or "incoming" (target → source)
|
|
1520
|
+
|
|
1521
|
+
Returns:
|
|
1522
|
+
Confirmation message with relationship ID.
|
|
1523
|
+
"""
|
|
1524
|
+
rel_id = self.add_relationship(
|
|
1525
|
+
entity_id=entity_id,
|
|
1526
|
+
entity_type=entity_type,
|
|
1527
|
+
related_entity_id=related_entity_id,
|
|
1528
|
+
relation=relation,
|
|
1529
|
+
direction=direction,
|
|
1530
|
+
user_id=user_id,
|
|
1531
|
+
agent_id=agent_id,
|
|
1532
|
+
team_id=team_id,
|
|
1533
|
+
namespace=namespace,
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
if rel_id:
|
|
1537
|
+
self.entity_updated = True
|
|
1538
|
+
return f"Relationship added: {entity_id} --[{relation}]--> {related_entity_id} (id: {rel_id})"
|
|
1539
|
+
return "Failed to add relationship (entity may not exist)"
|
|
1540
|
+
|
|
1541
|
+
return add_relationship
|
|
1542
|
+
|
|
1543
|
+
def _create_async_add_relationship_tool(
|
|
1544
|
+
self,
|
|
1545
|
+
user_id: Optional[str] = None,
|
|
1546
|
+
agent_id: Optional[str] = None,
|
|
1547
|
+
team_id: Optional[str] = None,
|
|
1548
|
+
namespace: Optional[str] = None,
|
|
1549
|
+
) -> Callable:
|
|
1550
|
+
"""Create the async add_relationship tool."""
|
|
1551
|
+
|
|
1552
|
+
async def add_relationship(
|
|
1553
|
+
entity_id: str,
|
|
1554
|
+
entity_type: str,
|
|
1555
|
+
related_entity_id: str,
|
|
1556
|
+
relation: str,
|
|
1557
|
+
direction: str = "outgoing",
|
|
1558
|
+
) -> str:
|
|
1559
|
+
"""Add a relationship between two entities.
|
|
1560
|
+
|
|
1561
|
+
Relationships are **graph edges** connecting entities - they capture
|
|
1562
|
+
how entities relate to each other.
|
|
1563
|
+
|
|
1564
|
+
**Common relationship patterns:**
|
|
1565
|
+
|
|
1566
|
+
People → Companies:
|
|
1567
|
+
- "jane_smith" --[CEO]--> "acme_corp"
|
|
1568
|
+
- "bob_jones" --[engineer_at]--> "acme_corp"
|
|
1569
|
+
- "sarah_chen" --[founder]--> "startup_xyz"
|
|
1570
|
+
|
|
1571
|
+
Companies → Companies:
|
|
1572
|
+
- "acme_corp" --[competitor_of]--> "beta_inc"
|
|
1573
|
+
- "acme_corp" --[acquired]--> "small_startup"
|
|
1574
|
+
- "acme_corp" --[partner_of]--> "big_vendor"
|
|
1575
|
+
|
|
1576
|
+
Projects → Other entities:
|
|
1577
|
+
- "project_atlas" --[uses]--> "postgresql"
|
|
1578
|
+
- "project_atlas" --[owned_by]--> "acme_corp"
|
|
1579
|
+
- "project_atlas" --[led_by]--> "jane_smith"
|
|
1580
|
+
|
|
1581
|
+
**Direction matters:**
|
|
1582
|
+
- "outgoing": This entity → Related entity (default)
|
|
1583
|
+
"jane_smith" --[CEO]--> "acme_corp" means Jane IS CEO OF Acme
|
|
1584
|
+
- "incoming": Related entity → This entity
|
|
1585
|
+
"acme_corp" with incoming "CEO" from "jane_smith" means Acme HAS CEO Jane
|
|
1586
|
+
|
|
1587
|
+
Args:
|
|
1588
|
+
entity_id: The source entity's identifier
|
|
1589
|
+
entity_type: Type of source entity
|
|
1590
|
+
related_entity_id: The target entity's identifier (must exist or will be created)
|
|
1591
|
+
relation: Type of relationship - use clear, consistent labels:
|
|
1592
|
+
For roles: "CEO", "CTO", "engineer_at", "founder"
|
|
1593
|
+
For ownership: "owns", "owned_by", "part_of"
|
|
1594
|
+
For competition: "competitor_of", "partner_of"
|
|
1595
|
+
For technical: "uses", "depends_on", "integrates_with"
|
|
1596
|
+
direction: "outgoing" (source → target) or "incoming" (target → source)
|
|
1597
|
+
|
|
1598
|
+
Returns:
|
|
1599
|
+
Confirmation message with relationship ID.
|
|
1600
|
+
"""
|
|
1601
|
+
rel_id = await self.aadd_relationship(
|
|
1602
|
+
entity_id=entity_id,
|
|
1603
|
+
entity_type=entity_type,
|
|
1604
|
+
related_entity_id=related_entity_id,
|
|
1605
|
+
relation=relation,
|
|
1606
|
+
direction=direction,
|
|
1607
|
+
user_id=user_id,
|
|
1608
|
+
agent_id=agent_id,
|
|
1609
|
+
team_id=team_id,
|
|
1610
|
+
namespace=namespace,
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
if rel_id:
|
|
1614
|
+
self.entity_updated = True
|
|
1615
|
+
return f"Relationship added: {entity_id} --[{relation}]--> {related_entity_id} (id: {rel_id})"
|
|
1616
|
+
return "Failed to add relationship (entity may not exist)"
|
|
1617
|
+
|
|
1618
|
+
return add_relationship
|
|
1619
|
+
|
|
1620
|
+
# =========================================================================
|
|
1621
|
+
# Read Operations
|
|
1622
|
+
# =========================================================================
|
|
1623
|
+
|
|
1624
|
+
def get(
|
|
1625
|
+
self,
|
|
1626
|
+
entity_id: str,
|
|
1627
|
+
entity_type: str,
|
|
1628
|
+
user_id: Optional[str] = None,
|
|
1629
|
+
namespace: Optional[str] = None,
|
|
1630
|
+
) -> Optional[EntityMemory]:
|
|
1631
|
+
"""Retrieve entity by entity_id and entity_type.
|
|
1632
|
+
|
|
1633
|
+
Args:
|
|
1634
|
+
entity_id: The unique entity identifier.
|
|
1635
|
+
entity_type: The type of entity.
|
|
1636
|
+
user_id: User ID for "user" namespace scoping.
|
|
1637
|
+
namespace: Namespace to search in.
|
|
1638
|
+
|
|
1639
|
+
Returns:
|
|
1640
|
+
EntityMemory instance, or None if not found.
|
|
1641
|
+
"""
|
|
1642
|
+
if not self.db:
|
|
1643
|
+
return None
|
|
1644
|
+
|
|
1645
|
+
effective_namespace = namespace or self.config.namespace
|
|
1646
|
+
|
|
1647
|
+
try:
|
|
1648
|
+
result = self.db.get_learning(
|
|
1649
|
+
learning_type=self.learning_type,
|
|
1650
|
+
entity_id=entity_id,
|
|
1651
|
+
entity_type=entity_type,
|
|
1652
|
+
namespace=effective_namespace,
|
|
1653
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
1654
|
+
)
|
|
1655
|
+
|
|
1656
|
+
if result and result.get("content"): # type: ignore[union-attr]
|
|
1657
|
+
return self.schema.from_dict(result["content"]) # type: ignore[index]
|
|
1658
|
+
|
|
1659
|
+
return None
|
|
1660
|
+
|
|
1661
|
+
except Exception as e:
|
|
1662
|
+
log_debug(f"EntityMemoryStore.get failed for {entity_type}/{entity_id}: {e}")
|
|
1663
|
+
return None
|
|
1664
|
+
|
|
1665
|
+
async def aget(
|
|
1666
|
+
self,
|
|
1667
|
+
entity_id: str,
|
|
1668
|
+
entity_type: str,
|
|
1669
|
+
user_id: Optional[str] = None,
|
|
1670
|
+
namespace: Optional[str] = None,
|
|
1671
|
+
) -> Optional[EntityMemory]:
|
|
1672
|
+
"""Async version of get."""
|
|
1673
|
+
if not self.db:
|
|
1674
|
+
return None
|
|
1675
|
+
|
|
1676
|
+
effective_namespace = namespace or self.config.namespace
|
|
1677
|
+
|
|
1678
|
+
try:
|
|
1679
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
1680
|
+
result = await self.db.get_learning(
|
|
1681
|
+
learning_type=self.learning_type,
|
|
1682
|
+
entity_id=entity_id,
|
|
1683
|
+
entity_type=entity_type,
|
|
1684
|
+
namespace=effective_namespace,
|
|
1685
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
1686
|
+
)
|
|
1687
|
+
else:
|
|
1688
|
+
result = self.db.get_learning(
|
|
1689
|
+
learning_type=self.learning_type,
|
|
1690
|
+
entity_id=entity_id,
|
|
1691
|
+
entity_type=entity_type,
|
|
1692
|
+
namespace=effective_namespace,
|
|
1693
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
1694
|
+
)
|
|
1695
|
+
|
|
1696
|
+
if result and result.get("content"):
|
|
1697
|
+
return self.schema.from_dict(result["content"])
|
|
1698
|
+
|
|
1699
|
+
return None
|
|
1700
|
+
|
|
1701
|
+
except Exception as e:
|
|
1702
|
+
log_debug(f"EntityMemoryStore.aget failed for {entity_type}/{entity_id}: {e}")
|
|
1703
|
+
return None
|
|
1704
|
+
|
|
1705
|
+
# =========================================================================
|
|
1706
|
+
# Search Operations
|
|
1707
|
+
# =========================================================================
|
|
1708
|
+
|
|
1709
|
+
def search(
|
|
1710
|
+
self,
|
|
1711
|
+
query: str,
|
|
1712
|
+
entity_type: Optional[str] = None,
|
|
1713
|
+
user_id: Optional[str] = None,
|
|
1714
|
+
namespace: Optional[str] = None,
|
|
1715
|
+
limit: int = 10,
|
|
1716
|
+
) -> List[EntityMemory]:
|
|
1717
|
+
"""Search for entities matching query.
|
|
1718
|
+
|
|
1719
|
+
Args:
|
|
1720
|
+
query: Search query (matched against name, facts, events, etc.).
|
|
1721
|
+
entity_type: Filter by entity type.
|
|
1722
|
+
user_id: User ID for "user" namespace scoping.
|
|
1723
|
+
namespace: Filter by namespace.
|
|
1724
|
+
limit: Maximum results to return.
|
|
1725
|
+
|
|
1726
|
+
Returns:
|
|
1727
|
+
List of matching EntityMemory objects.
|
|
1728
|
+
"""
|
|
1729
|
+
if not self.db:
|
|
1730
|
+
return []
|
|
1731
|
+
|
|
1732
|
+
effective_namespace = namespace or self.config.namespace
|
|
1733
|
+
|
|
1734
|
+
try:
|
|
1735
|
+
results = self.db.get_learnings(
|
|
1736
|
+
learning_type=self.learning_type,
|
|
1737
|
+
entity_type=entity_type,
|
|
1738
|
+
namespace=effective_namespace,
|
|
1739
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
1740
|
+
limit=limit * 3, # Over-fetch for filtering
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
entities = []
|
|
1744
|
+
query_lower = query.lower()
|
|
1745
|
+
|
|
1746
|
+
for result in results or []: # type: ignore[union-attr]
|
|
1747
|
+
content = result.get("content", {})
|
|
1748
|
+
if self._matches_query(content=content, query=query_lower):
|
|
1749
|
+
entity = self.schema.from_dict(content)
|
|
1750
|
+
if entity:
|
|
1751
|
+
entities.append(entity)
|
|
1752
|
+
|
|
1753
|
+
if len(entities) >= limit:
|
|
1754
|
+
break
|
|
1755
|
+
|
|
1756
|
+
log_debug(f"EntityMemoryStore.search: found {len(entities)} entities for query: {query[:50]}...")
|
|
1757
|
+
return entities
|
|
1758
|
+
|
|
1759
|
+
except Exception as e:
|
|
1760
|
+
log_debug(f"EntityMemoryStore.search failed: {e}")
|
|
1761
|
+
return []
|
|
1762
|
+
|
|
1763
|
+
async def asearch(
|
|
1764
|
+
self,
|
|
1765
|
+
query: str,
|
|
1766
|
+
entity_type: Optional[str] = None,
|
|
1767
|
+
user_id: Optional[str] = None,
|
|
1768
|
+
namespace: Optional[str] = None,
|
|
1769
|
+
limit: int = 10,
|
|
1770
|
+
) -> List[EntityMemory]:
|
|
1771
|
+
"""Async version of search."""
|
|
1772
|
+
if not self.db:
|
|
1773
|
+
return []
|
|
1774
|
+
|
|
1775
|
+
effective_namespace = namespace or self.config.namespace
|
|
1776
|
+
|
|
1777
|
+
try:
|
|
1778
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
1779
|
+
results = await self.db.get_learnings(
|
|
1780
|
+
learning_type=self.learning_type,
|
|
1781
|
+
entity_type=entity_type,
|
|
1782
|
+
namespace=effective_namespace,
|
|
1783
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
1784
|
+
limit=limit * 3,
|
|
1785
|
+
)
|
|
1786
|
+
else:
|
|
1787
|
+
results = self.db.get_learnings(
|
|
1788
|
+
learning_type=self.learning_type,
|
|
1789
|
+
entity_type=entity_type,
|
|
1790
|
+
namespace=effective_namespace,
|
|
1791
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
1792
|
+
limit=limit * 3,
|
|
1793
|
+
)
|
|
1794
|
+
|
|
1795
|
+
entities = []
|
|
1796
|
+
query_lower = query.lower()
|
|
1797
|
+
|
|
1798
|
+
for result in results or []:
|
|
1799
|
+
content = result.get("content", {})
|
|
1800
|
+
if self._matches_query(content=content, query=query_lower):
|
|
1801
|
+
entity = self.schema.from_dict(content)
|
|
1802
|
+
if entity:
|
|
1803
|
+
entities.append(entity)
|
|
1804
|
+
|
|
1805
|
+
if len(entities) >= limit:
|
|
1806
|
+
break
|
|
1807
|
+
|
|
1808
|
+
log_debug(f"EntityMemoryStore.asearch: found {len(entities)} entities for query: {query[:50]}...")
|
|
1809
|
+
return entities
|
|
1810
|
+
|
|
1811
|
+
except Exception as e:
|
|
1812
|
+
log_debug(f"EntityMemoryStore.asearch failed: {e}")
|
|
1813
|
+
return []
|
|
1814
|
+
|
|
1815
|
+
def _matches_query(self, content: Dict[str, Any], query: str) -> bool:
|
|
1816
|
+
"""Check if entity content matches search query."""
|
|
1817
|
+
# Check name
|
|
1818
|
+
name = content.get("name", "")
|
|
1819
|
+
if name and query in name.lower():
|
|
1820
|
+
return True
|
|
1821
|
+
|
|
1822
|
+
# Check entity_id
|
|
1823
|
+
entity_id = content.get("entity_id", "")
|
|
1824
|
+
if entity_id and query in entity_id.lower():
|
|
1825
|
+
return True
|
|
1826
|
+
|
|
1827
|
+
# Check description
|
|
1828
|
+
description = content.get("description", "")
|
|
1829
|
+
if description and query in description.lower():
|
|
1830
|
+
return True
|
|
1831
|
+
|
|
1832
|
+
# Check properties
|
|
1833
|
+
properties = content.get("properties", {})
|
|
1834
|
+
for value in properties.values():
|
|
1835
|
+
if query in str(value).lower():
|
|
1836
|
+
return True
|
|
1837
|
+
|
|
1838
|
+
# Check facts
|
|
1839
|
+
facts = content.get("facts", [])
|
|
1840
|
+
for fact in facts:
|
|
1841
|
+
fact_content = fact.get("content", "") if isinstance(fact, dict) else str(fact)
|
|
1842
|
+
if query in fact_content.lower():
|
|
1843
|
+
return True
|
|
1844
|
+
|
|
1845
|
+
# Check events
|
|
1846
|
+
events = content.get("events", [])
|
|
1847
|
+
for event in events:
|
|
1848
|
+
event_content = event.get("content", "") if isinstance(event, dict) else str(event)
|
|
1849
|
+
if query in event_content.lower():
|
|
1850
|
+
return True
|
|
1851
|
+
|
|
1852
|
+
# Check relationships
|
|
1853
|
+
relationships = content.get("relationships", [])
|
|
1854
|
+
for rel in relationships:
|
|
1855
|
+
if isinstance(rel, dict):
|
|
1856
|
+
if query in rel.get("entity_id", "").lower():
|
|
1857
|
+
return True
|
|
1858
|
+
if query in rel.get("relation", "").lower():
|
|
1859
|
+
return True
|
|
1860
|
+
|
|
1861
|
+
return False
|
|
1862
|
+
|
|
1863
|
+
# =========================================================================
|
|
1864
|
+
# Create Operations
|
|
1865
|
+
# =========================================================================
|
|
1866
|
+
|
|
1867
|
+
def create_entity(
|
|
1868
|
+
self,
|
|
1869
|
+
entity_id: str,
|
|
1870
|
+
entity_type: str,
|
|
1871
|
+
name: str,
|
|
1872
|
+
description: Optional[str] = None,
|
|
1873
|
+
properties: Optional[Dict[str, str]] = None,
|
|
1874
|
+
user_id: Optional[str] = None,
|
|
1875
|
+
agent_id: Optional[str] = None,
|
|
1876
|
+
team_id: Optional[str] = None,
|
|
1877
|
+
namespace: Optional[str] = None,
|
|
1878
|
+
) -> bool:
|
|
1879
|
+
"""Create a new entity.
|
|
1880
|
+
|
|
1881
|
+
Args:
|
|
1882
|
+
entity_id: Unique identifier for the entity.
|
|
1883
|
+
entity_type: Type of entity.
|
|
1884
|
+
name: Display name.
|
|
1885
|
+
description: Brief description.
|
|
1886
|
+
properties: Key-value properties.
|
|
1887
|
+
user_id: User ID (required for "user" namespace).
|
|
1888
|
+
agent_id: Agent context (stored for audit).
|
|
1889
|
+
team_id: Team context (stored for audit).
|
|
1890
|
+
namespace: Namespace for scoping.
|
|
1891
|
+
|
|
1892
|
+
Returns:
|
|
1893
|
+
True if created, False if already exists or error.
|
|
1894
|
+
"""
|
|
1895
|
+
if not self.db:
|
|
1896
|
+
return False
|
|
1897
|
+
|
|
1898
|
+
effective_namespace = namespace or self.config.namespace
|
|
1899
|
+
|
|
1900
|
+
# Validate "user" namespace has user_id
|
|
1901
|
+
if effective_namespace == "user" and not user_id:
|
|
1902
|
+
log_warning("EntityMemoryStore.create_entity: 'user' namespace requires user_id")
|
|
1903
|
+
return False
|
|
1904
|
+
|
|
1905
|
+
# Check if already exists
|
|
1906
|
+
existing = self.get(
|
|
1907
|
+
entity_id=entity_id,
|
|
1908
|
+
entity_type=entity_type,
|
|
1909
|
+
user_id=user_id,
|
|
1910
|
+
namespace=effective_namespace,
|
|
1911
|
+
)
|
|
1912
|
+
if existing:
|
|
1913
|
+
log_debug(f"EntityMemoryStore.create_entity: entity already exists {entity_type}/{entity_id}")
|
|
1914
|
+
return False
|
|
1915
|
+
|
|
1916
|
+
try:
|
|
1917
|
+
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
1918
|
+
|
|
1919
|
+
entity = self.schema(
|
|
1920
|
+
entity_id=entity_id,
|
|
1921
|
+
entity_type=entity_type,
|
|
1922
|
+
name=name,
|
|
1923
|
+
description=description,
|
|
1924
|
+
properties=properties or {},
|
|
1925
|
+
facts=[],
|
|
1926
|
+
events=[],
|
|
1927
|
+
relationships=[],
|
|
1928
|
+
namespace=effective_namespace,
|
|
1929
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
1930
|
+
agent_id=agent_id,
|
|
1931
|
+
team_id=team_id,
|
|
1932
|
+
created_at=now,
|
|
1933
|
+
updated_at=now,
|
|
1934
|
+
)
|
|
1935
|
+
|
|
1936
|
+
self.db.upsert_learning(
|
|
1937
|
+
id=self._build_entity_db_id(entity_id, entity_type, effective_namespace),
|
|
1938
|
+
learning_type=self.learning_type,
|
|
1939
|
+
entity_id=entity_id,
|
|
1940
|
+
entity_type=entity_type,
|
|
1941
|
+
namespace=effective_namespace,
|
|
1942
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
1943
|
+
agent_id=agent_id,
|
|
1944
|
+
team_id=team_id,
|
|
1945
|
+
content=entity.to_dict(),
|
|
1946
|
+
)
|
|
1947
|
+
|
|
1948
|
+
log_debug(f"EntityMemoryStore.create_entity: created {entity_type}/{entity_id}")
|
|
1949
|
+
return True
|
|
1950
|
+
|
|
1951
|
+
except Exception as e:
|
|
1952
|
+
log_debug(f"EntityMemoryStore.create_entity failed: {e}")
|
|
1953
|
+
return False
|
|
1954
|
+
|
|
1955
|
+
async def acreate_entity(
|
|
1956
|
+
self,
|
|
1957
|
+
entity_id: str,
|
|
1958
|
+
entity_type: str,
|
|
1959
|
+
name: str,
|
|
1960
|
+
description: Optional[str] = None,
|
|
1961
|
+
properties: Optional[Dict[str, str]] = None,
|
|
1962
|
+
user_id: Optional[str] = None,
|
|
1963
|
+
agent_id: Optional[str] = None,
|
|
1964
|
+
team_id: Optional[str] = None,
|
|
1965
|
+
namespace: Optional[str] = None,
|
|
1966
|
+
) -> bool:
|
|
1967
|
+
"""Async version of create_entity."""
|
|
1968
|
+
if not self.db:
|
|
1969
|
+
return False
|
|
1970
|
+
|
|
1971
|
+
effective_namespace = namespace or self.config.namespace
|
|
1972
|
+
|
|
1973
|
+
if effective_namespace == "user" and not user_id:
|
|
1974
|
+
log_warning("EntityMemoryStore.acreate_entity: 'user' namespace requires user_id")
|
|
1975
|
+
return False
|
|
1976
|
+
|
|
1977
|
+
existing = await self.aget(
|
|
1978
|
+
entity_id=entity_id,
|
|
1979
|
+
entity_type=entity_type,
|
|
1980
|
+
user_id=user_id,
|
|
1981
|
+
namespace=effective_namespace,
|
|
1982
|
+
)
|
|
1983
|
+
if existing:
|
|
1984
|
+
log_debug(f"EntityMemoryStore.acreate_entity: entity already exists {entity_type}/{entity_id}")
|
|
1985
|
+
return False
|
|
1986
|
+
|
|
1987
|
+
try:
|
|
1988
|
+
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
1989
|
+
|
|
1990
|
+
entity = self.schema(
|
|
1991
|
+
entity_id=entity_id,
|
|
1992
|
+
entity_type=entity_type,
|
|
1993
|
+
name=name,
|
|
1994
|
+
description=description,
|
|
1995
|
+
properties=properties or {},
|
|
1996
|
+
facts=[],
|
|
1997
|
+
events=[],
|
|
1998
|
+
relationships=[],
|
|
1999
|
+
namespace=effective_namespace,
|
|
2000
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
2001
|
+
agent_id=agent_id,
|
|
2002
|
+
team_id=team_id,
|
|
2003
|
+
created_at=now,
|
|
2004
|
+
updated_at=now,
|
|
2005
|
+
)
|
|
2006
|
+
|
|
2007
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
2008
|
+
await self.db.upsert_learning(
|
|
2009
|
+
id=self._build_entity_db_id(entity_id, entity_type, effective_namespace),
|
|
2010
|
+
learning_type=self.learning_type,
|
|
2011
|
+
entity_id=entity_id,
|
|
2012
|
+
entity_type=entity_type,
|
|
2013
|
+
namespace=effective_namespace,
|
|
2014
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
2015
|
+
agent_id=agent_id,
|
|
2016
|
+
team_id=team_id,
|
|
2017
|
+
content=entity.to_dict(),
|
|
2018
|
+
)
|
|
2019
|
+
else:
|
|
2020
|
+
self.db.upsert_learning(
|
|
2021
|
+
id=self._build_entity_db_id(entity_id, entity_type, effective_namespace),
|
|
2022
|
+
learning_type=self.learning_type,
|
|
2023
|
+
entity_id=entity_id,
|
|
2024
|
+
entity_type=entity_type,
|
|
2025
|
+
namespace=effective_namespace,
|
|
2026
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
2027
|
+
agent_id=agent_id,
|
|
2028
|
+
team_id=team_id,
|
|
2029
|
+
content=entity.to_dict(),
|
|
2030
|
+
)
|
|
2031
|
+
|
|
2032
|
+
log_debug(f"EntityMemoryStore.acreate_entity: created {entity_type}/{entity_id}")
|
|
2033
|
+
return True
|
|
2034
|
+
|
|
2035
|
+
except Exception as e:
|
|
2036
|
+
log_debug(f"EntityMemoryStore.acreate_entity failed: {e}")
|
|
2037
|
+
return False
|
|
2038
|
+
|
|
2039
|
+
# =========================================================================
|
|
2040
|
+
# Update Operations
|
|
2041
|
+
# =========================================================================
|
|
2042
|
+
|
|
2043
|
+
def update_entity(
|
|
2044
|
+
self,
|
|
2045
|
+
entity_id: str,
|
|
2046
|
+
entity_type: str,
|
|
2047
|
+
name: Optional[str] = None,
|
|
2048
|
+
description: Optional[str] = None,
|
|
2049
|
+
properties: Optional[Dict[str, str]] = None,
|
|
2050
|
+
user_id: Optional[str] = None,
|
|
2051
|
+
agent_id: Optional[str] = None,
|
|
2052
|
+
team_id: Optional[str] = None,
|
|
2053
|
+
namespace: Optional[str] = None,
|
|
2054
|
+
) -> bool:
|
|
2055
|
+
"""Update an existing entity's core properties.
|
|
2056
|
+
|
|
2057
|
+
Args:
|
|
2058
|
+
entity_id: The entity's identifier.
|
|
2059
|
+
entity_type: Type of entity.
|
|
2060
|
+
name: New display name (optional).
|
|
2061
|
+
description: New description (optional).
|
|
2062
|
+
properties: Properties to merge (optional).
|
|
2063
|
+
user_id: User ID for namespace scoping.
|
|
2064
|
+
agent_id: Agent context.
|
|
2065
|
+
team_id: Team context.
|
|
2066
|
+
namespace: Namespace to search in.
|
|
2067
|
+
|
|
2068
|
+
Returns:
|
|
2069
|
+
True if updated, False if not found.
|
|
2070
|
+
"""
|
|
2071
|
+
effective_namespace = namespace or self.config.namespace
|
|
2072
|
+
|
|
2073
|
+
entity = self.get(
|
|
2074
|
+
entity_id=entity_id,
|
|
2075
|
+
entity_type=entity_type,
|
|
2076
|
+
user_id=user_id,
|
|
2077
|
+
namespace=effective_namespace,
|
|
2078
|
+
)
|
|
2079
|
+
|
|
2080
|
+
if not entity:
|
|
2081
|
+
return False
|
|
2082
|
+
|
|
2083
|
+
# Update fields
|
|
2084
|
+
if name is not None:
|
|
2085
|
+
entity.name = name
|
|
2086
|
+
if description is not None:
|
|
2087
|
+
entity.description = description
|
|
2088
|
+
if properties is not None:
|
|
2089
|
+
entity.properties = {**(entity.properties or {}), **properties}
|
|
2090
|
+
|
|
2091
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2092
|
+
|
|
2093
|
+
return self._save_entity(
|
|
2094
|
+
entity=entity,
|
|
2095
|
+
user_id=user_id,
|
|
2096
|
+
agent_id=agent_id,
|
|
2097
|
+
team_id=team_id,
|
|
2098
|
+
namespace=effective_namespace,
|
|
2099
|
+
)
|
|
2100
|
+
|
|
2101
|
+
async def aupdate_entity(
|
|
2102
|
+
self,
|
|
2103
|
+
entity_id: str,
|
|
2104
|
+
entity_type: str,
|
|
2105
|
+
name: Optional[str] = None,
|
|
2106
|
+
description: Optional[str] = None,
|
|
2107
|
+
properties: Optional[Dict[str, str]] = None,
|
|
2108
|
+
user_id: Optional[str] = None,
|
|
2109
|
+
agent_id: Optional[str] = None,
|
|
2110
|
+
team_id: Optional[str] = None,
|
|
2111
|
+
namespace: Optional[str] = None,
|
|
2112
|
+
) -> bool:
|
|
2113
|
+
"""Async version of update_entity."""
|
|
2114
|
+
effective_namespace = namespace or self.config.namespace
|
|
2115
|
+
|
|
2116
|
+
entity = await self.aget(
|
|
2117
|
+
entity_id=entity_id,
|
|
2118
|
+
entity_type=entity_type,
|
|
2119
|
+
user_id=user_id,
|
|
2120
|
+
namespace=effective_namespace,
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
if not entity:
|
|
2124
|
+
return False
|
|
2125
|
+
|
|
2126
|
+
if name is not None:
|
|
2127
|
+
entity.name = name
|
|
2128
|
+
if description is not None:
|
|
2129
|
+
entity.description = description
|
|
2130
|
+
if properties is not None:
|
|
2131
|
+
entity.properties = {**(entity.properties or {}), **properties}
|
|
2132
|
+
|
|
2133
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2134
|
+
|
|
2135
|
+
return await self._asave_entity(
|
|
2136
|
+
entity=entity,
|
|
2137
|
+
user_id=user_id,
|
|
2138
|
+
agent_id=agent_id,
|
|
2139
|
+
team_id=team_id,
|
|
2140
|
+
namespace=effective_namespace,
|
|
2141
|
+
)
|
|
2142
|
+
|
|
2143
|
+
# =========================================================================
|
|
2144
|
+
# Fact Operations
|
|
2145
|
+
# =========================================================================
|
|
2146
|
+
|
|
2147
|
+
def add_fact(
|
|
2148
|
+
self,
|
|
2149
|
+
entity_id: str,
|
|
2150
|
+
entity_type: str,
|
|
2151
|
+
fact: str,
|
|
2152
|
+
user_id: Optional[str] = None,
|
|
2153
|
+
agent_id: Optional[str] = None,
|
|
2154
|
+
team_id: Optional[str] = None,
|
|
2155
|
+
namespace: Optional[str] = None,
|
|
2156
|
+
) -> Optional[str]:
|
|
2157
|
+
"""Add a fact to an entity.
|
|
2158
|
+
|
|
2159
|
+
Returns:
|
|
2160
|
+
Fact ID if added, None if entity not found.
|
|
2161
|
+
"""
|
|
2162
|
+
effective_namespace = namespace or self.config.namespace
|
|
2163
|
+
|
|
2164
|
+
entity = self.get(
|
|
2165
|
+
entity_id=entity_id,
|
|
2166
|
+
entity_type=entity_type,
|
|
2167
|
+
user_id=user_id,
|
|
2168
|
+
namespace=effective_namespace,
|
|
2169
|
+
)
|
|
2170
|
+
|
|
2171
|
+
if not entity:
|
|
2172
|
+
return None
|
|
2173
|
+
|
|
2174
|
+
fact_id = entity.add_fact(fact)
|
|
2175
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2176
|
+
|
|
2177
|
+
success = self._save_entity(
|
|
2178
|
+
entity=entity,
|
|
2179
|
+
user_id=user_id,
|
|
2180
|
+
agent_id=agent_id,
|
|
2181
|
+
team_id=team_id,
|
|
2182
|
+
namespace=effective_namespace,
|
|
2183
|
+
)
|
|
2184
|
+
|
|
2185
|
+
return fact_id if success else None
|
|
2186
|
+
|
|
2187
|
+
async def aadd_fact(
|
|
2188
|
+
self,
|
|
2189
|
+
entity_id: str,
|
|
2190
|
+
entity_type: str,
|
|
2191
|
+
fact: str,
|
|
2192
|
+
user_id: Optional[str] = None,
|
|
2193
|
+
agent_id: Optional[str] = None,
|
|
2194
|
+
team_id: Optional[str] = None,
|
|
2195
|
+
namespace: Optional[str] = None,
|
|
2196
|
+
) -> Optional[str]:
|
|
2197
|
+
"""Async version of add_fact."""
|
|
2198
|
+
effective_namespace = namespace or self.config.namespace
|
|
2199
|
+
|
|
2200
|
+
entity = await self.aget(
|
|
2201
|
+
entity_id=entity_id,
|
|
2202
|
+
entity_type=entity_type,
|
|
2203
|
+
user_id=user_id,
|
|
2204
|
+
namespace=effective_namespace,
|
|
2205
|
+
)
|
|
2206
|
+
|
|
2207
|
+
if not entity:
|
|
2208
|
+
return None
|
|
2209
|
+
|
|
2210
|
+
fact_id = entity.add_fact(fact)
|
|
2211
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2212
|
+
|
|
2213
|
+
success = await self._asave_entity(
|
|
2214
|
+
entity=entity,
|
|
2215
|
+
user_id=user_id,
|
|
2216
|
+
agent_id=agent_id,
|
|
2217
|
+
team_id=team_id,
|
|
2218
|
+
namespace=effective_namespace,
|
|
2219
|
+
)
|
|
2220
|
+
|
|
2221
|
+
return fact_id if success else None
|
|
2222
|
+
|
|
2223
|
+
def update_fact(
|
|
2224
|
+
self,
|
|
2225
|
+
entity_id: str,
|
|
2226
|
+
entity_type: str,
|
|
2227
|
+
fact_id: str,
|
|
2228
|
+
fact: str,
|
|
2229
|
+
user_id: Optional[str] = None,
|
|
2230
|
+
agent_id: Optional[str] = None,
|
|
2231
|
+
team_id: Optional[str] = None,
|
|
2232
|
+
namespace: Optional[str] = None,
|
|
2233
|
+
) -> bool:
|
|
2234
|
+
"""Update an existing fact."""
|
|
2235
|
+
effective_namespace = namespace or self.config.namespace
|
|
2236
|
+
|
|
2237
|
+
entity = self.get(
|
|
2238
|
+
entity_id=entity_id,
|
|
2239
|
+
entity_type=entity_type,
|
|
2240
|
+
user_id=user_id,
|
|
2241
|
+
namespace=effective_namespace,
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
if not entity:
|
|
2245
|
+
return False
|
|
2246
|
+
|
|
2247
|
+
if not entity.update_fact(fact_id, fact):
|
|
2248
|
+
return False
|
|
2249
|
+
|
|
2250
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2251
|
+
|
|
2252
|
+
return self._save_entity(
|
|
2253
|
+
entity=entity,
|
|
2254
|
+
user_id=user_id,
|
|
2255
|
+
agent_id=agent_id,
|
|
2256
|
+
team_id=team_id,
|
|
2257
|
+
namespace=effective_namespace,
|
|
2258
|
+
)
|
|
2259
|
+
|
|
2260
|
+
async def aupdate_fact(
|
|
2261
|
+
self,
|
|
2262
|
+
entity_id: str,
|
|
2263
|
+
entity_type: str,
|
|
2264
|
+
fact_id: str,
|
|
2265
|
+
fact: str,
|
|
2266
|
+
user_id: Optional[str] = None,
|
|
2267
|
+
agent_id: Optional[str] = None,
|
|
2268
|
+
team_id: Optional[str] = None,
|
|
2269
|
+
namespace: Optional[str] = None,
|
|
2270
|
+
) -> bool:
|
|
2271
|
+
"""Async version of update_fact."""
|
|
2272
|
+
effective_namespace = namespace or self.config.namespace
|
|
2273
|
+
|
|
2274
|
+
entity = await self.aget(
|
|
2275
|
+
entity_id=entity_id,
|
|
2276
|
+
entity_type=entity_type,
|
|
2277
|
+
user_id=user_id,
|
|
2278
|
+
namespace=effective_namespace,
|
|
2279
|
+
)
|
|
2280
|
+
|
|
2281
|
+
if not entity:
|
|
2282
|
+
return False
|
|
2283
|
+
|
|
2284
|
+
if not entity.update_fact(fact_id, fact):
|
|
2285
|
+
return False
|
|
2286
|
+
|
|
2287
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2288
|
+
|
|
2289
|
+
return await self._asave_entity(
|
|
2290
|
+
entity=entity,
|
|
2291
|
+
user_id=user_id,
|
|
2292
|
+
agent_id=agent_id,
|
|
2293
|
+
team_id=team_id,
|
|
2294
|
+
namespace=effective_namespace,
|
|
2295
|
+
)
|
|
2296
|
+
|
|
2297
|
+
def delete_fact(
|
|
2298
|
+
self,
|
|
2299
|
+
entity_id: str,
|
|
2300
|
+
entity_type: str,
|
|
2301
|
+
fact_id: str,
|
|
2302
|
+
user_id: Optional[str] = None,
|
|
2303
|
+
agent_id: Optional[str] = None,
|
|
2304
|
+
team_id: Optional[str] = None,
|
|
2305
|
+
namespace: Optional[str] = None,
|
|
2306
|
+
) -> bool:
|
|
2307
|
+
"""Delete a fact from an entity."""
|
|
2308
|
+
effective_namespace = namespace or self.config.namespace
|
|
2309
|
+
|
|
2310
|
+
entity = self.get(
|
|
2311
|
+
entity_id=entity_id,
|
|
2312
|
+
entity_type=entity_type,
|
|
2313
|
+
user_id=user_id,
|
|
2314
|
+
namespace=effective_namespace,
|
|
2315
|
+
)
|
|
2316
|
+
|
|
2317
|
+
if not entity:
|
|
2318
|
+
return False
|
|
2319
|
+
|
|
2320
|
+
if not entity.delete_fact(fact_id):
|
|
2321
|
+
return False
|
|
2322
|
+
|
|
2323
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2324
|
+
|
|
2325
|
+
return self._save_entity(
|
|
2326
|
+
entity=entity,
|
|
2327
|
+
user_id=user_id,
|
|
2328
|
+
agent_id=agent_id,
|
|
2329
|
+
team_id=team_id,
|
|
2330
|
+
namespace=effective_namespace,
|
|
2331
|
+
)
|
|
2332
|
+
|
|
2333
|
+
async def adelete_fact(
|
|
2334
|
+
self,
|
|
2335
|
+
entity_id: str,
|
|
2336
|
+
entity_type: str,
|
|
2337
|
+
fact_id: str,
|
|
2338
|
+
user_id: Optional[str] = None,
|
|
2339
|
+
agent_id: Optional[str] = None,
|
|
2340
|
+
team_id: Optional[str] = None,
|
|
2341
|
+
namespace: Optional[str] = None,
|
|
2342
|
+
) -> bool:
|
|
2343
|
+
"""Async version of delete_fact."""
|
|
2344
|
+
effective_namespace = namespace or self.config.namespace
|
|
2345
|
+
|
|
2346
|
+
entity = await self.aget(
|
|
2347
|
+
entity_id=entity_id,
|
|
2348
|
+
entity_type=entity_type,
|
|
2349
|
+
user_id=user_id,
|
|
2350
|
+
namespace=effective_namespace,
|
|
2351
|
+
)
|
|
2352
|
+
|
|
2353
|
+
if not entity:
|
|
2354
|
+
return False
|
|
2355
|
+
|
|
2356
|
+
if not entity.delete_fact(fact_id):
|
|
2357
|
+
return False
|
|
2358
|
+
|
|
2359
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2360
|
+
|
|
2361
|
+
return await self._asave_entity(
|
|
2362
|
+
entity=entity,
|
|
2363
|
+
user_id=user_id,
|
|
2364
|
+
agent_id=agent_id,
|
|
2365
|
+
team_id=team_id,
|
|
2366
|
+
namespace=effective_namespace,
|
|
2367
|
+
)
|
|
2368
|
+
|
|
2369
|
+
# =========================================================================
|
|
2370
|
+
# Event Operations
|
|
2371
|
+
# =========================================================================
|
|
2372
|
+
|
|
2373
|
+
def add_event(
|
|
2374
|
+
self,
|
|
2375
|
+
entity_id: str,
|
|
2376
|
+
entity_type: str,
|
|
2377
|
+
event: str,
|
|
2378
|
+
date: Optional[str] = None,
|
|
2379
|
+
user_id: Optional[str] = None,
|
|
2380
|
+
agent_id: Optional[str] = None,
|
|
2381
|
+
team_id: Optional[str] = None,
|
|
2382
|
+
namespace: Optional[str] = None,
|
|
2383
|
+
) -> Optional[str]:
|
|
2384
|
+
"""Add an event to an entity.
|
|
2385
|
+
|
|
2386
|
+
Returns:
|
|
2387
|
+
Event ID if added, None if entity not found.
|
|
2388
|
+
"""
|
|
2389
|
+
effective_namespace = namespace or self.config.namespace
|
|
2390
|
+
|
|
2391
|
+
entity = self.get(
|
|
2392
|
+
entity_id=entity_id,
|
|
2393
|
+
entity_type=entity_type,
|
|
2394
|
+
user_id=user_id,
|
|
2395
|
+
namespace=effective_namespace,
|
|
2396
|
+
)
|
|
2397
|
+
|
|
2398
|
+
if not entity:
|
|
2399
|
+
return None
|
|
2400
|
+
|
|
2401
|
+
event_id = entity.add_event(event, date=date)
|
|
2402
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2403
|
+
|
|
2404
|
+
success = self._save_entity(
|
|
2405
|
+
entity=entity,
|
|
2406
|
+
user_id=user_id,
|
|
2407
|
+
agent_id=agent_id,
|
|
2408
|
+
team_id=team_id,
|
|
2409
|
+
namespace=effective_namespace,
|
|
2410
|
+
)
|
|
2411
|
+
|
|
2412
|
+
return event_id if success else None
|
|
2413
|
+
|
|
2414
|
+
async def aadd_event(
|
|
2415
|
+
self,
|
|
2416
|
+
entity_id: str,
|
|
2417
|
+
entity_type: str,
|
|
2418
|
+
event: str,
|
|
2419
|
+
date: Optional[str] = None,
|
|
2420
|
+
user_id: Optional[str] = None,
|
|
2421
|
+
agent_id: Optional[str] = None,
|
|
2422
|
+
team_id: Optional[str] = None,
|
|
2423
|
+
namespace: Optional[str] = None,
|
|
2424
|
+
) -> Optional[str]:
|
|
2425
|
+
"""Async version of add_event."""
|
|
2426
|
+
effective_namespace = namespace or self.config.namespace
|
|
2427
|
+
|
|
2428
|
+
entity = await self.aget(
|
|
2429
|
+
entity_id=entity_id,
|
|
2430
|
+
entity_type=entity_type,
|
|
2431
|
+
user_id=user_id,
|
|
2432
|
+
namespace=effective_namespace,
|
|
2433
|
+
)
|
|
2434
|
+
|
|
2435
|
+
if not entity:
|
|
2436
|
+
return None
|
|
2437
|
+
|
|
2438
|
+
event_id = entity.add_event(event, date=date)
|
|
2439
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2440
|
+
|
|
2441
|
+
success = await self._asave_entity(
|
|
2442
|
+
entity=entity,
|
|
2443
|
+
user_id=user_id,
|
|
2444
|
+
agent_id=agent_id,
|
|
2445
|
+
team_id=team_id,
|
|
2446
|
+
namespace=effective_namespace,
|
|
2447
|
+
)
|
|
2448
|
+
|
|
2449
|
+
return event_id if success else None
|
|
2450
|
+
|
|
2451
|
+
# =========================================================================
|
|
2452
|
+
# Relationship Operations
|
|
2453
|
+
# =========================================================================
|
|
2454
|
+
|
|
2455
|
+
def add_relationship(
|
|
2456
|
+
self,
|
|
2457
|
+
entity_id: str,
|
|
2458
|
+
entity_type: str,
|
|
2459
|
+
related_entity_id: str,
|
|
2460
|
+
relation: str,
|
|
2461
|
+
direction: str = "outgoing",
|
|
2462
|
+
user_id: Optional[str] = None,
|
|
2463
|
+
agent_id: Optional[str] = None,
|
|
2464
|
+
team_id: Optional[str] = None,
|
|
2465
|
+
namespace: Optional[str] = None,
|
|
2466
|
+
) -> Optional[str]:
|
|
2467
|
+
"""Add a relationship to an entity.
|
|
2468
|
+
|
|
2469
|
+
Returns:
|
|
2470
|
+
Relationship ID if added, None if entity not found.
|
|
2471
|
+
"""
|
|
2472
|
+
effective_namespace = namespace or self.config.namespace
|
|
2473
|
+
|
|
2474
|
+
entity = self.get(
|
|
2475
|
+
entity_id=entity_id,
|
|
2476
|
+
entity_type=entity_type,
|
|
2477
|
+
user_id=user_id,
|
|
2478
|
+
namespace=effective_namespace,
|
|
2479
|
+
)
|
|
2480
|
+
|
|
2481
|
+
if not entity:
|
|
2482
|
+
return None
|
|
2483
|
+
|
|
2484
|
+
rel_id = entity.add_relationship(related_entity_id, relation, direction=direction)
|
|
2485
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2486
|
+
|
|
2487
|
+
success = self._save_entity(
|
|
2488
|
+
entity=entity,
|
|
2489
|
+
user_id=user_id,
|
|
2490
|
+
agent_id=agent_id,
|
|
2491
|
+
team_id=team_id,
|
|
2492
|
+
namespace=effective_namespace,
|
|
2493
|
+
)
|
|
2494
|
+
|
|
2495
|
+
return rel_id if success else None
|
|
2496
|
+
|
|
2497
|
+
async def aadd_relationship(
|
|
2498
|
+
self,
|
|
2499
|
+
entity_id: str,
|
|
2500
|
+
entity_type: str,
|
|
2501
|
+
related_entity_id: str,
|
|
2502
|
+
relation: str,
|
|
2503
|
+
direction: str = "outgoing",
|
|
2504
|
+
user_id: Optional[str] = None,
|
|
2505
|
+
agent_id: Optional[str] = None,
|
|
2506
|
+
team_id: Optional[str] = None,
|
|
2507
|
+
namespace: Optional[str] = None,
|
|
2508
|
+
) -> Optional[str]:
|
|
2509
|
+
"""Async version of add_relationship."""
|
|
2510
|
+
effective_namespace = namespace or self.config.namespace
|
|
2511
|
+
|
|
2512
|
+
entity = await self.aget(
|
|
2513
|
+
entity_id=entity_id,
|
|
2514
|
+
entity_type=entity_type,
|
|
2515
|
+
user_id=user_id,
|
|
2516
|
+
namespace=effective_namespace,
|
|
2517
|
+
)
|
|
2518
|
+
|
|
2519
|
+
if not entity:
|
|
2520
|
+
return None
|
|
2521
|
+
|
|
2522
|
+
rel_id = entity.add_relationship(related_entity_id, relation, direction=direction)
|
|
2523
|
+
entity.updated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
2524
|
+
|
|
2525
|
+
success = await self._asave_entity(
|
|
2526
|
+
entity=entity,
|
|
2527
|
+
user_id=user_id,
|
|
2528
|
+
agent_id=agent_id,
|
|
2529
|
+
team_id=team_id,
|
|
2530
|
+
namespace=effective_namespace,
|
|
2531
|
+
)
|
|
2532
|
+
|
|
2533
|
+
return rel_id if success else None
|
|
2534
|
+
|
|
2535
|
+
# =========================================================================
|
|
2536
|
+
# Internal Save Helpers
|
|
2537
|
+
# =========================================================================
|
|
2538
|
+
|
|
2539
|
+
def _save_entity(
|
|
2540
|
+
self,
|
|
2541
|
+
entity: EntityMemory,
|
|
2542
|
+
user_id: Optional[str] = None,
|
|
2543
|
+
agent_id: Optional[str] = None,
|
|
2544
|
+
team_id: Optional[str] = None,
|
|
2545
|
+
namespace: Optional[str] = None,
|
|
2546
|
+
) -> bool:
|
|
2547
|
+
"""Save entity to database."""
|
|
2548
|
+
if not self.db:
|
|
2549
|
+
return False
|
|
2550
|
+
|
|
2551
|
+
effective_namespace = namespace or self.config.namespace
|
|
2552
|
+
|
|
2553
|
+
try:
|
|
2554
|
+
content = entity.to_dict()
|
|
2555
|
+
if not content:
|
|
2556
|
+
return False
|
|
2557
|
+
|
|
2558
|
+
self.db.upsert_learning(
|
|
2559
|
+
id=self._build_entity_db_id(entity.entity_id, entity.entity_type, effective_namespace),
|
|
2560
|
+
learning_type=self.learning_type,
|
|
2561
|
+
entity_id=entity.entity_id,
|
|
2562
|
+
entity_type=entity.entity_type,
|
|
2563
|
+
namespace=effective_namespace,
|
|
2564
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
2565
|
+
agent_id=agent_id,
|
|
2566
|
+
team_id=team_id,
|
|
2567
|
+
content=content,
|
|
2568
|
+
)
|
|
2569
|
+
|
|
2570
|
+
return True
|
|
2571
|
+
|
|
2572
|
+
except Exception as e:
|
|
2573
|
+
log_debug(f"EntityMemoryStore._save_entity failed: {e}")
|
|
2574
|
+
return False
|
|
2575
|
+
|
|
2576
|
+
async def _asave_entity(
|
|
2577
|
+
self,
|
|
2578
|
+
entity: EntityMemory,
|
|
2579
|
+
user_id: Optional[str] = None,
|
|
2580
|
+
agent_id: Optional[str] = None,
|
|
2581
|
+
team_id: Optional[str] = None,
|
|
2582
|
+
namespace: Optional[str] = None,
|
|
2583
|
+
) -> bool:
|
|
2584
|
+
"""Async version of _save_entity."""
|
|
2585
|
+
if not self.db:
|
|
2586
|
+
return False
|
|
2587
|
+
|
|
2588
|
+
effective_namespace = namespace or self.config.namespace
|
|
2589
|
+
|
|
2590
|
+
try:
|
|
2591
|
+
content = entity.to_dict()
|
|
2592
|
+
if not content:
|
|
2593
|
+
return False
|
|
2594
|
+
|
|
2595
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
2596
|
+
await self.db.upsert_learning(
|
|
2597
|
+
id=self._build_entity_db_id(entity.entity_id, entity.entity_type, effective_namespace),
|
|
2598
|
+
learning_type=self.learning_type,
|
|
2599
|
+
entity_id=entity.entity_id,
|
|
2600
|
+
entity_type=entity.entity_type,
|
|
2601
|
+
namespace=effective_namespace,
|
|
2602
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
2603
|
+
agent_id=agent_id,
|
|
2604
|
+
team_id=team_id,
|
|
2605
|
+
content=content,
|
|
2606
|
+
)
|
|
2607
|
+
else:
|
|
2608
|
+
self.db.upsert_learning(
|
|
2609
|
+
id=self._build_entity_db_id(entity.entity_id, entity.entity_type, effective_namespace),
|
|
2610
|
+
learning_type=self.learning_type,
|
|
2611
|
+
entity_id=entity.entity_id,
|
|
2612
|
+
entity_type=entity.entity_type,
|
|
2613
|
+
namespace=effective_namespace,
|
|
2614
|
+
user_id=user_id if effective_namespace == "user" else None,
|
|
2615
|
+
agent_id=agent_id,
|
|
2616
|
+
team_id=team_id,
|
|
2617
|
+
content=content,
|
|
2618
|
+
)
|
|
2619
|
+
|
|
2620
|
+
return True
|
|
2621
|
+
|
|
2622
|
+
except Exception as e:
|
|
2623
|
+
log_debug(f"EntityMemoryStore._asave_entity failed: {e}")
|
|
2624
|
+
return False
|
|
2625
|
+
|
|
2626
|
+
# =========================================================================
|
|
2627
|
+
# Background Extraction
|
|
2628
|
+
# =========================================================================
|
|
2629
|
+
|
|
2630
|
+
def extract_and_save(
|
|
2631
|
+
self,
|
|
2632
|
+
messages: List[Any],
|
|
2633
|
+
user_id: Optional[str] = None,
|
|
2634
|
+
agent_id: Optional[str] = None,
|
|
2635
|
+
team_id: Optional[str] = None,
|
|
2636
|
+
namespace: Optional[str] = None,
|
|
2637
|
+
) -> None:
|
|
2638
|
+
"""Extract entities from messages (sync)."""
|
|
2639
|
+
if not self.model or not self.db:
|
|
2640
|
+
return
|
|
2641
|
+
|
|
2642
|
+
try:
|
|
2643
|
+
from agno.models.message import Message
|
|
2644
|
+
|
|
2645
|
+
conversation_text = self._messages_to_text(messages=messages)
|
|
2646
|
+
|
|
2647
|
+
tools = self._get_extraction_tools(
|
|
2648
|
+
user_id=user_id,
|
|
2649
|
+
agent_id=agent_id,
|
|
2650
|
+
team_id=team_id,
|
|
2651
|
+
namespace=namespace,
|
|
2652
|
+
)
|
|
2653
|
+
|
|
2654
|
+
functions = self._build_functions_for_model(tools=tools)
|
|
2655
|
+
|
|
2656
|
+
messages_for_model = [
|
|
2657
|
+
self._get_extraction_system_message(),
|
|
2658
|
+
Message(role="user", content=f"Extract entities from this conversation:\n\n{conversation_text}"),
|
|
2659
|
+
]
|
|
2660
|
+
|
|
2661
|
+
model_copy = deepcopy(self.model)
|
|
2662
|
+
response = model_copy.response(
|
|
2663
|
+
messages=messages_for_model,
|
|
2664
|
+
tools=functions,
|
|
2665
|
+
)
|
|
2666
|
+
|
|
2667
|
+
if response.tool_executions:
|
|
2668
|
+
self.entity_updated = True
|
|
2669
|
+
log_debug("EntityMemoryStore: Extraction saved entities")
|
|
2670
|
+
|
|
2671
|
+
except Exception as e:
|
|
2672
|
+
log_warning(f"EntityMemoryStore.extract_and_save failed: {e}")
|
|
2673
|
+
|
|
2674
|
+
async def aextract_and_save(
|
|
2675
|
+
self,
|
|
2676
|
+
messages: List[Any],
|
|
2677
|
+
user_id: Optional[str] = None,
|
|
2678
|
+
agent_id: Optional[str] = None,
|
|
2679
|
+
team_id: Optional[str] = None,
|
|
2680
|
+
namespace: Optional[str] = None,
|
|
2681
|
+
) -> None:
|
|
2682
|
+
"""Extract entities from messages (async)."""
|
|
2683
|
+
if not self.model or not self.db:
|
|
2684
|
+
return
|
|
2685
|
+
|
|
2686
|
+
try:
|
|
2687
|
+
conversation_text = self._messages_to_text(messages=messages)
|
|
2688
|
+
|
|
2689
|
+
tools = self._aget_extraction_tools(
|
|
2690
|
+
user_id=user_id,
|
|
2691
|
+
agent_id=agent_id,
|
|
2692
|
+
team_id=team_id,
|
|
2693
|
+
namespace=namespace,
|
|
2694
|
+
)
|
|
2695
|
+
|
|
2696
|
+
functions = self._build_functions_for_model(tools=tools)
|
|
2697
|
+
|
|
2698
|
+
messages_for_model = [
|
|
2699
|
+
self._get_extraction_system_message(),
|
|
2700
|
+
Message(role="user", content=f"Extract entities from this conversation:\n\n{conversation_text}"),
|
|
2701
|
+
]
|
|
2702
|
+
|
|
2703
|
+
model_copy = deepcopy(self.model)
|
|
2704
|
+
response = await model_copy.aresponse(
|
|
2705
|
+
messages=messages_for_model,
|
|
2706
|
+
tools=functions,
|
|
2707
|
+
)
|
|
2708
|
+
|
|
2709
|
+
if response.tool_executions:
|
|
2710
|
+
self.entity_updated = True
|
|
2711
|
+
log_debug("EntityMemoryStore: Extraction saved entities")
|
|
2712
|
+
|
|
2713
|
+
except Exception as e:
|
|
2714
|
+
log_warning(f"EntityMemoryStore.aextract_and_save failed: {e}")
|
|
2715
|
+
|
|
2716
|
+
def _get_extraction_system_message(self) -> "Message":
|
|
2717
|
+
"""Get system message for extraction."""
|
|
2718
|
+
from agno.models.message import Message
|
|
2719
|
+
|
|
2720
|
+
custom_instructions = self.config.instructions or ""
|
|
2721
|
+
additional = self.config.additional_instructions or ""
|
|
2722
|
+
|
|
2723
|
+
if self.config.system_message:
|
|
2724
|
+
return Message(role="system", content=self.config.system_message)
|
|
2725
|
+
|
|
2726
|
+
content = dedent("""\
|
|
2727
|
+
You are an Entity Extractor. Your job is to identify and capture knowledge about
|
|
2728
|
+
external entities - people, companies, projects, products, systems, and other things
|
|
2729
|
+
mentioned in conversations that are worth remembering.
|
|
2730
|
+
|
|
2731
|
+
## Philosophy
|
|
2732
|
+
|
|
2733
|
+
Entity memory is your knowledge about the WORLD, distinct from:
|
|
2734
|
+
- **User memory**: What you know about the user themselves
|
|
2735
|
+
- **Learned knowledge**: Reusable task insights and patterns
|
|
2736
|
+
- **Session context**: State of the current conversation
|
|
2737
|
+
|
|
2738
|
+
Think of entity memory like a professional's mental rolodex - the accumulated knowledge
|
|
2739
|
+
about clients, companies, technologies, and projects that helps you work effectively.
|
|
2740
|
+
|
|
2741
|
+
## Entity Structure
|
|
2742
|
+
|
|
2743
|
+
Each entity has:
|
|
2744
|
+
|
|
2745
|
+
**Core identity:**
|
|
2746
|
+
- `entity_id`: Lowercase with underscores (e.g., "acme_corp", "jane_smith", "project_atlas")
|
|
2747
|
+
- `entity_type`: Category - "person", "company", "project", "product", "system", "concept"
|
|
2748
|
+
- `name`: Human-readable display name
|
|
2749
|
+
- `description`: Brief description of what this entity is
|
|
2750
|
+
|
|
2751
|
+
**Three types of memory:**
|
|
2752
|
+
|
|
2753
|
+
1. **Facts** (semantic memory) - Timeless truths about the entity
|
|
2754
|
+
- "Uses PostgreSQL for their main database"
|
|
2755
|
+
- "Headquarters in San Francisco"
|
|
2756
|
+
- "Founded in 2019"
|
|
2757
|
+
- "Prefers async communication"
|
|
2758
|
+
|
|
2759
|
+
2. **Events** (episodic memory) - Time-bound occurrences
|
|
2760
|
+
- "Launched v2.0 on January 15, 2025"
|
|
2761
|
+
- "Acquired by BigCorp in Q3 2024"
|
|
2762
|
+
- "Had a major outage affecting 10K users"
|
|
2763
|
+
- "Completed Series B funding"
|
|
2764
|
+
|
|
2765
|
+
3. **Relationships** (graph edges) - Connections to other entities
|
|
2766
|
+
- "Bob Smith" --[CEO]--> "Acme Corp"
|
|
2767
|
+
- "Project Atlas" --[uses]--> "PostgreSQL"
|
|
2768
|
+
- "Acme Corp" --[competitor_of]--> "Beta Inc"
|
|
2769
|
+
- "Jane" --[reports_to]--> "Bob"
|
|
2770
|
+
|
|
2771
|
+
## What to Extract
|
|
2772
|
+
|
|
2773
|
+
**DO extract entities that are:**
|
|
2774
|
+
- Named specifically (not just "a company" but "Acme Corp")
|
|
2775
|
+
- Substantively discussed (not just mentioned in passing)
|
|
2776
|
+
- Likely to be referenced again in future conversations
|
|
2777
|
+
- Important to the user's work or context
|
|
2778
|
+
|
|
2779
|
+
**DO capture:**
|
|
2780
|
+
- Companies the user works with or mentions repeatedly
|
|
2781
|
+
- People (colleagues, clients, stakeholders) with specific roles
|
|
2782
|
+
- Projects with concrete details
|
|
2783
|
+
- Products or systems with technical specifics
|
|
2784
|
+
- Organizations relevant to the user's domain
|
|
2785
|
+
|
|
2786
|
+
## What NOT to Extract
|
|
2787
|
+
|
|
2788
|
+
**DO NOT extract:**
|
|
2789
|
+
- The user themselves (that belongs in UserProfile)
|
|
2790
|
+
- Generic concepts without specific identity ("databases" vs "PostgreSQL")
|
|
2791
|
+
- One-off mentions unlikely to recur ("I saw a company on the news")
|
|
2792
|
+
- Entities with no substantive information to store
|
|
2793
|
+
- Publicly available information that's easily searchable
|
|
2794
|
+
|
|
2795
|
+
**Avoid:**
|
|
2796
|
+
- Creating entities just because something was named
|
|
2797
|
+
- Storing obvious facts ("Google is a tech company")
|
|
2798
|
+
- Duplicating information across multiple entities unnecessarily
|
|
2799
|
+
|
|
2800
|
+
## Quality Guidelines
|
|
2801
|
+
|
|
2802
|
+
**Good entity example:**
|
|
2803
|
+
```
|
|
2804
|
+
entity_id: "northstar_analytics"
|
|
2805
|
+
entity_type: "company"
|
|
2806
|
+
name: "NorthStar Analytics"
|
|
2807
|
+
description: "Data analytics startup, potential client"
|
|
2808
|
+
facts:
|
|
2809
|
+
- "Series A stage, ~50 employees"
|
|
2810
|
+
- "Tech stack: Python, Snowflake, dbt"
|
|
2811
|
+
- "Main contact is Sarah Chen, VP Engineering"
|
|
2812
|
+
- "Decision timeline is Q1 2025"
|
|
2813
|
+
events:
|
|
2814
|
+
- "Initial meeting held December 2024"
|
|
2815
|
+
- "Requested technical deep-dive on ML capabilities"
|
|
2816
|
+
relationships:
|
|
2817
|
+
- sarah_chen --[works_at]--> northstar_analytics
|
|
2818
|
+
```
|
|
2819
|
+
|
|
2820
|
+
**Poor entity example:**
|
|
2821
|
+
```
|
|
2822
|
+
entity_id: "company1" # Too generic
|
|
2823
|
+
name: "Some Company" # Vague
|
|
2824
|
+
facts:
|
|
2825
|
+
- "It's a company" # Obvious, not useful
|
|
2826
|
+
```
|
|
2827
|
+
|
|
2828
|
+
## Extraction Guidelines
|
|
2829
|
+
|
|
2830
|
+
1. **Be selective**: Only extract entities with substantive, useful information
|
|
2831
|
+
2. **Be specific**: Capture concrete details, not vague generalities
|
|
2832
|
+
3. **Be accurate**: Only store information actually stated in the conversation
|
|
2833
|
+
4. **Categorize correctly**: Facts vs events vs relationships have different purposes
|
|
2834
|
+
5. **Use consistent IDs**: Lowercase, underscores, descriptive (e.g., "acme_corp" not "company_1")
|
|
2835
|
+
|
|
2836
|
+
It's perfectly fine to extract nothing if no notable entities are mentioned.
|
|
2837
|
+
Quality over quantity - one well-documented entity beats five sparse ones.
|
|
2838
|
+
|
|
2839
|
+
""")
|
|
2840
|
+
|
|
2841
|
+
if custom_instructions:
|
|
2842
|
+
content += f"\n## Additional Instructions\n\n{custom_instructions}\n"
|
|
2843
|
+
|
|
2844
|
+
if additional:
|
|
2845
|
+
content += f"\n{additional}\n"
|
|
2846
|
+
|
|
2847
|
+
return Message(role="system", content=content)
|
|
2848
|
+
|
|
2849
|
+
def _get_extraction_tools(
|
|
2850
|
+
self,
|
|
2851
|
+
user_id: Optional[str] = None,
|
|
2852
|
+
agent_id: Optional[str] = None,
|
|
2853
|
+
team_id: Optional[str] = None,
|
|
2854
|
+
namespace: Optional[str] = None,
|
|
2855
|
+
) -> List[Callable]:
|
|
2856
|
+
"""Get sync extraction tools based on config."""
|
|
2857
|
+
tools: List[Callable[..., str]] = []
|
|
2858
|
+
effective_namespace = namespace or self.config.namespace
|
|
2859
|
+
|
|
2860
|
+
if self.config.enable_create_entity:
|
|
2861
|
+
|
|
2862
|
+
def create_entity(
|
|
2863
|
+
entity_id: str,
|
|
2864
|
+
entity_type: str,
|
|
2865
|
+
name: str,
|
|
2866
|
+
description: Optional[str] = None,
|
|
2867
|
+
) -> str:
|
|
2868
|
+
"""Create a new entity."""
|
|
2869
|
+
success = self.create_entity(
|
|
2870
|
+
entity_id=entity_id,
|
|
2871
|
+
entity_type=entity_type,
|
|
2872
|
+
name=name,
|
|
2873
|
+
description=description,
|
|
2874
|
+
user_id=user_id,
|
|
2875
|
+
agent_id=agent_id,
|
|
2876
|
+
team_id=team_id,
|
|
2877
|
+
namespace=effective_namespace,
|
|
2878
|
+
)
|
|
2879
|
+
return f"Created: {entity_type}/{entity_id}" if success else "Entity exists"
|
|
2880
|
+
|
|
2881
|
+
tools.append(create_entity)
|
|
2882
|
+
|
|
2883
|
+
if self.config.enable_add_fact:
|
|
2884
|
+
|
|
2885
|
+
def add_fact(entity_id: str, entity_type: str, fact: str) -> str:
|
|
2886
|
+
"""Add a fact to an entity."""
|
|
2887
|
+
fact_id = self.add_fact(
|
|
2888
|
+
entity_id=entity_id,
|
|
2889
|
+
entity_type=entity_type,
|
|
2890
|
+
fact=fact,
|
|
2891
|
+
user_id=user_id,
|
|
2892
|
+
agent_id=agent_id,
|
|
2893
|
+
team_id=team_id,
|
|
2894
|
+
namespace=effective_namespace,
|
|
2895
|
+
)
|
|
2896
|
+
return f"Fact added: {fact_id}" if fact_id else "Entity not found"
|
|
2897
|
+
|
|
2898
|
+
tools.append(add_fact)
|
|
2899
|
+
|
|
2900
|
+
if self.config.enable_add_event:
|
|
2901
|
+
|
|
2902
|
+
def add_event(
|
|
2903
|
+
entity_id: str,
|
|
2904
|
+
entity_type: str,
|
|
2905
|
+
event: str,
|
|
2906
|
+
date: Optional[str] = None,
|
|
2907
|
+
) -> str:
|
|
2908
|
+
"""Add an event to an entity."""
|
|
2909
|
+
event_id = self.add_event(
|
|
2910
|
+
entity_id=entity_id,
|
|
2911
|
+
entity_type=entity_type,
|
|
2912
|
+
event=event,
|
|
2913
|
+
date=date,
|
|
2914
|
+
user_id=user_id,
|
|
2915
|
+
agent_id=agent_id,
|
|
2916
|
+
team_id=team_id,
|
|
2917
|
+
namespace=effective_namespace,
|
|
2918
|
+
)
|
|
2919
|
+
return f"Event added: {event_id}" if event_id else "Entity not found"
|
|
2920
|
+
|
|
2921
|
+
tools.append(add_event)
|
|
2922
|
+
|
|
2923
|
+
if self.config.enable_add_relationship:
|
|
2924
|
+
|
|
2925
|
+
def add_relationship(
|
|
2926
|
+
entity_id: str,
|
|
2927
|
+
entity_type: str,
|
|
2928
|
+
related_entity_id: str,
|
|
2929
|
+
relation: str,
|
|
2930
|
+
) -> str:
|
|
2931
|
+
"""Add a relationship between entities."""
|
|
2932
|
+
rel_id = self.add_relationship(
|
|
2933
|
+
entity_id=entity_id,
|
|
2934
|
+
entity_type=entity_type,
|
|
2935
|
+
related_entity_id=related_entity_id,
|
|
2936
|
+
relation=relation,
|
|
2937
|
+
user_id=user_id,
|
|
2938
|
+
agent_id=agent_id,
|
|
2939
|
+
team_id=team_id,
|
|
2940
|
+
namespace=effective_namespace,
|
|
2941
|
+
)
|
|
2942
|
+
return f"Relationship added: {rel_id}" if rel_id else "Entity not found"
|
|
2943
|
+
|
|
2944
|
+
tools.append(add_relationship)
|
|
2945
|
+
|
|
2946
|
+
return tools
|
|
2947
|
+
|
|
2948
|
+
def _aget_extraction_tools(
|
|
2949
|
+
self,
|
|
2950
|
+
user_id: Optional[str] = None,
|
|
2951
|
+
agent_id: Optional[str] = None,
|
|
2952
|
+
team_id: Optional[str] = None,
|
|
2953
|
+
namespace: Optional[str] = None,
|
|
2954
|
+
) -> List[Callable]:
|
|
2955
|
+
"""Get async extraction tools based on config."""
|
|
2956
|
+
tools: List[Callable] = []
|
|
2957
|
+
effective_namespace = namespace or self.config.namespace
|
|
2958
|
+
|
|
2959
|
+
if self.config.enable_create_entity:
|
|
2960
|
+
|
|
2961
|
+
async def create_entity(
|
|
2962
|
+
entity_id: str,
|
|
2963
|
+
entity_type: str,
|
|
2964
|
+
name: str,
|
|
2965
|
+
description: Optional[str] = None,
|
|
2966
|
+
) -> str:
|
|
2967
|
+
"""Create a new entity."""
|
|
2968
|
+
success = await self.acreate_entity(
|
|
2969
|
+
entity_id=entity_id,
|
|
2970
|
+
entity_type=entity_type,
|
|
2971
|
+
name=name,
|
|
2972
|
+
description=description,
|
|
2973
|
+
user_id=user_id,
|
|
2974
|
+
agent_id=agent_id,
|
|
2975
|
+
team_id=team_id,
|
|
2976
|
+
namespace=effective_namespace,
|
|
2977
|
+
)
|
|
2978
|
+
return f"Created: {entity_type}/{entity_id}" if success else "Entity exists"
|
|
2979
|
+
|
|
2980
|
+
tools.append(create_entity)
|
|
2981
|
+
|
|
2982
|
+
if self.config.enable_add_fact:
|
|
2983
|
+
|
|
2984
|
+
async def add_fact(entity_id: str, entity_type: str, fact: str) -> str:
|
|
2985
|
+
"""Add a fact to an entity."""
|
|
2986
|
+
fact_id = await self.aadd_fact(
|
|
2987
|
+
entity_id=entity_id,
|
|
2988
|
+
entity_type=entity_type,
|
|
2989
|
+
fact=fact,
|
|
2990
|
+
user_id=user_id,
|
|
2991
|
+
agent_id=agent_id,
|
|
2992
|
+
team_id=team_id,
|
|
2993
|
+
namespace=effective_namespace,
|
|
2994
|
+
)
|
|
2995
|
+
return f"Fact added: {fact_id}" if fact_id else "Entity not found"
|
|
2996
|
+
|
|
2997
|
+
tools.append(add_fact)
|
|
2998
|
+
|
|
2999
|
+
if self.config.enable_add_event:
|
|
3000
|
+
|
|
3001
|
+
async def add_event(
|
|
3002
|
+
entity_id: str,
|
|
3003
|
+
entity_type: str,
|
|
3004
|
+
event: str,
|
|
3005
|
+
date: Optional[str] = None,
|
|
3006
|
+
) -> str:
|
|
3007
|
+
"""Add an event to an entity."""
|
|
3008
|
+
event_id = await self.aadd_event(
|
|
3009
|
+
entity_id=entity_id,
|
|
3010
|
+
entity_type=entity_type,
|
|
3011
|
+
event=event,
|
|
3012
|
+
date=date,
|
|
3013
|
+
user_id=user_id,
|
|
3014
|
+
agent_id=agent_id,
|
|
3015
|
+
team_id=team_id,
|
|
3016
|
+
namespace=effective_namespace,
|
|
3017
|
+
)
|
|
3018
|
+
return f"Event added: {event_id}" if event_id else "Entity not found"
|
|
3019
|
+
|
|
3020
|
+
tools.append(add_event)
|
|
3021
|
+
|
|
3022
|
+
if self.config.enable_add_relationship:
|
|
3023
|
+
|
|
3024
|
+
async def add_relationship(
|
|
3025
|
+
entity_id: str,
|
|
3026
|
+
entity_type: str,
|
|
3027
|
+
related_entity_id: str,
|
|
3028
|
+
relation: str,
|
|
3029
|
+
) -> str:
|
|
3030
|
+
"""Add a relationship between entities."""
|
|
3031
|
+
rel_id = await self.aadd_relationship(
|
|
3032
|
+
entity_id=entity_id,
|
|
3033
|
+
entity_type=entity_type,
|
|
3034
|
+
related_entity_id=related_entity_id,
|
|
3035
|
+
relation=relation,
|
|
3036
|
+
user_id=user_id,
|
|
3037
|
+
agent_id=agent_id,
|
|
3038
|
+
team_id=team_id,
|
|
3039
|
+
namespace=effective_namespace,
|
|
3040
|
+
)
|
|
3041
|
+
return f"Relationship added: {rel_id}" if rel_id else "Entity not found"
|
|
3042
|
+
|
|
3043
|
+
tools.append(add_relationship)
|
|
3044
|
+
|
|
3045
|
+
return tools
|
|
3046
|
+
|
|
3047
|
+
def _build_functions_for_model(self, tools: List[Callable]) -> List[Any]:
|
|
3048
|
+
"""Convert callables to Functions for model."""
|
|
3049
|
+
from agno.tools.function import Function
|
|
3050
|
+
|
|
3051
|
+
functions = []
|
|
3052
|
+
seen_names = set()
|
|
3053
|
+
|
|
3054
|
+
for tool in tools:
|
|
3055
|
+
try:
|
|
3056
|
+
name = tool.__name__
|
|
3057
|
+
if name in seen_names:
|
|
3058
|
+
continue
|
|
3059
|
+
seen_names.add(name)
|
|
3060
|
+
|
|
3061
|
+
func = Function.from_callable(tool, strict=True)
|
|
3062
|
+
func.strict = True
|
|
3063
|
+
functions.append(func)
|
|
3064
|
+
except Exception as e:
|
|
3065
|
+
log_warning(f"Could not add function {tool}: {e}")
|
|
3066
|
+
|
|
3067
|
+
return functions
|
|
3068
|
+
|
|
3069
|
+
def _messages_to_text(self, messages: List[Any]) -> str:
|
|
3070
|
+
"""Convert messages to text for extraction."""
|
|
3071
|
+
parts = []
|
|
3072
|
+
for msg in messages:
|
|
3073
|
+
if msg.role == "user":
|
|
3074
|
+
content = msg.get_content_string() if hasattr(msg, "get_content_string") else str(msg.content)
|
|
3075
|
+
if content and content.strip():
|
|
3076
|
+
parts.append(f"User: {content}")
|
|
3077
|
+
elif msg.role in ["assistant", "model"]:
|
|
3078
|
+
content = msg.get_content_string() if hasattr(msg, "get_content_string") else str(msg.content)
|
|
3079
|
+
if content and content.strip():
|
|
3080
|
+
parts.append(f"Assistant: {content}")
|
|
3081
|
+
return "\n".join(parts)
|
|
3082
|
+
|
|
3083
|
+
# =========================================================================
|
|
3084
|
+
# Private Helpers
|
|
3085
|
+
# =========================================================================
|
|
3086
|
+
|
|
3087
|
+
def _build_entity_db_id(
|
|
3088
|
+
self,
|
|
3089
|
+
entity_id: str,
|
|
3090
|
+
entity_type: str,
|
|
3091
|
+
namespace: str,
|
|
3092
|
+
) -> str:
|
|
3093
|
+
"""Build unique DB ID for entity."""
|
|
3094
|
+
return f"entity_{namespace}_{entity_type}_{entity_id}"
|
|
3095
|
+
|
|
3096
|
+
def _format_entity_basic(self, entity: Any) -> str:
|
|
3097
|
+
"""Basic entity formatting fallback."""
|
|
3098
|
+
parts = []
|
|
3099
|
+
|
|
3100
|
+
name = getattr(entity, "name", None)
|
|
3101
|
+
entity_type = getattr(entity, "entity_type", "unknown")
|
|
3102
|
+
entity_id = getattr(entity, "entity_id", "unknown")
|
|
3103
|
+
|
|
3104
|
+
if name:
|
|
3105
|
+
parts.append(f"**{name}** ({entity_type})")
|
|
3106
|
+
else:
|
|
3107
|
+
parts.append(f"**{entity_id}** ({entity_type})")
|
|
3108
|
+
|
|
3109
|
+
description = getattr(entity, "description", None)
|
|
3110
|
+
if description:
|
|
3111
|
+
parts.append(description)
|
|
3112
|
+
|
|
3113
|
+
facts = getattr(entity, "facts", [])
|
|
3114
|
+
if facts:
|
|
3115
|
+
facts_text = "\n".join(f" - {f.get('content', f)}" for f in facts[:5])
|
|
3116
|
+
parts.append(f"Facts:\n{facts_text}")
|
|
3117
|
+
|
|
3118
|
+
return "\n".join(parts)
|
|
3119
|
+
|
|
3120
|
+
def _format_entities_list(self, entities: List[EntityMemory]) -> str:
|
|
3121
|
+
"""Format entities for tool output."""
|
|
3122
|
+
parts = []
|
|
3123
|
+
for i, entity in enumerate(entities, 1):
|
|
3124
|
+
if hasattr(entity, "get_context_text"):
|
|
3125
|
+
formatted = entity.get_context_text()
|
|
3126
|
+
else:
|
|
3127
|
+
formatted = self._format_entity_basic(entity=entity)
|
|
3128
|
+
parts.append(f"{i}. {formatted}")
|
|
3129
|
+
return "\n\n".join(parts)
|
|
3130
|
+
|
|
3131
|
+
# =========================================================================
|
|
3132
|
+
# Representation
|
|
3133
|
+
# =========================================================================
|
|
3134
|
+
|
|
3135
|
+
def __repr__(self) -> str:
|
|
3136
|
+
"""String representation for debugging."""
|
|
3137
|
+
has_db = self.db is not None
|
|
3138
|
+
has_model = self.model is not None
|
|
3139
|
+
return (
|
|
3140
|
+
f"EntityMemoryStore("
|
|
3141
|
+
f"mode={self.config.mode.value}, "
|
|
3142
|
+
f"namespace={self.config.namespace}, "
|
|
3143
|
+
f"db={has_db}, "
|
|
3144
|
+
f"model={has_model}, "
|
|
3145
|
+
f"enable_agent_tools={self.config.enable_agent_tools})"
|
|
3146
|
+
)
|
|
3147
|
+
|
|
3148
|
+
def print(
|
|
3149
|
+
self,
|
|
3150
|
+
entity_id: str,
|
|
3151
|
+
entity_type: str,
|
|
3152
|
+
*,
|
|
3153
|
+
user_id: Optional[str] = None,
|
|
3154
|
+
namespace: Optional[str] = None,
|
|
3155
|
+
raw: bool = False,
|
|
3156
|
+
) -> None:
|
|
3157
|
+
"""Print formatted entity memory.
|
|
3158
|
+
|
|
3159
|
+
Args:
|
|
3160
|
+
entity_id: The entity to print.
|
|
3161
|
+
entity_type: Type of entity.
|
|
3162
|
+
user_id: User ID for "user" namespace scoping.
|
|
3163
|
+
namespace: Namespace to search in.
|
|
3164
|
+
raw: If True, print raw dict using pprint instead of formatted panel.
|
|
3165
|
+
|
|
3166
|
+
Example:
|
|
3167
|
+
>>> store.print(entity_id="acme_corp", entity_type="company")
|
|
3168
|
+
╭────────────────── Entity Memory ──────────────────╮
|
|
3169
|
+
│ Acme Corporation (company) │
|
|
3170
|
+
│ Enterprise software company │
|
|
3171
|
+
│ │
|
|
3172
|
+
│ Properties: │
|
|
3173
|
+
│ industry: fintech │
|
|
3174
|
+
│ size: startup │
|
|
3175
|
+
│ │
|
|
3176
|
+
│ Facts: │
|
|
3177
|
+
│ [dim][f1][/dim] Uses PostgreSQL for main DB │
|
|
3178
|
+
│ [dim][f2][/dim] API uses OAuth2 authentication │
|
|
3179
|
+
│ │
|
|
3180
|
+
│ Events: │
|
|
3181
|
+
│ [dim][e1][/dim] Launched v2.0 (2024-01-15) │
|
|
3182
|
+
│ │
|
|
3183
|
+
│ Relationships: │
|
|
3184
|
+
│ CEO → bob_smith │
|
|
3185
|
+
╰────────────────── acme_corp ──────────────────────╯
|
|
3186
|
+
"""
|
|
3187
|
+
from agno.learn.utils import print_panel
|
|
3188
|
+
|
|
3189
|
+
effective_namespace = namespace or self.config.namespace
|
|
3190
|
+
|
|
3191
|
+
entity = self.get(
|
|
3192
|
+
entity_id=entity_id,
|
|
3193
|
+
entity_type=entity_type,
|
|
3194
|
+
user_id=user_id,
|
|
3195
|
+
namespace=effective_namespace,
|
|
3196
|
+
)
|
|
3197
|
+
|
|
3198
|
+
lines = []
|
|
3199
|
+
|
|
3200
|
+
if entity:
|
|
3201
|
+
# Header: name and type
|
|
3202
|
+
name = getattr(entity, "name", None)
|
|
3203
|
+
etype = getattr(entity, "entity_type", entity_type)
|
|
3204
|
+
if name:
|
|
3205
|
+
lines.append(f"[bold]{name}[/bold] ({etype})")
|
|
3206
|
+
else:
|
|
3207
|
+
lines.append(f"[bold]{entity_id}[/bold] ({etype})")
|
|
3208
|
+
|
|
3209
|
+
# Description
|
|
3210
|
+
description = getattr(entity, "description", None)
|
|
3211
|
+
if description:
|
|
3212
|
+
lines.append(description)
|
|
3213
|
+
|
|
3214
|
+
# Properties
|
|
3215
|
+
properties = getattr(entity, "properties", {})
|
|
3216
|
+
if properties:
|
|
3217
|
+
lines.append("")
|
|
3218
|
+
lines.append("Properties:")
|
|
3219
|
+
for key, value in properties.items():
|
|
3220
|
+
lines.append(f" {key}: {value}")
|
|
3221
|
+
|
|
3222
|
+
# Facts
|
|
3223
|
+
facts = getattr(entity, "facts", [])
|
|
3224
|
+
if facts:
|
|
3225
|
+
lines.append("")
|
|
3226
|
+
lines.append("Facts:")
|
|
3227
|
+
for fact in facts:
|
|
3228
|
+
if isinstance(fact, dict):
|
|
3229
|
+
fact_id = fact.get("id", "?")
|
|
3230
|
+
content = fact.get("content", str(fact))
|
|
3231
|
+
else:
|
|
3232
|
+
fact_id = "?"
|
|
3233
|
+
content = str(fact)
|
|
3234
|
+
lines.append(f" [dim]\\[{fact_id}][/dim] {content}")
|
|
3235
|
+
|
|
3236
|
+
# Events
|
|
3237
|
+
events = getattr(entity, "events", [])
|
|
3238
|
+
if events:
|
|
3239
|
+
lines.append("")
|
|
3240
|
+
lines.append("Events:")
|
|
3241
|
+
for event in events:
|
|
3242
|
+
if isinstance(event, dict):
|
|
3243
|
+
event_id = event.get("id", "?")
|
|
3244
|
+
content = event.get("content", str(event))
|
|
3245
|
+
date = event.get("date")
|
|
3246
|
+
date_str = f" ({date})" if date else ""
|
|
3247
|
+
else:
|
|
3248
|
+
event_id = "?"
|
|
3249
|
+
content = str(event)
|
|
3250
|
+
date_str = ""
|
|
3251
|
+
lines.append(f" [dim]\\[{event_id}][/dim] {content}{date_str}")
|
|
3252
|
+
|
|
3253
|
+
# Relationships
|
|
3254
|
+
relationships = getattr(entity, "relationships", [])
|
|
3255
|
+
if relationships:
|
|
3256
|
+
lines.append("")
|
|
3257
|
+
lines.append("Relationships:")
|
|
3258
|
+
for rel in relationships:
|
|
3259
|
+
if isinstance(rel, dict):
|
|
3260
|
+
related_id = rel.get("entity_id", "?")
|
|
3261
|
+
relation = rel.get("relation", "related_to")
|
|
3262
|
+
direction = rel.get("direction", "outgoing")
|
|
3263
|
+
if direction == "outgoing":
|
|
3264
|
+
lines.append(f" {relation} → {related_id}")
|
|
3265
|
+
else:
|
|
3266
|
+
lines.append(f" {relation} ← {related_id}")
|
|
3267
|
+
|
|
3268
|
+
print_panel(
|
|
3269
|
+
title="Entity Memory",
|
|
3270
|
+
subtitle=f"{entity_type}/{entity_id}",
|
|
3271
|
+
lines=lines,
|
|
3272
|
+
empty_message="No entity found",
|
|
3273
|
+
raw_data=entity,
|
|
3274
|
+
raw=raw,
|
|
3275
|
+
)
|