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.
- agno/agent/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.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
|