agno 2.3.24__py3-none-any.whl → 2.3.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. agno/agent/agent.py +357 -28
  2. agno/db/base.py +214 -0
  3. agno/db/dynamo/dynamo.py +47 -0
  4. agno/db/firestore/firestore.py +47 -0
  5. agno/db/gcs_json/gcs_json_db.py +47 -0
  6. agno/db/in_memory/in_memory_db.py +47 -0
  7. agno/db/json/json_db.py +47 -0
  8. agno/db/mongo/async_mongo.py +229 -0
  9. agno/db/mongo/mongo.py +47 -0
  10. agno/db/mongo/schemas.py +16 -0
  11. agno/db/mysql/async_mysql.py +47 -0
  12. agno/db/mysql/mysql.py +47 -0
  13. agno/db/postgres/async_postgres.py +231 -0
  14. agno/db/postgres/postgres.py +239 -0
  15. agno/db/postgres/schemas.py +19 -0
  16. agno/db/redis/redis.py +47 -0
  17. agno/db/singlestore/singlestore.py +47 -0
  18. agno/db/sqlite/async_sqlite.py +242 -0
  19. agno/db/sqlite/schemas.py +18 -0
  20. agno/db/sqlite/sqlite.py +239 -0
  21. agno/db/surrealdb/surrealdb.py +47 -0
  22. agno/knowledge/chunking/code.py +90 -0
  23. agno/knowledge/chunking/document.py +62 -2
  24. agno/knowledge/chunking/strategy.py +14 -0
  25. agno/knowledge/knowledge.py +7 -1
  26. agno/knowledge/reader/arxiv_reader.py +1 -0
  27. agno/knowledge/reader/csv_reader.py +1 -0
  28. agno/knowledge/reader/docx_reader.py +1 -0
  29. agno/knowledge/reader/firecrawl_reader.py +1 -0
  30. agno/knowledge/reader/json_reader.py +1 -0
  31. agno/knowledge/reader/markdown_reader.py +1 -0
  32. agno/knowledge/reader/pdf_reader.py +1 -0
  33. agno/knowledge/reader/pptx_reader.py +1 -0
  34. agno/knowledge/reader/s3_reader.py +1 -0
  35. agno/knowledge/reader/tavily_reader.py +1 -0
  36. agno/knowledge/reader/text_reader.py +1 -0
  37. agno/knowledge/reader/web_search_reader.py +1 -0
  38. agno/knowledge/reader/website_reader.py +1 -0
  39. agno/knowledge/reader/wikipedia_reader.py +1 -0
  40. agno/knowledge/reader/youtube_reader.py +1 -0
  41. agno/knowledge/utils.py +1 -0
  42. agno/learn/__init__.py +65 -0
  43. agno/learn/config.py +463 -0
  44. agno/learn/curate.py +185 -0
  45. agno/learn/machine.py +690 -0
  46. agno/learn/schemas.py +1043 -0
  47. agno/learn/stores/__init__.py +35 -0
  48. agno/learn/stores/entity_memory.py +3275 -0
  49. agno/learn/stores/learned_knowledge.py +1583 -0
  50. agno/learn/stores/protocol.py +117 -0
  51. agno/learn/stores/session_context.py +1217 -0
  52. agno/learn/stores/user_memory.py +1495 -0
  53. agno/learn/stores/user_profile.py +1220 -0
  54. agno/learn/utils.py +209 -0
  55. agno/models/base.py +59 -0
  56. agno/os/routers/agents/router.py +4 -4
  57. agno/os/routers/knowledge/knowledge.py +7 -0
  58. agno/os/routers/teams/router.py +3 -3
  59. agno/os/routers/workflows/router.py +5 -5
  60. agno/os/utils.py +55 -3
  61. agno/team/team.py +131 -0
  62. agno/tools/browserbase.py +78 -6
  63. agno/tools/google_bigquery.py +11 -2
  64. agno/utils/agent.py +30 -1
  65. agno/workflow/workflow.py +198 -0
  66. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/METADATA +24 -2
  67. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/RECORD +70 -56
  68. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/WHEEL +0 -0
  69. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/licenses/LICENSE +0 -0
  70. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1495 @@
1
+ """
2
+ User Memory Store
3
+ =================
4
+ Storage backend for User Memory learning type.
5
+
6
+ Stores unstructured observations about users that don't fit into
7
+ structured profile fields. These are long-term memories that persist
8
+ across sessions.
9
+
10
+ Key Features:
11
+ - Background extraction from conversations
12
+ - Agent tools for in-conversation updates
13
+ - Multi-user isolation (each user has their own memories)
14
+ - Add, update, delete memory operations
15
+
16
+ Scope:
17
+ - Memories are retrieved by user_id only
18
+ - agent_id/team_id stored in DB columns for audit trail
19
+ - agent_id/team_id stored on individual memories for granular audit
20
+
21
+ Supported Modes:
22
+ - ALWAYS: Automatic extraction after conversations
23
+ - AGENTIC: Agent calls update_user_memory tool directly
24
+ """
25
+
26
+ import uuid
27
+ from copy import deepcopy
28
+ from dataclasses import dataclass, field
29
+ from os import getenv
30
+ from textwrap import dedent
31
+ from typing import Any, Callable, List, Optional, Union
32
+
33
+ from agno.learn.config import LearningMode, UserMemoryConfig
34
+ from agno.learn.schemas import Memories
35
+ from agno.learn.stores.protocol import LearningStore
36
+ from agno.learn.utils import from_dict_safe, to_dict_safe
37
+ from agno.utils.log import (
38
+ log_debug,
39
+ log_warning,
40
+ set_log_level_to_debug,
41
+ set_log_level_to_info,
42
+ )
43
+
44
+ try:
45
+ from agno.db.base import AsyncBaseDb, BaseDb
46
+ from agno.models.message import Message
47
+ from agno.tools.function import Function
48
+ except ImportError:
49
+ pass
50
+
51
+
52
+ @dataclass
53
+ class UserMemoryStore(LearningStore):
54
+ """Storage backend for User Memory learning type.
55
+
56
+ Memories are retrieved by user_id only - all agents sharing the same DB
57
+ will see the same memories for a given user. agent_id and team_id are
58
+ stored for audit purposes (both at DB column level and on individual memories).
59
+
60
+ Args:
61
+ config: UserMemoryConfig with all settings including db and model.
62
+ debug_mode: Enable debug logging.
63
+ """
64
+
65
+ config: UserMemoryConfig = field(default_factory=UserMemoryConfig)
66
+ debug_mode: bool = False
67
+
68
+ # State tracking (internal)
69
+ memories_updated: bool = field(default=False, init=False)
70
+ _schema: Any = field(default=None, init=False)
71
+
72
+ def __post_init__(self):
73
+ self._schema = self.config.schema or Memories
74
+
75
+ if self.config.mode == LearningMode.PROPOSE:
76
+ log_warning("UserMemoryStore does not support PROPOSE mode.")
77
+ elif self.config.mode == LearningMode.HITL:
78
+ log_warning("UserMemoryStore does not support HITL mode.")
79
+
80
+ # =========================================================================
81
+ # LearningStore Protocol Implementation
82
+ # =========================================================================
83
+
84
+ @property
85
+ def learning_type(self) -> str:
86
+ """Unique identifier for this learning type."""
87
+ return "user_memory"
88
+
89
+ @property
90
+ def schema(self) -> Any:
91
+ """Schema class used for memories."""
92
+ return self._schema
93
+
94
+ def recall(self, user_id: str, **kwargs) -> Optional[Any]:
95
+ """Retrieve memories from storage.
96
+
97
+ Args:
98
+ user_id: The user to retrieve memories for (required).
99
+ **kwargs: Additional context (ignored).
100
+
101
+ Returns:
102
+ Memories, or None if not found.
103
+ """
104
+ if not user_id:
105
+ return None
106
+ return self.get(user_id=user_id)
107
+
108
+ async def arecall(self, user_id: str, **kwargs) -> Optional[Any]:
109
+ """Async version of recall."""
110
+ if not user_id:
111
+ return None
112
+ return await self.aget(user_id=user_id)
113
+
114
+ def process(
115
+ self,
116
+ messages: List[Any],
117
+ user_id: str,
118
+ agent_id: Optional[str] = None,
119
+ team_id: Optional[str] = None,
120
+ **kwargs,
121
+ ) -> None:
122
+ """Extract memories from messages.
123
+
124
+ Args:
125
+ messages: Conversation messages to analyze.
126
+ user_id: The user to update memories for (required).
127
+ agent_id: Agent context (stored for audit).
128
+ team_id: Team context (stored for audit).
129
+ **kwargs: Additional context (ignored).
130
+ """
131
+ # process only supported in ALWAYS mode
132
+ # for programmatic extraction, use extract_and_save directly
133
+ if self.config.mode != LearningMode.ALWAYS:
134
+ return
135
+
136
+ if not user_id or not messages:
137
+ return
138
+
139
+ self.extract_and_save(
140
+ messages=messages,
141
+ user_id=user_id,
142
+ agent_id=agent_id,
143
+ team_id=team_id,
144
+ )
145
+
146
+ async def aprocess(
147
+ self,
148
+ messages: List[Any],
149
+ user_id: str,
150
+ agent_id: Optional[str] = None,
151
+ team_id: Optional[str] = None,
152
+ **kwargs,
153
+ ) -> None:
154
+ """Async version of process."""
155
+ if self.config.mode != LearningMode.ALWAYS:
156
+ return
157
+
158
+ if not user_id or not messages:
159
+ return
160
+
161
+ await self.aextract_and_save(
162
+ messages=messages,
163
+ user_id=user_id,
164
+ agent_id=agent_id,
165
+ team_id=team_id,
166
+ )
167
+
168
+ def build_context(self, data: Any) -> str:
169
+ """Build context for the agent.
170
+
171
+ Formats memories data for injection into the agent's system prompt.
172
+ Designed to enable natural, personalized responses without meta-commentary
173
+ about memory systems.
174
+
175
+ Args:
176
+ data: Memories data from recall().
177
+
178
+ Returns:
179
+ Context string to inject into the agent's system prompt.
180
+ """
181
+ # Build tool documentation based on what's enabled
182
+ tool_docs = self._build_tool_documentation()
183
+
184
+ if not data:
185
+ if self._should_expose_tools:
186
+ return (
187
+ dedent("""\
188
+ <user_memory>
189
+ No memories saved about this user yet.
190
+
191
+ """)
192
+ + tool_docs
193
+ + dedent("""
194
+ </user_memory>""")
195
+ )
196
+ return ""
197
+
198
+ # Build memories section
199
+ memories_text = None
200
+ if hasattr(data, "get_memories_text"):
201
+ memories_text = data.get_memories_text()
202
+ elif hasattr(data, "memories") and data.memories:
203
+ memories_text = "\n".join(f"- {m.get('content', str(m))}" for m in data.memories)
204
+
205
+ if not memories_text:
206
+ if self._should_expose_tools:
207
+ return (
208
+ dedent("""\
209
+ <user_memory>
210
+ No memories saved about this user yet.
211
+
212
+ """)
213
+ + tool_docs
214
+ + dedent("""
215
+ </user_memory>""")
216
+ )
217
+ return ""
218
+
219
+ context = "<user_memory>\n"
220
+ context += memories_text + "\n"
221
+
222
+ context += dedent("""
223
+ <memory_application_guidelines>
224
+ Apply this knowledge naturally - respond as if you inherently know this information,
225
+ exactly as a colleague would recall shared history without narrating their thought process.
226
+
227
+ - Selectively apply memories based on relevance to the current query
228
+ - Never say "based on my memory" or "I remember that" - just use the information naturally
229
+ - Current conversation always takes precedence over stored memories
230
+ - Use memories to calibrate tone, depth, and examples without announcing it
231
+ </memory_application_guidelines>""")
232
+
233
+ if self._should_expose_tools:
234
+ context += (
235
+ dedent("""
236
+
237
+ <memory_updates>
238
+
239
+ """)
240
+ + tool_docs
241
+ + dedent("""
242
+ </memory_updates>""")
243
+ )
244
+
245
+ context += "\n</user_memory>"
246
+
247
+ return context
248
+
249
+ def _build_tool_documentation(self) -> str:
250
+ """Build documentation for available memory tools.
251
+
252
+ Returns:
253
+ String documenting which tools are available and when to use them.
254
+ """
255
+ docs = []
256
+
257
+ if self.config.agent_can_update_memories:
258
+ docs.append(
259
+ "Use `update_user_memory` to save observations, preferences, and context about this user "
260
+ "that would help personalize future conversations or avoid asking the same questions."
261
+ )
262
+
263
+ return "\n\n".join(docs) if docs else ""
264
+
265
+ def get_tools(
266
+ self,
267
+ user_id: Optional[str] = None,
268
+ agent_id: Optional[str] = None,
269
+ team_id: Optional[str] = None,
270
+ **kwargs,
271
+ ) -> List[Callable]:
272
+ """Get tools to expose to agent.
273
+
274
+ Args:
275
+ user_id: The user context (required for tool to work).
276
+ agent_id: Agent context (stored for audit).
277
+ team_id: Team context (stored for audit).
278
+ **kwargs: Additional context (ignored).
279
+
280
+ Returns:
281
+ List containing update_user_memory tool if enabled.
282
+ """
283
+ if not user_id or not self._should_expose_tools:
284
+ return []
285
+ return self.get_agent_tools(
286
+ user_id=user_id,
287
+ agent_id=agent_id,
288
+ team_id=team_id,
289
+ )
290
+
291
+ async def aget_tools(
292
+ self,
293
+ user_id: Optional[str] = None,
294
+ agent_id: Optional[str] = None,
295
+ team_id: Optional[str] = None,
296
+ **kwargs,
297
+ ) -> List[Callable]:
298
+ """Async version of get_tools."""
299
+ if not user_id or not self._should_expose_tools:
300
+ return []
301
+ return await self.aget_agent_tools(
302
+ user_id=user_id,
303
+ agent_id=agent_id,
304
+ team_id=team_id,
305
+ )
306
+
307
+ @property
308
+ def was_updated(self) -> bool:
309
+ """Check if memories were updated in last operation."""
310
+ return self.memories_updated
311
+
312
+ @property
313
+ def _should_expose_tools(self) -> bool:
314
+ """Check if tools should be exposed to the agent.
315
+
316
+ Returns True if either:
317
+ - mode is AGENTIC (tools are the primary way to update memory), OR
318
+ - enable_agent_tools is explicitly True
319
+ """
320
+ return self.config.mode == LearningMode.AGENTIC or self.config.enable_agent_tools
321
+
322
+ # =========================================================================
323
+ # Properties
324
+ # =========================================================================
325
+
326
+ @property
327
+ def db(self) -> Optional[Union["BaseDb", "AsyncBaseDb"]]:
328
+ """Database backend."""
329
+ return self.config.db
330
+
331
+ @property
332
+ def model(self):
333
+ """Model for extraction."""
334
+ return self.config.model
335
+
336
+ # =========================================================================
337
+ # Debug/Logging
338
+ # =========================================================================
339
+
340
+ def set_log_level(self):
341
+ """Set log level based on debug_mode or environment variable."""
342
+ if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true":
343
+ self.debug_mode = True
344
+ set_log_level_to_debug()
345
+ else:
346
+ set_log_level_to_info()
347
+
348
+ # =========================================================================
349
+ # Agent Tools
350
+ # =========================================================================
351
+
352
+ def get_agent_tools(
353
+ self,
354
+ user_id: str,
355
+ agent_id: Optional[str] = None,
356
+ team_id: Optional[str] = None,
357
+ ) -> List[Callable]:
358
+ """Get the tools to expose to the agent.
359
+
360
+ Args:
361
+ user_id: The user to update (required).
362
+ agent_id: Agent context (stored for audit).
363
+ team_id: Team context (stored for audit).
364
+
365
+ Returns:
366
+ List of callable tools based on config settings.
367
+ """
368
+ tools = []
369
+
370
+ # Memory update tool (delegates to extraction)
371
+ if self.config.agent_can_update_memories:
372
+
373
+ def update_user_memory(task: str) -> str:
374
+ """Save or update information about this user for future conversations.
375
+
376
+ Use this when you learn something worth remembering - information that would
377
+ help personalize future interactions or provide continuity across sessions.
378
+
379
+ Args:
380
+ task: What to save, update, or remove. Be specific and factual.
381
+ Good examples:
382
+ - "User is a senior engineer at Stripe working on payments"
383
+ - "Prefers concise responses without lengthy explanations"
384
+ - "Update: User moved from NYC to London"
385
+ - "Remove the memory about their old job at Acme"
386
+ Bad examples:
387
+ - "User seems nice" (too vague)
388
+ - "Had a meeting today" (not durable)
389
+
390
+ Returns:
391
+ Confirmation of what was saved/updated.
392
+ """
393
+ return self.run_memories_update(
394
+ task=task,
395
+ user_id=user_id,
396
+ agent_id=agent_id,
397
+ team_id=team_id,
398
+ )
399
+
400
+ tools.append(update_user_memory)
401
+
402
+ return tools
403
+
404
+ async def aget_agent_tools(
405
+ self,
406
+ user_id: str,
407
+ agent_id: Optional[str] = None,
408
+ team_id: Optional[str] = None,
409
+ ) -> List[Callable]:
410
+ """Get the async tools to expose to the agent."""
411
+ tools = []
412
+
413
+ if self.config.agent_can_update_memories:
414
+
415
+ async def update_user_memory(task: str) -> str:
416
+ """Save or update information about this user for future conversations.
417
+
418
+ Use this when you learn something worth remembering - information that would
419
+ help personalize future interactions or provide continuity across sessions.
420
+
421
+ Args:
422
+ task: What to save, update, or remove. Be specific and factual.
423
+ Good examples:
424
+ - "User is a senior engineer at Stripe working on payments"
425
+ - "Prefers concise responses without lengthy explanations"
426
+ - "Update: User moved from NYC to London"
427
+ - "Remove the memory about their old job at Acme"
428
+ Bad examples:
429
+ - "User seems nice" (too vague)
430
+ - "Had a meeting today" (not durable)
431
+
432
+ Returns:
433
+ Confirmation of what was saved/updated.
434
+ """
435
+ return await self.arun_memories_update(
436
+ task=task,
437
+ user_id=user_id,
438
+ agent_id=agent_id,
439
+ team_id=team_id,
440
+ )
441
+
442
+ tools.append(update_user_memory)
443
+
444
+ return tools
445
+
446
+ # =========================================================================
447
+ # Read Operations
448
+ # =========================================================================
449
+
450
+ def get(self, user_id: str) -> Optional[Any]:
451
+ """Retrieve memories by user_id.
452
+
453
+ Args:
454
+ user_id: The unique user identifier.
455
+
456
+ Returns:
457
+ Memories as schema instance, or None if not found.
458
+ """
459
+ if not self.db:
460
+ return None
461
+
462
+ try:
463
+ result = self.db.get_learning(
464
+ learning_type=self.learning_type,
465
+ user_id=user_id,
466
+ )
467
+
468
+ if result and result.get("content"): # type: ignore[union-attr]
469
+ return from_dict_safe(self.schema, result["content"]) # type: ignore[index]
470
+
471
+ return None
472
+
473
+ except Exception as e:
474
+ log_debug(f"UserMemoryStore.get failed for user_id={user_id}: {e}")
475
+ return None
476
+
477
+ async def aget(self, user_id: str) -> Optional[Any]:
478
+ """Async version of get."""
479
+ if not self.db:
480
+ return None
481
+
482
+ try:
483
+ if isinstance(self.db, AsyncBaseDb):
484
+ result = await self.db.get_learning(
485
+ learning_type=self.learning_type,
486
+ user_id=user_id,
487
+ )
488
+ else:
489
+ result = self.db.get_learning(
490
+ learning_type=self.learning_type,
491
+ user_id=user_id,
492
+ )
493
+
494
+ if result and result.get("content"):
495
+ return from_dict_safe(self.schema, result["content"])
496
+
497
+ return None
498
+
499
+ except Exception as e:
500
+ log_debug(f"UserMemoryStore.aget failed for user_id={user_id}: {e}")
501
+ return None
502
+
503
+ # =========================================================================
504
+ # Write Operations
505
+ # =========================================================================
506
+
507
+ def save(
508
+ self,
509
+ user_id: str,
510
+ memories: Any,
511
+ agent_id: Optional[str] = None,
512
+ team_id: Optional[str] = None,
513
+ ) -> None:
514
+ """Save or update memories.
515
+
516
+ Args:
517
+ user_id: The unique user identifier.
518
+ memories: The memories data to save.
519
+ agent_id: Agent context (stored in DB column for audit).
520
+ team_id: Team context (stored in DB column for audit).
521
+ """
522
+ if not self.db or not memories:
523
+ return
524
+
525
+ try:
526
+ content = to_dict_safe(memories)
527
+ if not content:
528
+ return
529
+
530
+ self.db.upsert_learning(
531
+ id=self._build_memories_id(user_id=user_id),
532
+ learning_type=self.learning_type,
533
+ user_id=user_id,
534
+ agent_id=agent_id,
535
+ team_id=team_id,
536
+ content=content,
537
+ )
538
+ log_debug(f"UserMemoryStore.save: saved memories for user_id={user_id}")
539
+
540
+ except Exception as e:
541
+ log_debug(f"UserMemoryStore.save failed for user_id={user_id}: {e}")
542
+
543
+ async def asave(
544
+ self,
545
+ user_id: str,
546
+ memories: Any,
547
+ agent_id: Optional[str] = None,
548
+ team_id: Optional[str] = None,
549
+ ) -> None:
550
+ """Async version of save."""
551
+ if not self.db or not memories:
552
+ return
553
+
554
+ try:
555
+ content = to_dict_safe(memories)
556
+ if not content:
557
+ return
558
+
559
+ if isinstance(self.db, AsyncBaseDb):
560
+ await self.db.upsert_learning(
561
+ id=self._build_memories_id(user_id=user_id),
562
+ learning_type=self.learning_type,
563
+ user_id=user_id,
564
+ agent_id=agent_id,
565
+ team_id=team_id,
566
+ content=content,
567
+ )
568
+ else:
569
+ self.db.upsert_learning(
570
+ id=self._build_memories_id(user_id=user_id),
571
+ learning_type=self.learning_type,
572
+ user_id=user_id,
573
+ agent_id=agent_id,
574
+ team_id=team_id,
575
+ content=content,
576
+ )
577
+ log_debug(f"UserMemoryStore.asave: saved memories for user_id={user_id}")
578
+
579
+ except Exception as e:
580
+ log_debug(f"UserMemoryStore.asave failed for user_id={user_id}: {e}")
581
+
582
+ # =========================================================================
583
+ # Delete Operations
584
+ # =========================================================================
585
+
586
+ def delete(self, user_id: str) -> bool:
587
+ """Delete memories for a user.
588
+
589
+ Args:
590
+ user_id: The unique user identifier.
591
+
592
+ Returns:
593
+ True if deleted, False otherwise.
594
+ """
595
+ if not self.db:
596
+ return False
597
+
598
+ try:
599
+ memories_id = self._build_memories_id(user_id=user_id)
600
+ return self.db.delete_learning(id=memories_id) # type: ignore[return-value]
601
+ except Exception as e:
602
+ log_debug(f"UserMemoryStore.delete failed for user_id={user_id}: {e}")
603
+ return False
604
+
605
+ async def adelete(self, user_id: str) -> bool:
606
+ """Async version of delete."""
607
+ if not self.db:
608
+ return False
609
+
610
+ try:
611
+ memories_id = self._build_memories_id(user_id=user_id)
612
+ if isinstance(self.db, AsyncBaseDb):
613
+ return await self.db.delete_learning(id=memories_id)
614
+ else:
615
+ return self.db.delete_learning(id=memories_id)
616
+ except Exception as e:
617
+ log_debug(f"UserMemoryStore.adelete failed for user_id={user_id}: {e}")
618
+ return False
619
+
620
+ def clear(
621
+ self,
622
+ user_id: str,
623
+ agent_id: Optional[str] = None,
624
+ team_id: Optional[str] = None,
625
+ ) -> None:
626
+ """Clear all memories for a user (reset to empty).
627
+
628
+ Args:
629
+ user_id: The unique user identifier.
630
+ agent_id: Agent context (stored for audit).
631
+ team_id: Team context (stored for audit).
632
+ """
633
+ if not self.db:
634
+ return
635
+
636
+ try:
637
+ empty_memories = self.schema(user_id=user_id)
638
+ self.save(user_id=user_id, memories=empty_memories, agent_id=agent_id, team_id=team_id)
639
+ log_debug(f"UserMemoryStore.clear: cleared memories for user_id={user_id}")
640
+ except Exception as e:
641
+ log_debug(f"UserMemoryStore.clear failed for user_id={user_id}: {e}")
642
+
643
+ async def aclear(
644
+ self,
645
+ user_id: str,
646
+ agent_id: Optional[str] = None,
647
+ team_id: Optional[str] = None,
648
+ ) -> None:
649
+ """Async version of clear."""
650
+ if not self.db:
651
+ return
652
+
653
+ try:
654
+ empty_memories = self.schema(user_id=user_id)
655
+ await self.asave(user_id=user_id, memories=empty_memories, agent_id=agent_id, team_id=team_id)
656
+ log_debug(f"UserMemoryStore.aclear: cleared memories for user_id={user_id}")
657
+ except Exception as e:
658
+ log_debug(f"UserMemoryStore.aclear failed for user_id={user_id}: {e}")
659
+
660
+ # =========================================================================
661
+ # Memory Operations
662
+ # =========================================================================
663
+
664
+ def add_memory(
665
+ self,
666
+ user_id: str,
667
+ memory: str,
668
+ agent_id: Optional[str] = None,
669
+ team_id: Optional[str] = None,
670
+ **kwargs,
671
+ ) -> Optional[str]:
672
+ """Add a single memory.
673
+
674
+ Args:
675
+ user_id: The unique user identifier.
676
+ memory: The memory text to add.
677
+ agent_id: Agent that added this (stored for audit).
678
+ team_id: Team context (stored for audit).
679
+ **kwargs: Additional fields for the memory.
680
+
681
+ Returns:
682
+ The memory ID if added, None otherwise.
683
+ """
684
+ memories_data = self.get(user_id=user_id)
685
+
686
+ if memories_data is None:
687
+ memories_data = self.schema(user_id=user_id)
688
+
689
+ memory_id = None
690
+ if hasattr(memories_data, "add_memory"):
691
+ memory_id = memories_data.add_memory(memory, **kwargs)
692
+ elif hasattr(memories_data, "memories"):
693
+ memory_id = str(uuid.uuid4())[:8]
694
+ memory_entry = {"id": memory_id, "content": memory, **kwargs}
695
+ if agent_id:
696
+ memory_entry["added_by_agent"] = agent_id
697
+ if team_id:
698
+ memory_entry["added_by_team"] = team_id
699
+ memories_data.memories.append(memory_entry)
700
+
701
+ self.save(user_id=user_id, memories=memories_data, agent_id=agent_id, team_id=team_id)
702
+ log_debug(f"UserMemoryStore.add_memory: added memory for user_id={user_id}")
703
+
704
+ return memory_id
705
+
706
+ async def aadd_memory(
707
+ self,
708
+ user_id: str,
709
+ memory: str,
710
+ agent_id: Optional[str] = None,
711
+ team_id: Optional[str] = None,
712
+ **kwargs,
713
+ ) -> Optional[str]:
714
+ """Async version of add_memory."""
715
+ memories_data = await self.aget(user_id=user_id)
716
+
717
+ if memories_data is None:
718
+ memories_data = self.schema(user_id=user_id)
719
+
720
+ memory_id = None
721
+ if hasattr(memories_data, "add_memory"):
722
+ memory_id = memories_data.add_memory(memory, **kwargs)
723
+ elif hasattr(memories_data, "memories"):
724
+ memory_id = str(uuid.uuid4())[:8]
725
+ memory_entry = {"id": memory_id, "content": memory, **kwargs}
726
+ if agent_id:
727
+ memory_entry["added_by_agent"] = agent_id
728
+ if team_id:
729
+ memory_entry["added_by_team"] = team_id
730
+ memories_data.memories.append(memory_entry)
731
+
732
+ await self.asave(user_id=user_id, memories=memories_data, agent_id=agent_id, team_id=team_id)
733
+ log_debug(f"UserMemoryStore.aadd_memory: added memory for user_id={user_id}")
734
+
735
+ return memory_id
736
+
737
+ # =========================================================================
738
+ # Extraction Operations
739
+ # =========================================================================
740
+
741
+ def extract_and_save(
742
+ self,
743
+ messages: List["Message"],
744
+ user_id: str,
745
+ agent_id: Optional[str] = None,
746
+ team_id: Optional[str] = None,
747
+ ) -> str:
748
+ """Extract memories from messages and save.
749
+
750
+ Args:
751
+ messages: Conversation messages to analyze.
752
+ user_id: The unique user identifier.
753
+ agent_id: Agent context (stored for audit).
754
+ team_id: Team context (stored for audit).
755
+
756
+ Returns:
757
+ Response from model.
758
+ """
759
+ if self.model is None:
760
+ log_warning("UserMemoryStore.extract_and_save: no model provided")
761
+ return "No model provided for memories extraction"
762
+
763
+ if not self.db:
764
+ log_warning("UserMemoryStore.extract_and_save: no database provided")
765
+ return "No DB provided for memories store"
766
+
767
+ log_debug("UserMemoryStore: Extracting memories", center=True)
768
+
769
+ self.memories_updated = False
770
+
771
+ existing_memories = self.get(user_id=user_id)
772
+ existing_data = self._memories_to_list(memories=existing_memories)
773
+
774
+ input_string = self._messages_to_input_string(messages=messages)
775
+
776
+ tools = self._get_extraction_tools(
777
+ user_id=user_id,
778
+ input_string=input_string,
779
+ existing_memories=existing_memories,
780
+ agent_id=agent_id,
781
+ team_id=team_id,
782
+ )
783
+
784
+ functions = self._build_functions_for_model(tools=tools)
785
+
786
+ messages_for_model = [
787
+ self._get_system_message(existing_data=existing_data),
788
+ *messages,
789
+ ]
790
+
791
+ model_copy = deepcopy(self.model)
792
+ response = model_copy.response(
793
+ messages=messages_for_model,
794
+ tools=functions,
795
+ )
796
+
797
+ if response.tool_executions:
798
+ self.memories_updated = True
799
+
800
+ log_debug("UserMemoryStore: Extraction complete", center=True)
801
+
802
+ return response.content or ("Memories updated" if self.memories_updated else "No updates needed")
803
+
804
+ async def aextract_and_save(
805
+ self,
806
+ messages: List["Message"],
807
+ user_id: str,
808
+ agent_id: Optional[str] = None,
809
+ team_id: Optional[str] = None,
810
+ ) -> str:
811
+ """Async version of extract_and_save."""
812
+ if self.model is None:
813
+ log_warning("UserMemoryStore.aextract_and_save: no model provided")
814
+ return "No model provided for memories extraction"
815
+
816
+ if not self.db:
817
+ log_warning("UserMemoryStore.aextract_and_save: no database provided")
818
+ return "No DB provided for memories store"
819
+
820
+ log_debug("UserMemoryStore: Extracting memories (async)", center=True)
821
+
822
+ self.memories_updated = False
823
+
824
+ existing_memories = await self.aget(user_id=user_id)
825
+ existing_data = self._memories_to_list(memories=existing_memories)
826
+
827
+ input_string = self._messages_to_input_string(messages=messages)
828
+
829
+ tools = await self._aget_extraction_tools(
830
+ user_id=user_id,
831
+ input_string=input_string,
832
+ existing_memories=existing_memories,
833
+ agent_id=agent_id,
834
+ team_id=team_id,
835
+ )
836
+
837
+ functions = self._build_functions_for_model(tools=tools)
838
+
839
+ messages_for_model = [
840
+ self._get_system_message(existing_data=existing_data),
841
+ *messages,
842
+ ]
843
+
844
+ model_copy = deepcopy(self.model)
845
+ response = await model_copy.aresponse(
846
+ messages=messages_for_model,
847
+ tools=functions,
848
+ )
849
+
850
+ if response.tool_executions:
851
+ self.memories_updated = True
852
+
853
+ log_debug("UserMemoryStore: Extraction complete", center=True)
854
+
855
+ return response.content or ("Memories updated" if self.memories_updated else "No updates needed")
856
+
857
+ # =========================================================================
858
+ # Update Operations (called by agent tool)
859
+ # =========================================================================
860
+
861
+ def run_memories_update(
862
+ self,
863
+ task: str,
864
+ user_id: str,
865
+ agent_id: Optional[str] = None,
866
+ team_id: Optional[str] = None,
867
+ ) -> str:
868
+ """Run a memories update task.
869
+
870
+ Args:
871
+ task: The update task description.
872
+ user_id: The unique user identifier.
873
+ agent_id: Agent context (stored for audit).
874
+ team_id: Team context (stored for audit).
875
+
876
+ Returns:
877
+ Response from model.
878
+ """
879
+ from agno.models.message import Message
880
+
881
+ messages = [Message(role="user", content=task)]
882
+ return self.extract_and_save(
883
+ messages=messages,
884
+ user_id=user_id,
885
+ agent_id=agent_id,
886
+ team_id=team_id,
887
+ )
888
+
889
+ async def arun_memories_update(
890
+ self,
891
+ task: str,
892
+ user_id: str,
893
+ agent_id: Optional[str] = None,
894
+ team_id: Optional[str] = None,
895
+ ) -> str:
896
+ """Async version of run_memories_update."""
897
+ from agno.models.message import Message
898
+
899
+ messages = [Message(role="user", content=task)]
900
+ return await self.aextract_and_save(
901
+ messages=messages,
902
+ user_id=user_id,
903
+ agent_id=agent_id,
904
+ team_id=team_id,
905
+ )
906
+
907
+ # =========================================================================
908
+ # Private Helpers
909
+ # =========================================================================
910
+
911
+ def _build_memories_id(self, user_id: str) -> str:
912
+ """Build a unique memories ID."""
913
+ return f"memories_{user_id}"
914
+
915
+ def _memories_to_list(self, memories: Optional[Any]) -> List[dict]:
916
+ """Convert memories to list of memory dicts for prompt."""
917
+ if not memories:
918
+ return []
919
+
920
+ result = []
921
+
922
+ if hasattr(memories, "memories") and memories.memories:
923
+ for mem in memories.memories:
924
+ if isinstance(mem, dict):
925
+ memory_id = mem.get("id", str(uuid.uuid4())[:8])
926
+ content = mem.get("content", str(mem))
927
+ else:
928
+ memory_id = str(uuid.uuid4())[:8]
929
+ content = str(mem)
930
+ result.append({"id": memory_id, "content": content})
931
+
932
+ return result
933
+
934
+ def _messages_to_input_string(self, messages: List["Message"]) -> str:
935
+ """Convert messages to input string."""
936
+ if len(messages) == 1:
937
+ return messages[0].get_content_string()
938
+ else:
939
+ return "\n".join([f"{m.role}: {m.get_content_string()}" for m in messages if m.content])
940
+
941
+ def _build_functions_for_model(self, tools: List[Callable]) -> List["Function"]:
942
+ """Convert callables to Functions for model."""
943
+ from agno.tools.function import Function
944
+
945
+ functions = []
946
+ seen_names = set()
947
+
948
+ for tool in tools:
949
+ try:
950
+ name = tool.__name__
951
+ if name in seen_names:
952
+ continue
953
+ seen_names.add(name)
954
+
955
+ func = Function.from_callable(tool, strict=True)
956
+ func.strict = True
957
+ functions.append(func)
958
+ log_debug(f"Added function {func.name}")
959
+ except Exception as e:
960
+ log_warning(f"Could not add function {tool}: {e}")
961
+
962
+ return functions
963
+
964
+ def _get_system_message(
965
+ self,
966
+ existing_data: List[dict],
967
+ ) -> "Message":
968
+ """Build system message for memory extraction."""
969
+ from agno.models.message import Message
970
+
971
+ if self.config.system_message is not None:
972
+ return Message(role="system", content=self.config.system_message)
973
+
974
+ system_prompt = dedent("""\
975
+ You are building a memory of this user to enable personalized, contextual interactions.
976
+
977
+ Your goal is NOT to create a database of facts, but to build working knowledge that helps an AI assistant engage naturally with this person - knowing their context, adapting to their preferences, and providing continuity across conversations.
978
+
979
+ ## Memory Philosophy
980
+
981
+ Think of memories as what a thoughtful colleague would remember after working with someone:
982
+ - Their role and what they're working on
983
+ - How they prefer to communicate
984
+ - What matters to them and what frustrates them
985
+ - Ongoing projects or situations worth tracking
986
+
987
+ Memories should make future interactions feel informed and personal, not robotic or surveillance-like.
988
+
989
+ ## Memory Categories
990
+
991
+ Use memory tools for contextual information organized by relevance:
992
+
993
+ **Work/Project Context** - What they're building, their role, current focus
994
+ **Personal Context** - Preferences, communication style, background that shapes interactions
995
+ **Top of Mind** - Active situations, ongoing challenges, time-sensitive context
996
+ **Patterns** - How they work, what they value, recurring themes
997
+
998
+ """)
999
+
1000
+ # Custom instructions or defaults
1001
+ capture_instructions = self.config.instructions or dedent("""\
1002
+ ## What To Capture
1003
+
1004
+ **DO save:**
1005
+ - Role, company, and what they're working on
1006
+ - Communication preferences (brevity vs detail, technical depth, tone)
1007
+ - Goals, priorities, and current challenges
1008
+ - Preferences that affect how to help them (tools, frameworks, approaches)
1009
+ - Context that would be awkward to ask about again
1010
+ - Patterns in how they think and work
1011
+
1012
+ **DO NOT save:**
1013
+ - Sensitive personal information (health conditions, financial details, relationships) unless directly relevant to helping them
1014
+ - One-off details unlikely to matter in future conversations
1015
+ - Information they'd find creepy to have remembered
1016
+ - Inferences or assumptions - only save what they've actually stated
1017
+ - Duplicates of existing memories (update instead)
1018
+ - Trivial preferences that don't affect interactions\
1019
+ """)
1020
+
1021
+ system_prompt += capture_instructions
1022
+
1023
+ system_prompt += dedent("""
1024
+
1025
+ ## Writing Style
1026
+
1027
+ Write memories as concise, factual statements in third person:
1028
+
1029
+ **Good memories:**
1030
+ - "Founder and CEO of Acme, a 10-person AI startup"
1031
+ - "Prefers direct feedback without excessive caveats"
1032
+ - "Currently preparing for Series A fundraise, targeting $50M"
1033
+ - "Values simplicity over cleverness in code architecture"
1034
+
1035
+ **Bad memories:**
1036
+ - "User mentioned they work at a company" (too vague)
1037
+ - "User seems to like technology" (obvious/not useful)
1038
+ - "Had a meeting yesterday" (not durable)
1039
+ - "User is stressed about fundraising" (inference without direct statement)
1040
+
1041
+ ## Consolidation Over Accumulation
1042
+
1043
+ **Critical:** Prefer updating existing memories over adding new ones.
1044
+
1045
+ - If new information extends an existing memory, UPDATE it
1046
+ - If new information contradicts an existing memory, REPLACE it
1047
+ - If information is truly new and distinct, then add it
1048
+ - Periodically consolidate related memories into cohesive summaries
1049
+ - Delete memories that are no longer accurate or relevant
1050
+
1051
+ Think of memory maintenance like note-taking: a few well-organized notes beat many scattered fragments.
1052
+
1053
+ """)
1054
+
1055
+ # Current memories section
1056
+ system_prompt += "## Current Memories\n\n"
1057
+
1058
+ if existing_data:
1059
+ system_prompt += "Existing memories for this user:\n"
1060
+ for entry in existing_data:
1061
+ system_prompt += f"- [{entry['id']}] {entry['content']}\n"
1062
+ system_prompt += dedent("""
1063
+ Review these before adding new ones:
1064
+ - UPDATE if new information extends or modifies an existing memory
1065
+ - DELETE if a memory is no longer accurate
1066
+ - Only ADD if the information is genuinely new and distinct
1067
+ """)
1068
+ else:
1069
+ system_prompt += "No existing memories. Extract what's worth remembering from this conversation.\n"
1070
+
1071
+ # Available actions
1072
+ system_prompt += "\n## Available Actions\n\n"
1073
+
1074
+ if self.config.enable_add_memory:
1075
+ system_prompt += "- `add_memory`: Add a new memory (only if genuinely new information)\n"
1076
+ if self.config.enable_update_memory:
1077
+ system_prompt += "- `update_memory`: Update existing memory with new/corrected information\n"
1078
+ if self.config.enable_delete_memory:
1079
+ system_prompt += "- `delete_memory`: Remove outdated or incorrect memory\n"
1080
+ if self.config.enable_clear_memories:
1081
+ system_prompt += "- `clear_all_memories`: Reset all memories (use rarely)\n"
1082
+
1083
+ # Examples
1084
+ system_prompt += dedent("""
1085
+ ## Examples
1086
+
1087
+ **Example 1: New user introduction**
1088
+ User: "I'm Sarah, I run engineering at Stripe. We're migrating to Kubernetes."
1089
+ → add_memory("Engineering lead at Stripe, currently migrating infrastructure to Kubernetes")
1090
+
1091
+ **Example 2: Updating existing context**
1092
+ Existing memory: "Working on Series A fundraise"
1093
+ User: "We closed our Series A last week! $12M from Sequoia."
1094
+ → update_memory(id, "Closed $12M Series A from Sequoia")
1095
+
1096
+ **Example 3: Learning preferences**
1097
+ User: "Can you skip the explanations and just give me the code?"
1098
+ → add_memory("Prefers concise responses with code over lengthy explanations")
1099
+
1100
+ **Example 4: Nothing worth saving**
1101
+ User: "What's the weather like?"
1102
+ → No action needed (trivial, no lasting relevance)
1103
+
1104
+ ## Final Guidance
1105
+
1106
+ - Quality over quantity: 5 great memories beat 20 mediocre ones
1107
+ - Durability matters: save information that will still be relevant next month
1108
+ - Respect boundaries: when in doubt about whether to save something, don't
1109
+ - It's fine to do nothing if the conversation reveals nothing worth remembering\
1110
+ """)
1111
+
1112
+ if self.config.additional_instructions:
1113
+ system_prompt += f"\n\n{self.config.additional_instructions}"
1114
+
1115
+ return Message(role="system", content=system_prompt)
1116
+
1117
+ def _get_extraction_tools(
1118
+ self,
1119
+ user_id: str,
1120
+ input_string: str,
1121
+ existing_memories: Optional[Any] = None,
1122
+ agent_id: Optional[str] = None,
1123
+ team_id: Optional[str] = None,
1124
+ ) -> List[Callable]:
1125
+ """Get sync extraction tools for the model."""
1126
+ functions: List[Callable] = []
1127
+
1128
+ if self.config.enable_add_memory:
1129
+
1130
+ def add_memory(memory: str) -> str:
1131
+ """Save a new memory about this user.
1132
+
1133
+ Only add genuinely new information that will help personalize future interactions.
1134
+ Before adding, check if this extends an existing memory (use update_memory instead).
1135
+
1136
+ Args:
1137
+ memory: Concise, factual statement in third person.
1138
+ Good: "Senior engineer at Stripe, working on payment infrastructure"
1139
+ Bad: "User works at a company" (too vague)
1140
+
1141
+ Returns:
1142
+ Confirmation message.
1143
+ """
1144
+ try:
1145
+ memories_data = self.get(user_id=user_id)
1146
+ if memories_data is None:
1147
+ memories_data = self.schema(user_id=user_id)
1148
+
1149
+ if hasattr(memories_data, "memories"):
1150
+ memory_id = str(uuid.uuid4())[:8]
1151
+ memory_entry = {
1152
+ "id": memory_id,
1153
+ "content": memory,
1154
+ "source": input_string[:200] if input_string else None,
1155
+ }
1156
+ if agent_id:
1157
+ memory_entry["added_by_agent"] = agent_id
1158
+ if team_id:
1159
+ memory_entry["added_by_team"] = team_id
1160
+ memories_data.memories.append(memory_entry)
1161
+
1162
+ self.save(user_id=user_id, memories=memories_data, agent_id=agent_id, team_id=team_id)
1163
+ log_debug(f"Memory added: {memory[:50]}...")
1164
+ return f"Memory saved: {memory}"
1165
+ except Exception as e:
1166
+ log_warning(f"Error adding memory: {e}")
1167
+ return f"Error: {e}"
1168
+
1169
+ functions.append(add_memory)
1170
+
1171
+ if self.config.enable_update_memory:
1172
+
1173
+ def update_memory(memory_id: str, memory: str) -> str:
1174
+ """Update an existing memory with new or corrected information.
1175
+
1176
+ Prefer updating over adding when new information extends or modifies
1177
+ something already stored. This keeps memories consolidated and accurate.
1178
+
1179
+ Args:
1180
+ memory_id: The ID of the memory to update (shown in brackets like [abc123]).
1181
+ memory: The updated memory content. Should be a complete replacement,
1182
+ not a diff or addition.
1183
+
1184
+ Returns:
1185
+ Confirmation message.
1186
+ """
1187
+ try:
1188
+ memories_data = self.get(user_id=user_id)
1189
+ if memories_data is None:
1190
+ return "No memories found"
1191
+
1192
+ if hasattr(memories_data, "memories"):
1193
+ for mem in memories_data.memories:
1194
+ if isinstance(mem, dict) and mem.get("id") == memory_id:
1195
+ mem["content"] = memory
1196
+ mem["source"] = input_string[:200] if input_string else None
1197
+ if agent_id:
1198
+ mem["updated_by_agent"] = agent_id
1199
+ if team_id:
1200
+ mem["updated_by_team"] = team_id
1201
+ self.save(user_id=user_id, memories=memories_data, agent_id=agent_id, team_id=team_id)
1202
+ log_debug(f"Memory updated: {memory_id}")
1203
+ return f"Memory updated: {memory}"
1204
+ return f"Memory {memory_id} not found"
1205
+
1206
+ return "No memories field"
1207
+ except Exception as e:
1208
+ log_warning(f"Error updating memory: {e}")
1209
+ return f"Error: {e}"
1210
+
1211
+ functions.append(update_memory)
1212
+
1213
+ if self.config.enable_delete_memory:
1214
+
1215
+ def delete_memory(memory_id: str) -> str:
1216
+ """Remove a memory that is outdated, incorrect, or no longer relevant.
1217
+
1218
+ Delete when:
1219
+ - Information is no longer accurate (e.g., they changed jobs)
1220
+ - The memory was a misunderstanding
1221
+ - It's been superseded by a more complete memory
1222
+
1223
+ Args:
1224
+ memory_id: The ID of the memory to delete (shown in brackets like [abc123]).
1225
+
1226
+ Returns:
1227
+ Confirmation message.
1228
+ """
1229
+ try:
1230
+ memories_data = self.get(user_id=user_id)
1231
+ if memories_data is None:
1232
+ return "No memories found"
1233
+
1234
+ if hasattr(memories_data, "memories"):
1235
+ original_len = len(memories_data.memories)
1236
+ memories_data.memories = [
1237
+ mem
1238
+ for mem in memories_data.memories
1239
+ if not (isinstance(mem, dict) and mem.get("id") == memory_id)
1240
+ ]
1241
+ if len(memories_data.memories) < original_len:
1242
+ self.save(user_id=user_id, memories=memories_data, agent_id=agent_id, team_id=team_id)
1243
+ log_debug(f"Memory deleted: {memory_id}")
1244
+ return f"Memory {memory_id} deleted"
1245
+ return f"Memory {memory_id} not found"
1246
+
1247
+ return "No memories field"
1248
+ except Exception as e:
1249
+ log_warning(f"Error deleting memory: {e}")
1250
+ return f"Error: {e}"
1251
+
1252
+ functions.append(delete_memory)
1253
+
1254
+ if self.config.enable_clear_memories:
1255
+
1256
+ def clear_all_memories() -> str:
1257
+ """Clear all memories for this user. Use sparingly.
1258
+
1259
+ Returns:
1260
+ Confirmation message.
1261
+ """
1262
+ try:
1263
+ self.clear(user_id=user_id, agent_id=agent_id, team_id=team_id)
1264
+ log_debug("All memories cleared")
1265
+ return "All memories cleared"
1266
+ except Exception as e:
1267
+ log_warning(f"Error clearing memories: {e}")
1268
+ return f"Error: {e}"
1269
+
1270
+ functions.append(clear_all_memories)
1271
+
1272
+ return functions
1273
+
1274
+ async def _aget_extraction_tools(
1275
+ self,
1276
+ user_id: str,
1277
+ input_string: str,
1278
+ existing_memories: Optional[Any] = None,
1279
+ agent_id: Optional[str] = None,
1280
+ team_id: Optional[str] = None,
1281
+ ) -> List[Callable]:
1282
+ """Get async extraction tools for the model."""
1283
+ functions: List[Callable] = []
1284
+
1285
+ if self.config.enable_add_memory:
1286
+
1287
+ async def add_memory(memory: str) -> str:
1288
+ """Save a new memory about this user.
1289
+
1290
+ Only add genuinely new information that will help personalize future interactions.
1291
+ Before adding, check if this extends an existing memory (use update_memory instead).
1292
+
1293
+ Args:
1294
+ memory: Concise, factual statement in third person.
1295
+ Good: "Senior engineer at Stripe, working on payment infrastructure"
1296
+ Bad: "User works at a company" (too vague)
1297
+
1298
+ Returns:
1299
+ Confirmation message.
1300
+ """
1301
+ try:
1302
+ memories_data = await self.aget(user_id=user_id)
1303
+ if memories_data is None:
1304
+ memories_data = self.schema(user_id=user_id)
1305
+
1306
+ if hasattr(memories_data, "memories"):
1307
+ memory_id = str(uuid.uuid4())[:8]
1308
+ memory_entry = {
1309
+ "id": memory_id,
1310
+ "content": memory,
1311
+ "source": input_string[:200] if input_string else None,
1312
+ }
1313
+ if agent_id:
1314
+ memory_entry["added_by_agent"] = agent_id
1315
+ if team_id:
1316
+ memory_entry["added_by_team"] = team_id
1317
+ memories_data.memories.append(memory_entry)
1318
+
1319
+ await self.asave(user_id=user_id, memories=memories_data, agent_id=agent_id, team_id=team_id)
1320
+ log_debug(f"Memory added: {memory[:50]}...")
1321
+ return f"Memory saved: {memory}"
1322
+ except Exception as e:
1323
+ log_warning(f"Error adding memory: {e}")
1324
+ return f"Error: {e}"
1325
+
1326
+ functions.append(add_memory)
1327
+
1328
+ if self.config.enable_update_memory:
1329
+
1330
+ async def update_memory(memory_id: str, memory: str) -> str:
1331
+ """Update an existing memory with new or corrected information.
1332
+
1333
+ Prefer updating over adding when new information extends or modifies
1334
+ something already stored. This keeps memories consolidated and accurate.
1335
+
1336
+ Args:
1337
+ memory_id: The ID of the memory to update (shown in brackets like [abc123]).
1338
+ memory: The updated memory content. Should be a complete replacement,
1339
+ not a diff or addition.
1340
+
1341
+ Returns:
1342
+ Confirmation message.
1343
+ """
1344
+ try:
1345
+ memories_data = await self.aget(user_id=user_id)
1346
+ if memories_data is None:
1347
+ return "No memories found"
1348
+
1349
+ if hasattr(memories_data, "memories"):
1350
+ for mem in memories_data.memories:
1351
+ if isinstance(mem, dict) and mem.get("id") == memory_id:
1352
+ mem["content"] = memory
1353
+ mem["source"] = input_string[:200] if input_string else None
1354
+ if agent_id:
1355
+ mem["updated_by_agent"] = agent_id
1356
+ if team_id:
1357
+ mem["updated_by_team"] = team_id
1358
+ await self.asave(
1359
+ user_id=user_id, memories=memories_data, agent_id=agent_id, team_id=team_id
1360
+ )
1361
+ log_debug(f"Memory updated: {memory_id}")
1362
+ return f"Memory updated: {memory}"
1363
+ return f"Memory {memory_id} not found"
1364
+
1365
+ return "No memories field"
1366
+ except Exception as e:
1367
+ log_warning(f"Error updating memory: {e}")
1368
+ return f"Error: {e}"
1369
+
1370
+ functions.append(update_memory)
1371
+
1372
+ if self.config.enable_delete_memory:
1373
+
1374
+ async def delete_memory(memory_id: str) -> str:
1375
+ """Remove a memory that is outdated, incorrect, or no longer relevant.
1376
+
1377
+ Delete when:
1378
+ - Information is no longer accurate (e.g., they changed jobs)
1379
+ - The memory was a misunderstanding
1380
+ - It's been superseded by a more complete memory
1381
+
1382
+ Args:
1383
+ memory_id: The ID of the memory to delete (shown in brackets like [abc123]).
1384
+
1385
+ Returns:
1386
+ Confirmation message.
1387
+ """
1388
+ try:
1389
+ memories_data = await self.aget(user_id=user_id)
1390
+ if memories_data is None:
1391
+ return "No memories found"
1392
+
1393
+ if hasattr(memories_data, "memories"):
1394
+ original_len = len(memories_data.memories)
1395
+ memories_data.memories = [
1396
+ mem
1397
+ for mem in memories_data.memories
1398
+ if not (isinstance(mem, dict) and mem.get("id") == memory_id)
1399
+ ]
1400
+ if len(memories_data.memories) < original_len:
1401
+ await self.asave(
1402
+ user_id=user_id, memories=memories_data, agent_id=agent_id, team_id=team_id
1403
+ )
1404
+ log_debug(f"Memory deleted: {memory_id}")
1405
+ return f"Memory {memory_id} deleted"
1406
+ return f"Memory {memory_id} not found"
1407
+
1408
+ return "No memories field"
1409
+ except Exception as e:
1410
+ log_warning(f"Error deleting memory: {e}")
1411
+ return f"Error: {e}"
1412
+
1413
+ functions.append(delete_memory)
1414
+
1415
+ if self.config.enable_clear_memories:
1416
+
1417
+ async def clear_all_memories() -> str:
1418
+ """Clear all memories for this user. Use sparingly.
1419
+
1420
+ Returns:
1421
+ Confirmation message.
1422
+ """
1423
+ try:
1424
+ await self.aclear(user_id=user_id, agent_id=agent_id, team_id=team_id)
1425
+ log_debug("All memories cleared")
1426
+ return "All memories cleared"
1427
+ except Exception as e:
1428
+ log_warning(f"Error clearing memories: {e}")
1429
+ return f"Error: {e}"
1430
+
1431
+ functions.append(clear_all_memories)
1432
+
1433
+ return functions
1434
+
1435
+ # =========================================================================
1436
+ # Representation
1437
+ # =========================================================================
1438
+
1439
+ def __repr__(self) -> str:
1440
+ """String representation for debugging."""
1441
+ has_db = self.db is not None
1442
+ has_model = self.model is not None
1443
+ return (
1444
+ f"UserMemoryStore("
1445
+ f"mode={self.config.mode.value}, "
1446
+ f"db={has_db}, "
1447
+ f"model={has_model}, "
1448
+ f"enable_agent_tools={self.config.enable_agent_tools})"
1449
+ )
1450
+
1451
+ def print(self, user_id: str, *, raw: bool = False) -> None:
1452
+ """Print formatted memories.
1453
+
1454
+ Args:
1455
+ user_id: The user to print memories for.
1456
+ raw: If True, print raw dict using pprint instead of formatted panel.
1457
+
1458
+ Example:
1459
+ >>> store.print(user_id="alice@example.com")
1460
+ ╭──────────────── Memories ─────────────────╮
1461
+ │ Memories: │
1462
+ │ [dim][a1b2c3d4][/dim] Loves Python │
1463
+ │ [dim][e5f6g7h8][/dim] Works at Anthropic│
1464
+ ╰─────────────── alice@example.com ─────────╯
1465
+ """
1466
+ from agno.learn.utils import print_panel
1467
+
1468
+ memories_data = self.get(user_id=user_id)
1469
+
1470
+ lines = []
1471
+
1472
+ if memories_data:
1473
+ if hasattr(memories_data, "memories") and memories_data.memories:
1474
+ lines.append("Memories:")
1475
+ for mem in memories_data.memories:
1476
+ if isinstance(mem, dict):
1477
+ mem_id = mem.get("id", "?")
1478
+ content = mem.get("content", str(mem))
1479
+ else:
1480
+ mem_id = "?"
1481
+ content = str(mem)
1482
+ lines.append(f" [dim]\\[{mem_id}][/dim] {content}")
1483
+
1484
+ print_panel(
1485
+ title="Memories",
1486
+ subtitle=user_id,
1487
+ lines=lines,
1488
+ empty_message="No memories",
1489
+ raw_data=memories_data,
1490
+ raw=raw,
1491
+ )
1492
+
1493
+
1494
+ # Backwards compatibility alias
1495
+ MemoriesStore = UserMemoryStore