agno 2.2.13__py3-none-any.whl → 2.4.3__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.
Files changed (383) hide show
  1. agno/agent/__init__.py +6 -0
  2. agno/agent/agent.py +5252 -3145
  3. agno/agent/remote.py +525 -0
  4. agno/api/api.py +2 -0
  5. agno/client/__init__.py +3 -0
  6. agno/client/a2a/__init__.py +10 -0
  7. agno/client/a2a/client.py +554 -0
  8. agno/client/a2a/schemas.py +112 -0
  9. agno/client/a2a/utils.py +369 -0
  10. agno/client/os.py +2669 -0
  11. agno/compression/__init__.py +3 -0
  12. agno/compression/manager.py +247 -0
  13. agno/culture/manager.py +2 -2
  14. agno/db/base.py +927 -6
  15. agno/db/dynamo/dynamo.py +788 -2
  16. agno/db/dynamo/schemas.py +128 -0
  17. agno/db/dynamo/utils.py +26 -3
  18. agno/db/firestore/firestore.py +674 -50
  19. agno/db/firestore/schemas.py +41 -0
  20. agno/db/firestore/utils.py +25 -10
  21. agno/db/gcs_json/gcs_json_db.py +506 -3
  22. agno/db/gcs_json/utils.py +14 -2
  23. agno/db/in_memory/in_memory_db.py +203 -4
  24. agno/db/in_memory/utils.py +14 -2
  25. agno/db/json/json_db.py +498 -2
  26. agno/db/json/utils.py +14 -2
  27. agno/db/migrations/manager.py +199 -0
  28. agno/db/migrations/utils.py +19 -0
  29. agno/db/migrations/v1_to_v2.py +54 -16
  30. agno/db/migrations/versions/__init__.py +0 -0
  31. agno/db/migrations/versions/v2_3_0.py +977 -0
  32. agno/db/mongo/async_mongo.py +1013 -39
  33. agno/db/mongo/mongo.py +684 -4
  34. agno/db/mongo/schemas.py +48 -0
  35. agno/db/mongo/utils.py +17 -0
  36. agno/db/mysql/__init__.py +2 -1
  37. agno/db/mysql/async_mysql.py +2958 -0
  38. agno/db/mysql/mysql.py +722 -53
  39. agno/db/mysql/schemas.py +77 -11
  40. agno/db/mysql/utils.py +151 -8
  41. agno/db/postgres/async_postgres.py +1254 -137
  42. agno/db/postgres/postgres.py +2316 -93
  43. agno/db/postgres/schemas.py +153 -21
  44. agno/db/postgres/utils.py +22 -7
  45. agno/db/redis/redis.py +531 -3
  46. agno/db/redis/schemas.py +36 -0
  47. agno/db/redis/utils.py +31 -15
  48. agno/db/schemas/evals.py +1 -0
  49. agno/db/schemas/memory.py +20 -9
  50. agno/db/singlestore/schemas.py +70 -1
  51. agno/db/singlestore/singlestore.py +737 -74
  52. agno/db/singlestore/utils.py +13 -3
  53. agno/db/sqlite/async_sqlite.py +1069 -89
  54. agno/db/sqlite/schemas.py +133 -1
  55. agno/db/sqlite/sqlite.py +2203 -165
  56. agno/db/sqlite/utils.py +21 -11
  57. agno/db/surrealdb/models.py +25 -0
  58. agno/db/surrealdb/surrealdb.py +603 -1
  59. agno/db/utils.py +60 -0
  60. agno/eval/__init__.py +26 -3
  61. agno/eval/accuracy.py +25 -12
  62. agno/eval/agent_as_judge.py +871 -0
  63. agno/eval/base.py +29 -0
  64. agno/eval/performance.py +10 -4
  65. agno/eval/reliability.py +22 -13
  66. agno/eval/utils.py +2 -1
  67. agno/exceptions.py +42 -0
  68. agno/hooks/__init__.py +3 -0
  69. agno/hooks/decorator.py +164 -0
  70. agno/integrations/discord/client.py +13 -2
  71. agno/knowledge/__init__.py +4 -0
  72. agno/knowledge/chunking/code.py +90 -0
  73. agno/knowledge/chunking/document.py +65 -4
  74. agno/knowledge/chunking/fixed.py +4 -1
  75. agno/knowledge/chunking/markdown.py +102 -11
  76. agno/knowledge/chunking/recursive.py +2 -2
  77. agno/knowledge/chunking/semantic.py +130 -48
  78. agno/knowledge/chunking/strategy.py +18 -0
  79. agno/knowledge/embedder/azure_openai.py +0 -1
  80. agno/knowledge/embedder/google.py +1 -1
  81. agno/knowledge/embedder/mistral.py +1 -1
  82. agno/knowledge/embedder/nebius.py +1 -1
  83. agno/knowledge/embedder/openai.py +16 -12
  84. agno/knowledge/filesystem.py +412 -0
  85. agno/knowledge/knowledge.py +4261 -1199
  86. agno/knowledge/protocol.py +134 -0
  87. agno/knowledge/reader/arxiv_reader.py +3 -2
  88. agno/knowledge/reader/base.py +9 -7
  89. agno/knowledge/reader/csv_reader.py +91 -42
  90. agno/knowledge/reader/docx_reader.py +9 -10
  91. agno/knowledge/reader/excel_reader.py +225 -0
  92. agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
  93. agno/knowledge/reader/firecrawl_reader.py +3 -2
  94. agno/knowledge/reader/json_reader.py +16 -22
  95. agno/knowledge/reader/markdown_reader.py +15 -14
  96. agno/knowledge/reader/pdf_reader.py +33 -28
  97. agno/knowledge/reader/pptx_reader.py +9 -10
  98. agno/knowledge/reader/reader_factory.py +135 -1
  99. agno/knowledge/reader/s3_reader.py +8 -16
  100. agno/knowledge/reader/tavily_reader.py +3 -3
  101. agno/knowledge/reader/text_reader.py +15 -14
  102. agno/knowledge/reader/utils/__init__.py +17 -0
  103. agno/knowledge/reader/utils/spreadsheet.py +114 -0
  104. agno/knowledge/reader/web_search_reader.py +8 -65
  105. agno/knowledge/reader/website_reader.py +16 -13
  106. agno/knowledge/reader/wikipedia_reader.py +36 -3
  107. agno/knowledge/reader/youtube_reader.py +3 -2
  108. agno/knowledge/remote_content/__init__.py +33 -0
  109. agno/knowledge/remote_content/config.py +266 -0
  110. agno/knowledge/remote_content/remote_content.py +105 -17
  111. agno/knowledge/utils.py +76 -22
  112. agno/learn/__init__.py +71 -0
  113. agno/learn/config.py +463 -0
  114. agno/learn/curate.py +185 -0
  115. agno/learn/machine.py +725 -0
  116. agno/learn/schemas.py +1114 -0
  117. agno/learn/stores/__init__.py +38 -0
  118. agno/learn/stores/decision_log.py +1156 -0
  119. agno/learn/stores/entity_memory.py +3275 -0
  120. agno/learn/stores/learned_knowledge.py +1583 -0
  121. agno/learn/stores/protocol.py +117 -0
  122. agno/learn/stores/session_context.py +1217 -0
  123. agno/learn/stores/user_memory.py +1495 -0
  124. agno/learn/stores/user_profile.py +1220 -0
  125. agno/learn/utils.py +209 -0
  126. agno/media.py +22 -6
  127. agno/memory/__init__.py +14 -1
  128. agno/memory/manager.py +223 -8
  129. agno/memory/strategies/__init__.py +15 -0
  130. agno/memory/strategies/base.py +66 -0
  131. agno/memory/strategies/summarize.py +196 -0
  132. agno/memory/strategies/types.py +37 -0
  133. agno/models/aimlapi/aimlapi.py +17 -0
  134. agno/models/anthropic/claude.py +434 -59
  135. agno/models/aws/bedrock.py +121 -20
  136. agno/models/aws/claude.py +131 -274
  137. agno/models/azure/ai_foundry.py +10 -6
  138. agno/models/azure/openai_chat.py +33 -10
  139. agno/models/base.py +1162 -561
  140. agno/models/cerebras/cerebras.py +120 -24
  141. agno/models/cerebras/cerebras_openai.py +21 -2
  142. agno/models/cohere/chat.py +65 -6
  143. agno/models/cometapi/cometapi.py +18 -1
  144. agno/models/dashscope/dashscope.py +2 -3
  145. agno/models/deepinfra/deepinfra.py +18 -1
  146. agno/models/deepseek/deepseek.py +69 -3
  147. agno/models/fireworks/fireworks.py +18 -1
  148. agno/models/google/gemini.py +959 -89
  149. agno/models/google/utils.py +22 -0
  150. agno/models/groq/groq.py +48 -18
  151. agno/models/huggingface/huggingface.py +17 -6
  152. agno/models/ibm/watsonx.py +16 -6
  153. agno/models/internlm/internlm.py +18 -1
  154. agno/models/langdb/langdb.py +13 -1
  155. agno/models/litellm/chat.py +88 -9
  156. agno/models/litellm/litellm_openai.py +18 -1
  157. agno/models/message.py +24 -5
  158. agno/models/meta/llama.py +40 -13
  159. agno/models/meta/llama_openai.py +22 -21
  160. agno/models/metrics.py +12 -0
  161. agno/models/mistral/mistral.py +8 -4
  162. agno/models/n1n/__init__.py +3 -0
  163. agno/models/n1n/n1n.py +57 -0
  164. agno/models/nebius/nebius.py +6 -7
  165. agno/models/nvidia/nvidia.py +20 -3
  166. agno/models/ollama/__init__.py +2 -0
  167. agno/models/ollama/chat.py +17 -6
  168. agno/models/ollama/responses.py +100 -0
  169. agno/models/openai/__init__.py +2 -0
  170. agno/models/openai/chat.py +117 -26
  171. agno/models/openai/open_responses.py +46 -0
  172. agno/models/openai/responses.py +110 -32
  173. agno/models/openrouter/__init__.py +2 -0
  174. agno/models/openrouter/openrouter.py +67 -2
  175. agno/models/openrouter/responses.py +146 -0
  176. agno/models/perplexity/perplexity.py +19 -1
  177. agno/models/portkey/portkey.py +7 -6
  178. agno/models/requesty/requesty.py +19 -2
  179. agno/models/response.py +20 -2
  180. agno/models/sambanova/sambanova.py +20 -3
  181. agno/models/siliconflow/siliconflow.py +19 -2
  182. agno/models/together/together.py +20 -3
  183. agno/models/vercel/v0.py +20 -3
  184. agno/models/vertexai/claude.py +124 -4
  185. agno/models/vllm/vllm.py +19 -14
  186. agno/models/xai/xai.py +19 -2
  187. agno/os/app.py +467 -137
  188. agno/os/auth.py +253 -5
  189. agno/os/config.py +22 -0
  190. agno/os/interfaces/a2a/a2a.py +7 -6
  191. agno/os/interfaces/a2a/router.py +635 -26
  192. agno/os/interfaces/a2a/utils.py +32 -33
  193. agno/os/interfaces/agui/agui.py +5 -3
  194. agno/os/interfaces/agui/router.py +26 -16
  195. agno/os/interfaces/agui/utils.py +97 -57
  196. agno/os/interfaces/base.py +7 -7
  197. agno/os/interfaces/slack/router.py +16 -7
  198. agno/os/interfaces/slack/slack.py +7 -7
  199. agno/os/interfaces/whatsapp/router.py +35 -7
  200. agno/os/interfaces/whatsapp/security.py +3 -1
  201. agno/os/interfaces/whatsapp/whatsapp.py +11 -8
  202. agno/os/managers.py +326 -0
  203. agno/os/mcp.py +652 -79
  204. agno/os/middleware/__init__.py +4 -0
  205. agno/os/middleware/jwt.py +718 -115
  206. agno/os/middleware/trailing_slash.py +27 -0
  207. agno/os/router.py +105 -1558
  208. agno/os/routers/agents/__init__.py +3 -0
  209. agno/os/routers/agents/router.py +655 -0
  210. agno/os/routers/agents/schema.py +288 -0
  211. agno/os/routers/components/__init__.py +3 -0
  212. agno/os/routers/components/components.py +475 -0
  213. agno/os/routers/database.py +155 -0
  214. agno/os/routers/evals/evals.py +111 -18
  215. agno/os/routers/evals/schemas.py +38 -5
  216. agno/os/routers/evals/utils.py +80 -11
  217. agno/os/routers/health.py +3 -3
  218. agno/os/routers/knowledge/knowledge.py +284 -35
  219. agno/os/routers/knowledge/schemas.py +14 -2
  220. agno/os/routers/memory/memory.py +274 -11
  221. agno/os/routers/memory/schemas.py +44 -3
  222. agno/os/routers/metrics/metrics.py +30 -15
  223. agno/os/routers/metrics/schemas.py +10 -6
  224. agno/os/routers/registry/__init__.py +3 -0
  225. agno/os/routers/registry/registry.py +337 -0
  226. agno/os/routers/session/session.py +143 -14
  227. agno/os/routers/teams/__init__.py +3 -0
  228. agno/os/routers/teams/router.py +550 -0
  229. agno/os/routers/teams/schema.py +280 -0
  230. agno/os/routers/traces/__init__.py +3 -0
  231. agno/os/routers/traces/schemas.py +414 -0
  232. agno/os/routers/traces/traces.py +549 -0
  233. agno/os/routers/workflows/__init__.py +3 -0
  234. agno/os/routers/workflows/router.py +757 -0
  235. agno/os/routers/workflows/schema.py +139 -0
  236. agno/os/schema.py +157 -584
  237. agno/os/scopes.py +469 -0
  238. agno/os/settings.py +3 -0
  239. agno/os/utils.py +574 -185
  240. agno/reasoning/anthropic.py +85 -1
  241. agno/reasoning/azure_ai_foundry.py +93 -1
  242. agno/reasoning/deepseek.py +102 -2
  243. agno/reasoning/default.py +6 -7
  244. agno/reasoning/gemini.py +87 -3
  245. agno/reasoning/groq.py +109 -2
  246. agno/reasoning/helpers.py +6 -7
  247. agno/reasoning/manager.py +1238 -0
  248. agno/reasoning/ollama.py +93 -1
  249. agno/reasoning/openai.py +115 -1
  250. agno/reasoning/vertexai.py +85 -1
  251. agno/registry/__init__.py +3 -0
  252. agno/registry/registry.py +68 -0
  253. agno/remote/__init__.py +3 -0
  254. agno/remote/base.py +581 -0
  255. agno/run/__init__.py +2 -4
  256. agno/run/agent.py +134 -19
  257. agno/run/base.py +49 -1
  258. agno/run/cancel.py +65 -52
  259. agno/run/cancellation_management/__init__.py +9 -0
  260. agno/run/cancellation_management/base.py +78 -0
  261. agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
  262. agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
  263. agno/run/requirement.py +181 -0
  264. agno/run/team.py +111 -19
  265. agno/run/workflow.py +2 -1
  266. agno/session/agent.py +57 -92
  267. agno/session/summary.py +1 -1
  268. agno/session/team.py +62 -115
  269. agno/session/workflow.py +353 -57
  270. agno/skills/__init__.py +17 -0
  271. agno/skills/agent_skills.py +377 -0
  272. agno/skills/errors.py +32 -0
  273. agno/skills/loaders/__init__.py +4 -0
  274. agno/skills/loaders/base.py +27 -0
  275. agno/skills/loaders/local.py +216 -0
  276. agno/skills/skill.py +65 -0
  277. agno/skills/utils.py +107 -0
  278. agno/skills/validator.py +277 -0
  279. agno/table.py +10 -0
  280. agno/team/__init__.py +5 -1
  281. agno/team/remote.py +447 -0
  282. agno/team/team.py +3769 -2202
  283. agno/tools/brandfetch.py +27 -18
  284. agno/tools/browserbase.py +225 -16
  285. agno/tools/crawl4ai.py +3 -0
  286. agno/tools/duckduckgo.py +25 -71
  287. agno/tools/exa.py +0 -21
  288. agno/tools/file.py +14 -13
  289. agno/tools/file_generation.py +12 -6
  290. agno/tools/firecrawl.py +15 -7
  291. agno/tools/function.py +94 -113
  292. agno/tools/google_bigquery.py +11 -2
  293. agno/tools/google_drive.py +4 -3
  294. agno/tools/knowledge.py +9 -4
  295. agno/tools/mcp/mcp.py +301 -18
  296. agno/tools/mcp/multi_mcp.py +269 -14
  297. agno/tools/mem0.py +11 -10
  298. agno/tools/memory.py +47 -46
  299. agno/tools/mlx_transcribe.py +10 -7
  300. agno/tools/models/nebius.py +5 -5
  301. agno/tools/models_labs.py +20 -10
  302. agno/tools/nano_banana.py +151 -0
  303. agno/tools/parallel.py +0 -7
  304. agno/tools/postgres.py +76 -36
  305. agno/tools/python.py +14 -6
  306. agno/tools/reasoning.py +30 -23
  307. agno/tools/redshift.py +406 -0
  308. agno/tools/shopify.py +1519 -0
  309. agno/tools/spotify.py +919 -0
  310. agno/tools/tavily.py +4 -1
  311. agno/tools/toolkit.py +253 -18
  312. agno/tools/websearch.py +93 -0
  313. agno/tools/website.py +1 -1
  314. agno/tools/wikipedia.py +1 -1
  315. agno/tools/workflow.py +56 -48
  316. agno/tools/yfinance.py +12 -11
  317. agno/tracing/__init__.py +12 -0
  318. agno/tracing/exporter.py +161 -0
  319. agno/tracing/schemas.py +276 -0
  320. agno/tracing/setup.py +112 -0
  321. agno/utils/agent.py +251 -10
  322. agno/utils/cryptography.py +22 -0
  323. agno/utils/dttm.py +33 -0
  324. agno/utils/events.py +264 -7
  325. agno/utils/hooks.py +111 -3
  326. agno/utils/http.py +161 -2
  327. agno/utils/mcp.py +49 -8
  328. agno/utils/media.py +22 -1
  329. agno/utils/models/ai_foundry.py +9 -2
  330. agno/utils/models/claude.py +20 -5
  331. agno/utils/models/cohere.py +9 -2
  332. agno/utils/models/llama.py +9 -2
  333. agno/utils/models/mistral.py +4 -2
  334. agno/utils/os.py +0 -0
  335. agno/utils/print_response/agent.py +99 -16
  336. agno/utils/print_response/team.py +223 -24
  337. agno/utils/print_response/workflow.py +0 -2
  338. agno/utils/prompts.py +8 -6
  339. agno/utils/remote.py +23 -0
  340. agno/utils/response.py +1 -13
  341. agno/utils/string.py +91 -2
  342. agno/utils/team.py +62 -12
  343. agno/utils/tokens.py +657 -0
  344. agno/vectordb/base.py +15 -2
  345. agno/vectordb/cassandra/cassandra.py +1 -1
  346. agno/vectordb/chroma/__init__.py +2 -1
  347. agno/vectordb/chroma/chromadb.py +468 -23
  348. agno/vectordb/clickhouse/clickhousedb.py +1 -1
  349. agno/vectordb/couchbase/couchbase.py +6 -2
  350. agno/vectordb/lancedb/lance_db.py +7 -38
  351. agno/vectordb/lightrag/lightrag.py +7 -6
  352. agno/vectordb/milvus/milvus.py +118 -84
  353. agno/vectordb/mongodb/__init__.py +2 -1
  354. agno/vectordb/mongodb/mongodb.py +14 -31
  355. agno/vectordb/pgvector/pgvector.py +120 -66
  356. agno/vectordb/pineconedb/pineconedb.py +2 -19
  357. agno/vectordb/qdrant/__init__.py +2 -1
  358. agno/vectordb/qdrant/qdrant.py +33 -56
  359. agno/vectordb/redis/__init__.py +2 -1
  360. agno/vectordb/redis/redisdb.py +19 -31
  361. agno/vectordb/singlestore/singlestore.py +17 -9
  362. agno/vectordb/surrealdb/surrealdb.py +2 -38
  363. agno/vectordb/weaviate/__init__.py +2 -1
  364. agno/vectordb/weaviate/weaviate.py +7 -3
  365. agno/workflow/__init__.py +5 -1
  366. agno/workflow/agent.py +2 -2
  367. agno/workflow/condition.py +12 -10
  368. agno/workflow/loop.py +28 -9
  369. agno/workflow/parallel.py +21 -13
  370. agno/workflow/remote.py +362 -0
  371. agno/workflow/router.py +12 -9
  372. agno/workflow/step.py +261 -36
  373. agno/workflow/steps.py +12 -8
  374. agno/workflow/types.py +40 -77
  375. agno/workflow/workflow.py +939 -213
  376. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
  377. agno-2.4.3.dist-info/RECORD +677 -0
  378. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
  379. agno/tools/googlesearch.py +0 -98
  380. agno/tools/memori.py +0 -339
  381. agno-2.2.13.dist-info/RECORD +0 -575
  382. {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
  383. {agno-2.2.13.dist-info → agno-2.4.3.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
+ )