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,1583 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Learned Knowledge Store
|
|
3
|
+
=======================
|
|
4
|
+
Storage backend for Learned Knowledge learning type.
|
|
5
|
+
|
|
6
|
+
Stores reusable insights that apply across users and agents.
|
|
7
|
+
Think of it as:
|
|
8
|
+
- UserProfile = what you know about a person
|
|
9
|
+
- SessionContext = what happened in this meeting
|
|
10
|
+
- LearnedKnowledge = reusable insights that apply anywhere
|
|
11
|
+
|
|
12
|
+
Key Features:
|
|
13
|
+
- TWO agent tools: save_learning and search_learnings
|
|
14
|
+
- Semantic search for relevant learnings
|
|
15
|
+
- Shared across all agents using the same knowledge base
|
|
16
|
+
- Supports namespace-based scoping for privacy/sharing control:
|
|
17
|
+
- namespace="user": Private per user (scoped by user_id)
|
|
18
|
+
- namespace="global": Shared with everyone (default)
|
|
19
|
+
- namespace="<custom>": Custom grouping (literal string, e.g., "engineering")
|
|
20
|
+
|
|
21
|
+
Supported Modes:
|
|
22
|
+
- AGENTIC: Agent calls save_learning directly when it discovers insights
|
|
23
|
+
- PROPOSE: Agent proposes learnings, user approves before saving
|
|
24
|
+
- ALWAYS: Automatic extraction with duplicate detection
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from copy import deepcopy
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from os import getenv
|
|
31
|
+
from textwrap import dedent
|
|
32
|
+
from typing import Any, Callable, List, Optional
|
|
33
|
+
|
|
34
|
+
from agno.learn.config import LearnedKnowledgeConfig, LearningMode
|
|
35
|
+
from agno.learn.schemas import LearnedKnowledge
|
|
36
|
+
from agno.learn.stores.protocol import LearningStore
|
|
37
|
+
from agno.learn.utils import to_dict_safe
|
|
38
|
+
from agno.utils.log import (
|
|
39
|
+
log_debug,
|
|
40
|
+
log_warning,
|
|
41
|
+
set_log_level_to_debug,
|
|
42
|
+
set_log_level_to_info,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class LearnedKnowledgeStore(LearningStore):
|
|
48
|
+
"""Storage backend for Learned Knowledge learning type.
|
|
49
|
+
|
|
50
|
+
Uses a Knowledge base with vector embeddings for semantic search.
|
|
51
|
+
Supports namespace-based scoping for privacy/sharing control.
|
|
52
|
+
|
|
53
|
+
Namespace Scoping:
|
|
54
|
+
- namespace="global": Shared with everyone (default)
|
|
55
|
+
- namespace="user": Private per user (requires user_id)
|
|
56
|
+
- namespace="<custom>": Custom grouping (e.g., "engineering", "sales")
|
|
57
|
+
|
|
58
|
+
Provides TWO tools to the agent (when enable_agent_tools=True):
|
|
59
|
+
1. search_learnings - Find relevant learnings via semantic search
|
|
60
|
+
2. save_learning - Save reusable insights
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
config: LearnedKnowledgeConfig with all settings including knowledge base.
|
|
64
|
+
debug_mode: Enable debug logging.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
config: LearnedKnowledgeConfig = field(default_factory=LearnedKnowledgeConfig)
|
|
68
|
+
debug_mode: bool = False
|
|
69
|
+
|
|
70
|
+
# State tracking (internal)
|
|
71
|
+
learning_saved: bool = field(default=False, init=False)
|
|
72
|
+
_schema: Any = field(default=None, init=False)
|
|
73
|
+
|
|
74
|
+
def __post_init__(self):
|
|
75
|
+
self._schema = self.config.schema or LearnedKnowledge
|
|
76
|
+
|
|
77
|
+
if self.config.mode == LearningMode.HITL:
|
|
78
|
+
log_warning(
|
|
79
|
+
"LearnedKnowledgeStore does not support HITL mode. Use PROPOSE mode for human-in-the-loop approval. "
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# =========================================================================
|
|
83
|
+
# LearningStore Protocol Implementation
|
|
84
|
+
# =========================================================================
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def learning_type(self) -> str:
|
|
88
|
+
"""Unique identifier for this learning type."""
|
|
89
|
+
return "learned_knowledge"
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def schema(self) -> Any:
|
|
93
|
+
"""Schema class used for learnings."""
|
|
94
|
+
return self._schema
|
|
95
|
+
|
|
96
|
+
def recall(
|
|
97
|
+
self,
|
|
98
|
+
query: Optional[str] = None,
|
|
99
|
+
message: Optional[str] = None,
|
|
100
|
+
user_id: Optional[str] = None,
|
|
101
|
+
namespace: Optional[str] = None,
|
|
102
|
+
limit: int = 5,
|
|
103
|
+
**kwargs,
|
|
104
|
+
) -> Optional[List[Any]]:
|
|
105
|
+
"""Retrieve relevant learnings via semantic search.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
query: Search query (searches title, learning, context).
|
|
109
|
+
message: Current user message to find relevant learnings for (alternative).
|
|
110
|
+
user_id: User ID for "user" namespace scoping.
|
|
111
|
+
namespace: Filter by namespace (None = all accessible).
|
|
112
|
+
limit: Maximum number of results.
|
|
113
|
+
**kwargs: Additional context (ignored).
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
List of relevant learnings, or None if no query.
|
|
117
|
+
"""
|
|
118
|
+
search_query = query or message
|
|
119
|
+
if not search_query:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
effective_namespace = namespace or self.config.namespace
|
|
123
|
+
if effective_namespace == "user" and not user_id:
|
|
124
|
+
log_warning("LearnedKnowledgeStore.recall: namespace='user' requires user_id")
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
results = self.search(
|
|
128
|
+
query=search_query,
|
|
129
|
+
user_id=user_id,
|
|
130
|
+
namespace=effective_namespace,
|
|
131
|
+
limit=limit,
|
|
132
|
+
)
|
|
133
|
+
return results if results else None
|
|
134
|
+
|
|
135
|
+
async def arecall(
|
|
136
|
+
self,
|
|
137
|
+
query: Optional[str] = None,
|
|
138
|
+
message: Optional[str] = None,
|
|
139
|
+
user_id: Optional[str] = None,
|
|
140
|
+
namespace: Optional[str] = None,
|
|
141
|
+
limit: int = 5,
|
|
142
|
+
**kwargs,
|
|
143
|
+
) -> Optional[List[Any]]:
|
|
144
|
+
"""Async version of recall."""
|
|
145
|
+
search_query = query or message
|
|
146
|
+
if not search_query:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
effective_namespace = namespace or self.config.namespace
|
|
150
|
+
if effective_namespace == "user" and not user_id:
|
|
151
|
+
log_warning("LearnedKnowledgeStore.arecall: namespace='user' requires user_id")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
results = await self.asearch(
|
|
155
|
+
query=search_query,
|
|
156
|
+
user_id=user_id,
|
|
157
|
+
namespace=effective_namespace,
|
|
158
|
+
limit=limit,
|
|
159
|
+
)
|
|
160
|
+
return results if results else None
|
|
161
|
+
|
|
162
|
+
def process(
|
|
163
|
+
self,
|
|
164
|
+
messages: List[Any],
|
|
165
|
+
user_id: Optional[str] = None,
|
|
166
|
+
agent_id: Optional[str] = None,
|
|
167
|
+
team_id: Optional[str] = None,
|
|
168
|
+
namespace: Optional[str] = None,
|
|
169
|
+
**kwargs,
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Extract learned knowledge from messages.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
messages: Conversation messages to analyze.
|
|
175
|
+
user_id: User context (for "user" namespace scoping).
|
|
176
|
+
agent_id: Agent context (stored for audit).
|
|
177
|
+
team_id: Team context (stored for audit).
|
|
178
|
+
namespace: Namespace to save learnings to (default: "global").
|
|
179
|
+
**kwargs: Additional context (ignored).
|
|
180
|
+
"""
|
|
181
|
+
# process only supported in ALWAYS mode
|
|
182
|
+
# for programmatic extraction, use extract_and_save directly
|
|
183
|
+
if self.config.mode != LearningMode.ALWAYS:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
if not messages:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
self.extract_and_save(
|
|
190
|
+
messages=messages,
|
|
191
|
+
user_id=user_id,
|
|
192
|
+
agent_id=agent_id,
|
|
193
|
+
team_id=team_id,
|
|
194
|
+
namespace=namespace,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def aprocess(
|
|
198
|
+
self,
|
|
199
|
+
messages: List[Any],
|
|
200
|
+
user_id: Optional[str] = None,
|
|
201
|
+
agent_id: Optional[str] = None,
|
|
202
|
+
team_id: Optional[str] = None,
|
|
203
|
+
namespace: Optional[str] = None,
|
|
204
|
+
**kwargs,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Async version of process."""
|
|
207
|
+
if self.config.mode != LearningMode.ALWAYS:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if not messages:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
await self.aextract_and_save(
|
|
214
|
+
messages=messages,
|
|
215
|
+
user_id=user_id,
|
|
216
|
+
agent_id=agent_id,
|
|
217
|
+
team_id=team_id,
|
|
218
|
+
namespace=namespace,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def build_context(self, data: Any) -> str:
|
|
222
|
+
"""Build context for the agent.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
data: List of learning objects from recall() (may be None).
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Context string to inject into the agent's system prompt.
|
|
229
|
+
"""
|
|
230
|
+
mode = self.config.mode
|
|
231
|
+
|
|
232
|
+
if mode == LearningMode.PROPOSE:
|
|
233
|
+
return self._build_propose_mode_context(data=data)
|
|
234
|
+
elif mode == LearningMode.AGENTIC:
|
|
235
|
+
return self._build_agentic_mode_context(data=data)
|
|
236
|
+
else:
|
|
237
|
+
return self._build_background_mode_context(data=data)
|
|
238
|
+
|
|
239
|
+
def _build_agentic_mode_context(self, data: Any) -> str:
|
|
240
|
+
"""Build context for AGENTIC mode."""
|
|
241
|
+
instructions = dedent("""\
|
|
242
|
+
<learning_system>
|
|
243
|
+
You have a knowledge base of reusable learnings from past interactions.
|
|
244
|
+
|
|
245
|
+
## CRITICAL RULES - ALWAYS FOLLOW
|
|
246
|
+
|
|
247
|
+
**RULE 1: ALWAYS search before answering substantive questions.**
|
|
248
|
+
When the user asks for advice, recommendations, how-to guidance, or best practices:
|
|
249
|
+
→ First call `search_learnings` with relevant keywords
|
|
250
|
+
→ Then incorporate any relevant findings into your response
|
|
251
|
+
|
|
252
|
+
**RULE 2: ALWAYS search before saving.**
|
|
253
|
+
When asked to save a learning or when you want to save an insight:
|
|
254
|
+
→ First call `search_learnings` to check if similar knowledge exists
|
|
255
|
+
→ Only save if it's genuinely new (not a duplicate or minor variation)
|
|
256
|
+
|
|
257
|
+
## Tools
|
|
258
|
+
|
|
259
|
+
`search_learnings(query)` - Search for relevant prior insights. Use liberally.
|
|
260
|
+
`save_learning(title, learning, context, tags)` - Save genuinely new insights.
|
|
261
|
+
|
|
262
|
+
## When to Search
|
|
263
|
+
|
|
264
|
+
ALWAYS search when the user:
|
|
265
|
+
- Asks for recommendations or best practices
|
|
266
|
+
- Asks how to approach a problem
|
|
267
|
+
- Asks about trade-offs or considerations
|
|
268
|
+
- Mentions a technology, domain, or problem area
|
|
269
|
+
- Asks you to save something (search first to check for duplicates!)
|
|
270
|
+
|
|
271
|
+
## When to Save
|
|
272
|
+
|
|
273
|
+
Only save insights that are:
|
|
274
|
+
- Non-obvious (required investigation to discover)
|
|
275
|
+
- Reusable (applies to a category of problems)
|
|
276
|
+
- Actionable (specific enough to apply directly)
|
|
277
|
+
- Not already in the knowledge base (you checked by searching first!)
|
|
278
|
+
|
|
279
|
+
Do NOT save:
|
|
280
|
+
- Raw facts or common knowledge
|
|
281
|
+
- User-specific preferences (use user memory instead)
|
|
282
|
+
- Duplicates of existing learnings
|
|
283
|
+
</learning_system>\
|
|
284
|
+
""")
|
|
285
|
+
|
|
286
|
+
if data:
|
|
287
|
+
learnings = data if isinstance(data, list) else [data]
|
|
288
|
+
if learnings:
|
|
289
|
+
formatted = self._format_learnings_for_context(learnings=learnings)
|
|
290
|
+
instructions += f"\n\n<relevant_learnings>\nPrior insights that may help with this task:\n\n{formatted}\n\nApply these naturally if relevant. Current context takes precedence.\n</relevant_learnings>"
|
|
291
|
+
|
|
292
|
+
return instructions
|
|
293
|
+
|
|
294
|
+
def _build_propose_mode_context(self, data: Any) -> str:
|
|
295
|
+
"""Build context for PROPOSE mode."""
|
|
296
|
+
instructions = dedent("""\
|
|
297
|
+
<learning_system>
|
|
298
|
+
You have a knowledge base of reusable learnings. In PROPOSE mode, saving requires user approval.
|
|
299
|
+
|
|
300
|
+
## CRITICAL RULES - ALWAYS FOLLOW
|
|
301
|
+
|
|
302
|
+
**RULE 1: ALWAYS search before answering substantive questions.**
|
|
303
|
+
When the user asks for advice, recommendations, how-to guidance, or best practices:
|
|
304
|
+
→ First call `search_learnings` with relevant keywords
|
|
305
|
+
→ Then incorporate any relevant findings into your response
|
|
306
|
+
|
|
307
|
+
**RULE 2: Propose learnings, don't save directly.**
|
|
308
|
+
If you discover something worth preserving, propose it at the end of your response:
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
**💡 Proposed Learning**
|
|
312
|
+
**Title:** [Concise title]
|
|
313
|
+
**Context:** [When this applies]
|
|
314
|
+
**Insight:** [The learning - specific and actionable]
|
|
315
|
+
|
|
316
|
+
Save this to the knowledge base? (yes/no)
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
**RULE 3: Only save after explicit approval.**
|
|
320
|
+
Call `save_learning` ONLY after the user says "yes" to your proposal.
|
|
321
|
+
Before saving, search first to check for duplicates.
|
|
322
|
+
|
|
323
|
+
## Tools
|
|
324
|
+
|
|
325
|
+
`search_learnings(query)` - Search for relevant prior insights. Use liberally.
|
|
326
|
+
`save_learning(title, learning, context, tags)` - Save ONLY after user approval.
|
|
327
|
+
|
|
328
|
+
## What to Propose
|
|
329
|
+
|
|
330
|
+
Only propose insights that are:
|
|
331
|
+
- Non-obvious (required investigation to discover)
|
|
332
|
+
- Reusable (applies to a category of problems)
|
|
333
|
+
- Actionable (specific enough to apply directly)
|
|
334
|
+
|
|
335
|
+
Do NOT propose:
|
|
336
|
+
- Raw facts or common knowledge
|
|
337
|
+
- User-specific preferences
|
|
338
|
+
- Things the user already knew
|
|
339
|
+
</learning_system>\
|
|
340
|
+
""")
|
|
341
|
+
|
|
342
|
+
if data:
|
|
343
|
+
learnings = data if isinstance(data, list) else [data]
|
|
344
|
+
if learnings:
|
|
345
|
+
formatted = self._format_learnings_for_context(learnings=learnings)
|
|
346
|
+
instructions += f"\n\n<relevant_learnings>\nPrior insights that may help:\n\n{formatted}\n\nApply these naturally if relevant.\n</relevant_learnings>"
|
|
347
|
+
|
|
348
|
+
return instructions
|
|
349
|
+
|
|
350
|
+
def _build_background_mode_context(self, data: Any) -> str:
|
|
351
|
+
"""Build context for ALWAYS mode (just show relevant learnings)."""
|
|
352
|
+
if not data:
|
|
353
|
+
return ""
|
|
354
|
+
|
|
355
|
+
learnings = data if isinstance(data, list) else [data]
|
|
356
|
+
if not learnings:
|
|
357
|
+
return ""
|
|
358
|
+
|
|
359
|
+
formatted = self._format_learnings_for_context(learnings=learnings)
|
|
360
|
+
return dedent(f"""\
|
|
361
|
+
<relevant_learnings>
|
|
362
|
+
Prior insights that may help with this task:
|
|
363
|
+
|
|
364
|
+
{formatted}
|
|
365
|
+
|
|
366
|
+
Apply these naturally if they're relevant to the current request.
|
|
367
|
+
Your current analysis and the user's specific context take precedence.
|
|
368
|
+
</relevant_learnings>\
|
|
369
|
+
""")
|
|
370
|
+
|
|
371
|
+
def _format_learnings_for_context(self, learnings: List[Any]) -> str:
|
|
372
|
+
"""Format learnings for inclusion in context."""
|
|
373
|
+
parts = []
|
|
374
|
+
for i, learning in enumerate(learnings, 1):
|
|
375
|
+
formatted = self._format_single_learning(learning=learning)
|
|
376
|
+
if formatted:
|
|
377
|
+
parts.append(f"{i}. {formatted}")
|
|
378
|
+
return "\n".join(parts)
|
|
379
|
+
|
|
380
|
+
def get_tools(
|
|
381
|
+
self,
|
|
382
|
+
user_id: Optional[str] = None,
|
|
383
|
+
agent_id: Optional[str] = None,
|
|
384
|
+
team_id: Optional[str] = None,
|
|
385
|
+
namespace: Optional[str] = None,
|
|
386
|
+
**kwargs,
|
|
387
|
+
) -> List[Callable]:
|
|
388
|
+
"""Get tools to expose to agent.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
user_id: User context (for "user" namespace scoping).
|
|
392
|
+
agent_id: Agent context (stored for audit on saves).
|
|
393
|
+
team_id: Team context (stored for audit on saves).
|
|
394
|
+
namespace: Default namespace for saves (default: "global").
|
|
395
|
+
**kwargs: Additional context (ignored).
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
List of callable tools (empty if enable_agent_tools=False).
|
|
399
|
+
"""
|
|
400
|
+
if not self.config.enable_agent_tools:
|
|
401
|
+
return []
|
|
402
|
+
return self.get_agent_tools(
|
|
403
|
+
user_id=user_id,
|
|
404
|
+
agent_id=agent_id,
|
|
405
|
+
team_id=team_id,
|
|
406
|
+
namespace=namespace,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
async def aget_tools(
|
|
410
|
+
self,
|
|
411
|
+
user_id: Optional[str] = None,
|
|
412
|
+
agent_id: Optional[str] = None,
|
|
413
|
+
team_id: Optional[str] = None,
|
|
414
|
+
namespace: Optional[str] = None,
|
|
415
|
+
**kwargs,
|
|
416
|
+
) -> List[Callable]:
|
|
417
|
+
"""Async version of get_tools."""
|
|
418
|
+
if not self.config.enable_agent_tools:
|
|
419
|
+
return []
|
|
420
|
+
return await self.aget_agent_tools(
|
|
421
|
+
user_id=user_id,
|
|
422
|
+
agent_id=agent_id,
|
|
423
|
+
team_id=team_id,
|
|
424
|
+
namespace=namespace,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
@property
|
|
428
|
+
def was_updated(self) -> bool:
|
|
429
|
+
"""Check if a learning was saved in last operation."""
|
|
430
|
+
return self.learning_saved
|
|
431
|
+
|
|
432
|
+
# =========================================================================
|
|
433
|
+
# Properties
|
|
434
|
+
# =========================================================================
|
|
435
|
+
|
|
436
|
+
@property
|
|
437
|
+
def knowledge(self):
|
|
438
|
+
"""The knowledge base (vector store)."""
|
|
439
|
+
return self.config.knowledge
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def model(self):
|
|
443
|
+
"""Model for extraction (if needed)."""
|
|
444
|
+
return self.config.model
|
|
445
|
+
|
|
446
|
+
# =========================================================================
|
|
447
|
+
# Debug/Logging
|
|
448
|
+
# =========================================================================
|
|
449
|
+
|
|
450
|
+
def set_log_level(self):
|
|
451
|
+
"""Set log level based on debug_mode or environment variable."""
|
|
452
|
+
if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true":
|
|
453
|
+
self.debug_mode = True
|
|
454
|
+
set_log_level_to_debug()
|
|
455
|
+
else:
|
|
456
|
+
set_log_level_to_info()
|
|
457
|
+
|
|
458
|
+
# =========================================================================
|
|
459
|
+
# Agent Tools
|
|
460
|
+
# =========================================================================
|
|
461
|
+
|
|
462
|
+
def get_agent_tools(
|
|
463
|
+
self,
|
|
464
|
+
user_id: Optional[str] = None,
|
|
465
|
+
agent_id: Optional[str] = None,
|
|
466
|
+
team_id: Optional[str] = None,
|
|
467
|
+
namespace: Optional[str] = None,
|
|
468
|
+
) -> List[Callable]:
|
|
469
|
+
"""Get the tools to expose to the agent.
|
|
470
|
+
|
|
471
|
+
Returns TWO tools (based on config settings):
|
|
472
|
+
1. search_learnings - Find relevant learnings
|
|
473
|
+
2. save_learning - Save reusable insights
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
user_id: User context (for "user" namespace scoping).
|
|
477
|
+
agent_id: Agent context (stored for audit on saves).
|
|
478
|
+
team_id: Team context (stored for audit on saves).
|
|
479
|
+
namespace: Default namespace for saves (default: "global").
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
List of callable tools.
|
|
483
|
+
"""
|
|
484
|
+
tools = []
|
|
485
|
+
|
|
486
|
+
if self.config.agent_can_search:
|
|
487
|
+
tools.append(self._create_search_learnings_tool(user_id=user_id))
|
|
488
|
+
|
|
489
|
+
if self.config.agent_can_save:
|
|
490
|
+
tools.append(
|
|
491
|
+
self._create_save_learning_tool(
|
|
492
|
+
user_id=user_id,
|
|
493
|
+
agent_id=agent_id,
|
|
494
|
+
team_id=team_id,
|
|
495
|
+
default_namespace=namespace,
|
|
496
|
+
)
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return tools
|
|
500
|
+
|
|
501
|
+
async def aget_agent_tools(
|
|
502
|
+
self,
|
|
503
|
+
user_id: Optional[str] = None,
|
|
504
|
+
agent_id: Optional[str] = None,
|
|
505
|
+
team_id: Optional[str] = None,
|
|
506
|
+
namespace: Optional[str] = None,
|
|
507
|
+
) -> List[Callable]:
|
|
508
|
+
"""Async version of get_agent_tools."""
|
|
509
|
+
tools = []
|
|
510
|
+
|
|
511
|
+
if self.config.agent_can_search:
|
|
512
|
+
tools.append(self._create_async_search_learnings_tool(user_id=user_id))
|
|
513
|
+
|
|
514
|
+
if self.config.agent_can_save:
|
|
515
|
+
tools.append(
|
|
516
|
+
self._create_async_save_learning_tool(
|
|
517
|
+
user_id=user_id,
|
|
518
|
+
agent_id=agent_id,
|
|
519
|
+
team_id=team_id,
|
|
520
|
+
default_namespace=namespace,
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
return tools
|
|
525
|
+
|
|
526
|
+
# =========================================================================
|
|
527
|
+
# Tool: save_learning
|
|
528
|
+
# =========================================================================
|
|
529
|
+
|
|
530
|
+
def _create_save_learning_tool(
|
|
531
|
+
self,
|
|
532
|
+
user_id: Optional[str] = None,
|
|
533
|
+
agent_id: Optional[str] = None,
|
|
534
|
+
team_id: Optional[str] = None,
|
|
535
|
+
default_namespace: Optional[str] = None,
|
|
536
|
+
) -> Callable:
|
|
537
|
+
"""Create the save_learning tool for the agent."""
|
|
538
|
+
|
|
539
|
+
def save_learning(
|
|
540
|
+
title: str,
|
|
541
|
+
learning: str,
|
|
542
|
+
context: Optional[str] = None,
|
|
543
|
+
tags: Optional[List[str]] = None,
|
|
544
|
+
namespace: Optional[str] = None,
|
|
545
|
+
) -> str:
|
|
546
|
+
"""Save a reusable insight to the knowledge base.
|
|
547
|
+
|
|
548
|
+
IMPORTANT: Before calling this, you MUST first call search_learnings to check
|
|
549
|
+
if similar knowledge already exists. Do not save duplicates!
|
|
550
|
+
|
|
551
|
+
Only save insights that are:
|
|
552
|
+
- Non-obvious (not common knowledge)
|
|
553
|
+
- Reusable (applies beyond this specific case)
|
|
554
|
+
- Actionable (specific enough to apply directly)
|
|
555
|
+
- Not already saved (you searched first, right?)
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
title: Concise, searchable title (e.g., "Cloud egress cost variations").
|
|
559
|
+
learning: The insight - specific and actionable.
|
|
560
|
+
context: When/where this applies (e.g., "When selecting cloud providers").
|
|
561
|
+
tags: Categories for organization (e.g., ["cloud", "costs"]).
|
|
562
|
+
namespace: Access scope - "global" (shared) or "user" (private).
|
|
563
|
+
|
|
564
|
+
Returns:
|
|
565
|
+
Confirmation message.
|
|
566
|
+
"""
|
|
567
|
+
effective_namespace = namespace or default_namespace or "global"
|
|
568
|
+
|
|
569
|
+
success = self.save(
|
|
570
|
+
title=title,
|
|
571
|
+
learning=learning,
|
|
572
|
+
context=context,
|
|
573
|
+
tags=tags,
|
|
574
|
+
user_id=user_id,
|
|
575
|
+
agent_id=agent_id,
|
|
576
|
+
team_id=team_id,
|
|
577
|
+
namespace=effective_namespace,
|
|
578
|
+
)
|
|
579
|
+
if success:
|
|
580
|
+
self.learning_saved = True
|
|
581
|
+
return f"Learning saved: {title} (namespace: {effective_namespace})"
|
|
582
|
+
return "Failed to save learning"
|
|
583
|
+
|
|
584
|
+
return save_learning
|
|
585
|
+
|
|
586
|
+
def _create_async_save_learning_tool(
|
|
587
|
+
self,
|
|
588
|
+
user_id: Optional[str] = None,
|
|
589
|
+
agent_id: Optional[str] = None,
|
|
590
|
+
team_id: Optional[str] = None,
|
|
591
|
+
default_namespace: Optional[str] = None,
|
|
592
|
+
) -> Callable:
|
|
593
|
+
"""Create the async save_learning tool for the agent."""
|
|
594
|
+
|
|
595
|
+
async def save_learning(
|
|
596
|
+
title: str,
|
|
597
|
+
learning: str,
|
|
598
|
+
context: Optional[str] = None,
|
|
599
|
+
tags: Optional[List[str]] = None,
|
|
600
|
+
namespace: Optional[str] = None,
|
|
601
|
+
) -> str:
|
|
602
|
+
"""Save a reusable insight to the knowledge base.
|
|
603
|
+
|
|
604
|
+
IMPORTANT: Before calling this, you MUST first call search_learnings to check
|
|
605
|
+
if similar knowledge already exists. Do not save duplicates!
|
|
606
|
+
|
|
607
|
+
Only save insights that are:
|
|
608
|
+
- Non-obvious (not common knowledge)
|
|
609
|
+
- Reusable (applies beyond this specific case)
|
|
610
|
+
- Actionable (specific enough to apply directly)
|
|
611
|
+
- Not already saved (you searched first, right?)
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
title: Concise, searchable title (e.g., "Cloud egress cost variations").
|
|
615
|
+
learning: The insight - specific and actionable.
|
|
616
|
+
context: When/where this applies (e.g., "When selecting cloud providers").
|
|
617
|
+
tags: Categories for organization (e.g., ["cloud", "costs"]).
|
|
618
|
+
namespace: Access scope - "global" (shared) or "user" (private).
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
Confirmation message.
|
|
622
|
+
"""
|
|
623
|
+
effective_namespace = namespace or default_namespace or "global"
|
|
624
|
+
|
|
625
|
+
success = await self.asave(
|
|
626
|
+
title=title,
|
|
627
|
+
learning=learning,
|
|
628
|
+
context=context,
|
|
629
|
+
tags=tags,
|
|
630
|
+
user_id=user_id,
|
|
631
|
+
agent_id=agent_id,
|
|
632
|
+
team_id=team_id,
|
|
633
|
+
namespace=effective_namespace,
|
|
634
|
+
)
|
|
635
|
+
if success:
|
|
636
|
+
self.learning_saved = True
|
|
637
|
+
return f"Learning saved: {title} (namespace: {effective_namespace})"
|
|
638
|
+
return "Failed to save learning"
|
|
639
|
+
|
|
640
|
+
return save_learning
|
|
641
|
+
|
|
642
|
+
# =========================================================================
|
|
643
|
+
# Tool: search_learnings
|
|
644
|
+
# =========================================================================
|
|
645
|
+
|
|
646
|
+
def _create_search_learnings_tool(
|
|
647
|
+
self,
|
|
648
|
+
user_id: Optional[str] = None,
|
|
649
|
+
) -> Callable:
|
|
650
|
+
"""Create the search_learnings tool for the agent."""
|
|
651
|
+
|
|
652
|
+
def search_learnings(
|
|
653
|
+
query: str,
|
|
654
|
+
limit: int = 5,
|
|
655
|
+
namespace: Optional[str] = None,
|
|
656
|
+
) -> str:
|
|
657
|
+
"""Search for relevant insights in the knowledge base.
|
|
658
|
+
|
|
659
|
+
ALWAYS call this:
|
|
660
|
+
1. Before answering questions about best practices, recommendations, or how-to
|
|
661
|
+
2. Before saving a new learning (to check for duplicates)
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
query: Keywords describing what you're looking for.
|
|
665
|
+
Examples: "cloud costs", "API rate limiting", "database migration"
|
|
666
|
+
limit: Maximum results (default: 5)
|
|
667
|
+
namespace: Filter by scope (None = all, "global", "user", or custom)
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
List of relevant learnings, or message if none found.
|
|
671
|
+
"""
|
|
672
|
+
results = self.search(
|
|
673
|
+
query=query,
|
|
674
|
+
user_id=user_id,
|
|
675
|
+
namespace=namespace,
|
|
676
|
+
limit=limit,
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if not results:
|
|
680
|
+
return "No relevant learnings found."
|
|
681
|
+
|
|
682
|
+
formatted = self._format_learnings_list(learnings=results)
|
|
683
|
+
return f"Found {len(results)} relevant learning(s):\n\n{formatted}"
|
|
684
|
+
|
|
685
|
+
return search_learnings
|
|
686
|
+
|
|
687
|
+
def _create_async_search_learnings_tool(
|
|
688
|
+
self,
|
|
689
|
+
user_id: Optional[str] = None,
|
|
690
|
+
) -> Callable:
|
|
691
|
+
"""Create the async search_learnings tool for the agent."""
|
|
692
|
+
|
|
693
|
+
async def search_learnings(
|
|
694
|
+
query: str,
|
|
695
|
+
limit: int = 5,
|
|
696
|
+
namespace: Optional[str] = None,
|
|
697
|
+
) -> str:
|
|
698
|
+
"""Search for relevant insights in the knowledge base.
|
|
699
|
+
|
|
700
|
+
ALWAYS call this:
|
|
701
|
+
1. Before answering questions about best practices, recommendations, or how-to
|
|
702
|
+
2. Before saving a new learning (to check for duplicates)
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
query: Keywords describing what you're looking for.
|
|
706
|
+
Examples: "cloud costs", "API rate limiting", "database migration"
|
|
707
|
+
limit: Maximum results (default: 5)
|
|
708
|
+
namespace: Filter by scope (None = all, "global", "user", or custom)
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
List of relevant learnings, or message if none found.
|
|
712
|
+
"""
|
|
713
|
+
results = await self.asearch(
|
|
714
|
+
query=query,
|
|
715
|
+
user_id=user_id,
|
|
716
|
+
namespace=namespace,
|
|
717
|
+
limit=limit,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
if not results:
|
|
721
|
+
return "No relevant learnings found."
|
|
722
|
+
|
|
723
|
+
formatted = self._format_learnings_list(learnings=results)
|
|
724
|
+
return f"Found {len(results)} relevant learning(s):\n\n{formatted}"
|
|
725
|
+
|
|
726
|
+
return search_learnings
|
|
727
|
+
|
|
728
|
+
# =========================================================================
|
|
729
|
+
# Search Operations
|
|
730
|
+
# =========================================================================
|
|
731
|
+
|
|
732
|
+
def search(
|
|
733
|
+
self,
|
|
734
|
+
query: str,
|
|
735
|
+
user_id: Optional[str] = None,
|
|
736
|
+
namespace: Optional[str] = None,
|
|
737
|
+
limit: int = 5,
|
|
738
|
+
) -> List[Any]:
|
|
739
|
+
"""Search for relevant learnings based on query.
|
|
740
|
+
|
|
741
|
+
Uses semantic search to find learnings most relevant to the query.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
query: The search query.
|
|
745
|
+
user_id: User ID for "user" namespace access.
|
|
746
|
+
namespace: Filter by namespace (None = all accessible).
|
|
747
|
+
limit: Maximum number of results to return.
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
List of learning objects matching the query.
|
|
751
|
+
"""
|
|
752
|
+
if not self.knowledge:
|
|
753
|
+
log_warning("LearnedKnowledgeStore.search: no knowledge base configured")
|
|
754
|
+
return []
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
# Build filters based on namespace
|
|
758
|
+
filters = self._build_search_filters(user_id=user_id, namespace=namespace)
|
|
759
|
+
|
|
760
|
+
# Search with filters if supported
|
|
761
|
+
if filters:
|
|
762
|
+
results = self.knowledge.search(query=query, max_results=limit, filters=filters)
|
|
763
|
+
else:
|
|
764
|
+
results = self.knowledge.search(query=query, max_results=limit)
|
|
765
|
+
|
|
766
|
+
learnings = []
|
|
767
|
+
for result in results or []:
|
|
768
|
+
learning = self._parse_result(result=result)
|
|
769
|
+
if learning:
|
|
770
|
+
# Post-filter by namespace if KB doesn't support filtering
|
|
771
|
+
if self._matches_namespace_filter(learning, user_id=user_id, namespace=namespace):
|
|
772
|
+
learnings.append(learning)
|
|
773
|
+
|
|
774
|
+
log_debug(f"LearnedKnowledgeStore.search: found {len(learnings)} learnings for query: {query[:50]}...")
|
|
775
|
+
return learnings[:limit]
|
|
776
|
+
|
|
777
|
+
except Exception as e:
|
|
778
|
+
log_warning(f"LearnedKnowledgeStore.search failed: {e}")
|
|
779
|
+
return []
|
|
780
|
+
|
|
781
|
+
async def asearch(
|
|
782
|
+
self,
|
|
783
|
+
query: str,
|
|
784
|
+
user_id: Optional[str] = None,
|
|
785
|
+
namespace: Optional[str] = None,
|
|
786
|
+
limit: int = 5,
|
|
787
|
+
) -> List[Any]:
|
|
788
|
+
"""Async version of search."""
|
|
789
|
+
if not self.knowledge:
|
|
790
|
+
log_warning("LearnedKnowledgeStore.asearch: no knowledge base configured")
|
|
791
|
+
return []
|
|
792
|
+
|
|
793
|
+
try:
|
|
794
|
+
# Build filters based on namespace
|
|
795
|
+
filters = self._build_search_filters(user_id=user_id, namespace=namespace)
|
|
796
|
+
|
|
797
|
+
# Search with filters if supported
|
|
798
|
+
if hasattr(self.knowledge, "asearch"):
|
|
799
|
+
if filters:
|
|
800
|
+
results = await self.knowledge.asearch(query=query, max_results=limit, filters=filters)
|
|
801
|
+
else:
|
|
802
|
+
results = await self.knowledge.asearch(query=query, max_results=limit)
|
|
803
|
+
else:
|
|
804
|
+
if filters:
|
|
805
|
+
results = self.knowledge.search(query=query, max_results=limit, filters=filters)
|
|
806
|
+
else:
|
|
807
|
+
results = self.knowledge.search(query=query, max_results=limit)
|
|
808
|
+
|
|
809
|
+
learnings = []
|
|
810
|
+
for result in results or []:
|
|
811
|
+
learning = self._parse_result(result=result)
|
|
812
|
+
if learning:
|
|
813
|
+
# Post-filter by namespace if KB doesn't support filtering
|
|
814
|
+
if self._matches_namespace_filter(learning, user_id=user_id, namespace=namespace):
|
|
815
|
+
learnings.append(learning)
|
|
816
|
+
|
|
817
|
+
log_debug(f"LearnedKnowledgeStore.asearch: found {len(learnings)} learnings for query: {query[:50]}...")
|
|
818
|
+
return learnings[:limit]
|
|
819
|
+
|
|
820
|
+
except Exception as e:
|
|
821
|
+
log_warning(f"LearnedKnowledgeStore.asearch failed: {e}")
|
|
822
|
+
return []
|
|
823
|
+
|
|
824
|
+
def _build_search_filters(
|
|
825
|
+
self,
|
|
826
|
+
user_id: Optional[str] = None,
|
|
827
|
+
namespace: Optional[str] = None,
|
|
828
|
+
) -> Optional[dict]:
|
|
829
|
+
"""Build search filters for namespace scoping.
|
|
830
|
+
|
|
831
|
+
Returns filter dict for knowledge base, or None if no filtering needed.
|
|
832
|
+
"""
|
|
833
|
+
if not namespace:
|
|
834
|
+
return None
|
|
835
|
+
|
|
836
|
+
if namespace == "user":
|
|
837
|
+
if not user_id:
|
|
838
|
+
log_warning("LearnedKnowledgeStore: 'user' namespace requires user_id")
|
|
839
|
+
return None
|
|
840
|
+
return {"namespace": "user", "user_id": user_id}
|
|
841
|
+
|
|
842
|
+
return {"namespace": namespace}
|
|
843
|
+
|
|
844
|
+
def _matches_namespace_filter(
|
|
845
|
+
self,
|
|
846
|
+
learning: Any,
|
|
847
|
+
user_id: Optional[str] = None,
|
|
848
|
+
namespace: Optional[str] = None,
|
|
849
|
+
) -> bool:
|
|
850
|
+
"""Check if a learning matches the namespace filter (for post-filtering)."""
|
|
851
|
+
if not namespace:
|
|
852
|
+
return True
|
|
853
|
+
|
|
854
|
+
learning_namespace = getattr(learning, "namespace", None) or "global"
|
|
855
|
+
learning_user_id = getattr(learning, "user_id", None)
|
|
856
|
+
|
|
857
|
+
if namespace == "user":
|
|
858
|
+
return learning_namespace == "user" and learning_user_id == user_id
|
|
859
|
+
|
|
860
|
+
return learning_namespace == namespace
|
|
861
|
+
|
|
862
|
+
# =========================================================================
|
|
863
|
+
# Save Operations
|
|
864
|
+
# =========================================================================
|
|
865
|
+
|
|
866
|
+
def save(
|
|
867
|
+
self,
|
|
868
|
+
title: str,
|
|
869
|
+
learning: str,
|
|
870
|
+
context: Optional[str] = None,
|
|
871
|
+
tags: Optional[List[str]] = None,
|
|
872
|
+
user_id: Optional[str] = None,
|
|
873
|
+
agent_id: Optional[str] = None,
|
|
874
|
+
team_id: Optional[str] = None,
|
|
875
|
+
namespace: Optional[str] = None,
|
|
876
|
+
) -> bool:
|
|
877
|
+
"""Save a learning to the knowledge base.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
title: Short descriptive title.
|
|
881
|
+
learning: The actual insight.
|
|
882
|
+
context: When/why this applies.
|
|
883
|
+
tags: Tags for categorization.
|
|
884
|
+
user_id: User ID (required for "user" namespace).
|
|
885
|
+
agent_id: Agent that created this (stored as metadata for audit).
|
|
886
|
+
team_id: Team context (stored as metadata for audit).
|
|
887
|
+
namespace: Namespace for scoping (default: "global").
|
|
888
|
+
|
|
889
|
+
Returns:
|
|
890
|
+
True if saved successfully, False otherwise.
|
|
891
|
+
"""
|
|
892
|
+
if not self.knowledge:
|
|
893
|
+
log_warning("LearnedKnowledgeStore.save: no knowledge base configured")
|
|
894
|
+
return False
|
|
895
|
+
|
|
896
|
+
effective_namespace = namespace or "global"
|
|
897
|
+
|
|
898
|
+
# Validate "user" namespace has user_id
|
|
899
|
+
if effective_namespace == "user" and not user_id:
|
|
900
|
+
log_warning("LearnedKnowledgeStore.save: 'user' namespace requires user_id")
|
|
901
|
+
return False
|
|
902
|
+
|
|
903
|
+
try:
|
|
904
|
+
from agno.knowledge.reader.text_reader import TextReader
|
|
905
|
+
|
|
906
|
+
learning_data = {
|
|
907
|
+
"title": title.strip(),
|
|
908
|
+
"learning": learning.strip(),
|
|
909
|
+
"context": context.strip() if context else None,
|
|
910
|
+
"tags": tags or [],
|
|
911
|
+
"namespace": effective_namespace,
|
|
912
|
+
"user_id": user_id if effective_namespace == "user" else None,
|
|
913
|
+
"agent_id": agent_id,
|
|
914
|
+
"team_id": team_id,
|
|
915
|
+
"created_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
learning_obj = self.schema(**learning_data)
|
|
919
|
+
text_content = self._to_text_content(learning=learning_obj)
|
|
920
|
+
|
|
921
|
+
# Build metadata for filtering
|
|
922
|
+
# Metadata must be passed separately to insert for filters to work
|
|
923
|
+
filter_metadata: dict[str, Any] = {
|
|
924
|
+
"namespace": effective_namespace,
|
|
925
|
+
}
|
|
926
|
+
if effective_namespace == "user" and user_id:
|
|
927
|
+
filter_metadata["user_id"] = user_id
|
|
928
|
+
if agent_id:
|
|
929
|
+
filter_metadata["agent_id"] = agent_id
|
|
930
|
+
if team_id:
|
|
931
|
+
filter_metadata["team_id"] = team_id
|
|
932
|
+
if tags:
|
|
933
|
+
filter_metadata["tags"] = tags
|
|
934
|
+
|
|
935
|
+
self.knowledge.insert(
|
|
936
|
+
name=learning_data["title"],
|
|
937
|
+
text_content=text_content,
|
|
938
|
+
reader=TextReader(),
|
|
939
|
+
skip_if_exists=True,
|
|
940
|
+
metadata=filter_metadata, # Pass metadata for filtering
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
log_debug(f"LearnedKnowledgeStore.save: saved learning '{title}' (namespace: {effective_namespace})")
|
|
944
|
+
return True
|
|
945
|
+
|
|
946
|
+
except Exception as e:
|
|
947
|
+
log_warning(f"LearnedKnowledgeStore.save failed: {e}")
|
|
948
|
+
return False
|
|
949
|
+
|
|
950
|
+
async def asave(
|
|
951
|
+
self,
|
|
952
|
+
title: str,
|
|
953
|
+
learning: str,
|
|
954
|
+
context: Optional[str] = None,
|
|
955
|
+
tags: Optional[List[str]] = None,
|
|
956
|
+
user_id: Optional[str] = None,
|
|
957
|
+
agent_id: Optional[str] = None,
|
|
958
|
+
team_id: Optional[str] = None,
|
|
959
|
+
namespace: Optional[str] = None,
|
|
960
|
+
) -> bool:
|
|
961
|
+
"""Async version of save."""
|
|
962
|
+
if not self.knowledge:
|
|
963
|
+
log_warning("LearnedKnowledgeStore.asave: no knowledge base configured")
|
|
964
|
+
return False
|
|
965
|
+
|
|
966
|
+
effective_namespace = namespace or "global"
|
|
967
|
+
|
|
968
|
+
# Validate "user" namespace has user_id
|
|
969
|
+
if effective_namespace == "user" and not user_id:
|
|
970
|
+
log_warning("LearnedKnowledgeStore.asave: 'user' namespace requires user_id")
|
|
971
|
+
return False
|
|
972
|
+
|
|
973
|
+
try:
|
|
974
|
+
from agno.knowledge.reader.text_reader import TextReader
|
|
975
|
+
|
|
976
|
+
learning_data = {
|
|
977
|
+
"title": title.strip(),
|
|
978
|
+
"learning": learning.strip(),
|
|
979
|
+
"context": context.strip() if context else None,
|
|
980
|
+
"tags": tags or [],
|
|
981
|
+
"namespace": effective_namespace,
|
|
982
|
+
"user_id": user_id if effective_namespace == "user" else None,
|
|
983
|
+
"agent_id": agent_id,
|
|
984
|
+
"team_id": team_id,
|
|
985
|
+
"created_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
learning_obj = self.schema(**learning_data)
|
|
989
|
+
text_content = self._to_text_content(learning=learning_obj)
|
|
990
|
+
|
|
991
|
+
# Build metadata for filtering - THIS IS THE KEY FIX!
|
|
992
|
+
# Metadata must be passed separately to insert for filters to work
|
|
993
|
+
filter_metadata: dict[str, Any] = {
|
|
994
|
+
"namespace": effective_namespace,
|
|
995
|
+
}
|
|
996
|
+
if effective_namespace == "user" and user_id:
|
|
997
|
+
filter_metadata["user_id"] = user_id
|
|
998
|
+
if agent_id:
|
|
999
|
+
filter_metadata["agent_id"] = agent_id
|
|
1000
|
+
if team_id:
|
|
1001
|
+
filter_metadata["team_id"] = team_id
|
|
1002
|
+
if tags:
|
|
1003
|
+
filter_metadata["tags"] = tags
|
|
1004
|
+
|
|
1005
|
+
if hasattr(self.knowledge, "ainsert"):
|
|
1006
|
+
await self.knowledge.ainsert(
|
|
1007
|
+
name=learning_data["title"],
|
|
1008
|
+
text_content=text_content,
|
|
1009
|
+
reader=TextReader(),
|
|
1010
|
+
skip_if_exists=True,
|
|
1011
|
+
metadata=filter_metadata, # Pass metadata for filtering
|
|
1012
|
+
)
|
|
1013
|
+
else:
|
|
1014
|
+
self.knowledge.insert(
|
|
1015
|
+
name=learning_data["title"],
|
|
1016
|
+
text_content=text_content,
|
|
1017
|
+
reader=TextReader(),
|
|
1018
|
+
skip_if_exists=True,
|
|
1019
|
+
metadata=filter_metadata, # Pass metadata for filtering
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
log_debug(f"LearnedKnowledgeStore.asave: saved learning '{title}' (namespace: {effective_namespace})")
|
|
1023
|
+
return True
|
|
1024
|
+
|
|
1025
|
+
except Exception as e:
|
|
1026
|
+
log_warning(f"LearnedKnowledgeStore.asave failed: {e}")
|
|
1027
|
+
return False
|
|
1028
|
+
|
|
1029
|
+
# =========================================================================
|
|
1030
|
+
# Delete Operations
|
|
1031
|
+
# =========================================================================
|
|
1032
|
+
|
|
1033
|
+
def delete(self, title: str) -> bool:
|
|
1034
|
+
"""Delete a learning by title.
|
|
1035
|
+
|
|
1036
|
+
Args:
|
|
1037
|
+
title: The title of the learning to delete.
|
|
1038
|
+
|
|
1039
|
+
Returns:
|
|
1040
|
+
True if deleted, False otherwise.
|
|
1041
|
+
"""
|
|
1042
|
+
if not self.knowledge:
|
|
1043
|
+
log_warning("LearnedKnowledgeStore.delete: no knowledge base configured")
|
|
1044
|
+
return False
|
|
1045
|
+
|
|
1046
|
+
try:
|
|
1047
|
+
if hasattr(self.knowledge, "delete_content"):
|
|
1048
|
+
self.knowledge.delete_content(name=title)
|
|
1049
|
+
log_debug(f"LearnedKnowledgeStore.delete: deleted learning '{title}'")
|
|
1050
|
+
return True
|
|
1051
|
+
else:
|
|
1052
|
+
log_warning("LearnedKnowledgeStore.delete: knowledge base does not support deletion")
|
|
1053
|
+
return False
|
|
1054
|
+
|
|
1055
|
+
except Exception as e:
|
|
1056
|
+
log_warning(f"LearnedKnowledgeStore.delete failed: {e}")
|
|
1057
|
+
return False
|
|
1058
|
+
|
|
1059
|
+
async def adelete(self, title: str) -> bool:
|
|
1060
|
+
"""Async version of delete."""
|
|
1061
|
+
if not self.knowledge:
|
|
1062
|
+
log_warning("LearnedKnowledgeStore.adelete: no knowledge base configured")
|
|
1063
|
+
return False
|
|
1064
|
+
|
|
1065
|
+
try:
|
|
1066
|
+
if hasattr(self.knowledge, "adelete_content"):
|
|
1067
|
+
await self.knowledge.adelete_content(name=title)
|
|
1068
|
+
elif hasattr(self.knowledge, "delete_content"):
|
|
1069
|
+
self.knowledge.delete_content(name=title)
|
|
1070
|
+
else:
|
|
1071
|
+
log_warning("LearnedKnowledgeStore.adelete: knowledge base does not support deletion")
|
|
1072
|
+
return False
|
|
1073
|
+
|
|
1074
|
+
log_debug(f"LearnedKnowledgeStore.adelete: deleted learning '{title}'")
|
|
1075
|
+
return True
|
|
1076
|
+
|
|
1077
|
+
except Exception as e:
|
|
1078
|
+
log_warning(f"LearnedKnowledgeStore.adelete failed: {e}")
|
|
1079
|
+
return False
|
|
1080
|
+
|
|
1081
|
+
# =========================================================================
|
|
1082
|
+
# Background Extraction (ALWAYS mode)
|
|
1083
|
+
# =========================================================================
|
|
1084
|
+
|
|
1085
|
+
def extract_and_save(
|
|
1086
|
+
self,
|
|
1087
|
+
messages: List[Any],
|
|
1088
|
+
user_id: Optional[str] = None,
|
|
1089
|
+
agent_id: Optional[str] = None,
|
|
1090
|
+
team_id: Optional[str] = None,
|
|
1091
|
+
namespace: Optional[str] = None,
|
|
1092
|
+
) -> None:
|
|
1093
|
+
"""Extract learnings from messages (sync)."""
|
|
1094
|
+
if not self.model or not self.knowledge:
|
|
1095
|
+
return
|
|
1096
|
+
|
|
1097
|
+
try:
|
|
1098
|
+
conversation_text = self._messages_to_text(messages=messages)
|
|
1099
|
+
|
|
1100
|
+
# Search for existing learnings to avoid duplicates
|
|
1101
|
+
existing = self.search(query=conversation_text[:500], limit=5)
|
|
1102
|
+
existing_summary = self._summarize_existing(learnings=existing)
|
|
1103
|
+
|
|
1104
|
+
extraction_messages = self._build_extraction_messages(
|
|
1105
|
+
conversation_text=conversation_text,
|
|
1106
|
+
existing_summary=existing_summary,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
tools = self._get_extraction_tools(
|
|
1110
|
+
user_id=user_id,
|
|
1111
|
+
agent_id=agent_id,
|
|
1112
|
+
team_id=team_id,
|
|
1113
|
+
namespace=namespace,
|
|
1114
|
+
)
|
|
1115
|
+
functions = self._build_functions_for_model(tools=tools)
|
|
1116
|
+
|
|
1117
|
+
model_copy = deepcopy(self.model)
|
|
1118
|
+
response = model_copy.response(
|
|
1119
|
+
messages=extraction_messages,
|
|
1120
|
+
tools=functions,
|
|
1121
|
+
)
|
|
1122
|
+
|
|
1123
|
+
if response.tool_executions:
|
|
1124
|
+
self.learning_saved = True
|
|
1125
|
+
log_debug("LearnedKnowledgeStore: Extraction saved new learning(s)")
|
|
1126
|
+
|
|
1127
|
+
except Exception as e:
|
|
1128
|
+
log_warning(f"LearnedKnowledgeStore.extract_and_save failed: {e}")
|
|
1129
|
+
|
|
1130
|
+
async def aextract_and_save(
|
|
1131
|
+
self,
|
|
1132
|
+
messages: List[Any],
|
|
1133
|
+
user_id: Optional[str] = None,
|
|
1134
|
+
agent_id: Optional[str] = None,
|
|
1135
|
+
team_id: Optional[str] = None,
|
|
1136
|
+
namespace: Optional[str] = None,
|
|
1137
|
+
) -> None:
|
|
1138
|
+
"""Extract learnings from messages (async)."""
|
|
1139
|
+
if not self.model or not self.knowledge:
|
|
1140
|
+
return
|
|
1141
|
+
|
|
1142
|
+
try:
|
|
1143
|
+
conversation_text = self._messages_to_text(messages=messages)
|
|
1144
|
+
|
|
1145
|
+
# Search for existing learnings to avoid duplicates
|
|
1146
|
+
existing = await self.asearch(query=conversation_text[:500], limit=5)
|
|
1147
|
+
existing_summary = self._summarize_existing(learnings=existing)
|
|
1148
|
+
|
|
1149
|
+
extraction_messages = self._build_extraction_messages(
|
|
1150
|
+
conversation_text=conversation_text,
|
|
1151
|
+
existing_summary=existing_summary,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
tools = self._aget_extraction_tools(
|
|
1155
|
+
user_id=user_id,
|
|
1156
|
+
agent_id=agent_id,
|
|
1157
|
+
team_id=team_id,
|
|
1158
|
+
namespace=namespace,
|
|
1159
|
+
)
|
|
1160
|
+
functions = self._build_functions_for_model(tools=tools)
|
|
1161
|
+
|
|
1162
|
+
model_copy = deepcopy(self.model)
|
|
1163
|
+
response = await model_copy.aresponse(
|
|
1164
|
+
messages=extraction_messages,
|
|
1165
|
+
tools=functions,
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
if response.tool_executions:
|
|
1169
|
+
self.learning_saved = True
|
|
1170
|
+
log_debug("LearnedKnowledgeStore: Extraction saved new learning(s)")
|
|
1171
|
+
|
|
1172
|
+
except Exception as e:
|
|
1173
|
+
log_warning(f"LearnedKnowledgeStore.aextract_and_save failed: {e}")
|
|
1174
|
+
|
|
1175
|
+
def _build_extraction_messages(
|
|
1176
|
+
self,
|
|
1177
|
+
conversation_text: str,
|
|
1178
|
+
existing_summary: str,
|
|
1179
|
+
) -> List[Any]:
|
|
1180
|
+
"""Build messages for extraction."""
|
|
1181
|
+
from agno.models.message import Message
|
|
1182
|
+
|
|
1183
|
+
system_prompt = dedent("""\
|
|
1184
|
+
You are a Learning Extractor. Your job is to identify genuinely reusable insights
|
|
1185
|
+
from conversations - the kind of knowledge that would help with similar tasks in the future.
|
|
1186
|
+
|
|
1187
|
+
## What Makes Something Worth Saving
|
|
1188
|
+
|
|
1189
|
+
A good learning is:
|
|
1190
|
+
- **Discovered, not stated**: The insight emerged through work, not just repeated from the user
|
|
1191
|
+
- **Non-obvious**: It required reasoning, investigation, or experience to arrive at
|
|
1192
|
+
- **Reusable**: It applies to a category of problems, not just this exact situation
|
|
1193
|
+
- **Actionable**: Someone encountering a similar situation could apply it directly
|
|
1194
|
+
- **Durable**: It won't become outdated quickly
|
|
1195
|
+
|
|
1196
|
+
## What NOT to Save
|
|
1197
|
+
|
|
1198
|
+
- **Raw facts**: "Python 3.12 was released in October 2023" (use search for retrieval)
|
|
1199
|
+
- **User-specific info**: "User prefers TypeScript" (belongs in user memory)
|
|
1200
|
+
- **Common knowledge**: "Use version control for code" (everyone knows this)
|
|
1201
|
+
- **One-off answers**: "The error was a typo on line 42" (not generalizable)
|
|
1202
|
+
- **Summaries**: Recaps of what was discussed (no new insight)
|
|
1203
|
+
- **Uncertain conclusions**: If you're not confident, don't save it
|
|
1204
|
+
|
|
1205
|
+
## Examples of Good Learnings
|
|
1206
|
+
|
|
1207
|
+
From a debugging session:
|
|
1208
|
+
> **Title:** Debugging intermittent PostgreSQL connection timeouts
|
|
1209
|
+
> **Learning:** When connection timeouts are intermittent, check for connection pool exhaustion
|
|
1210
|
+
> before investigating network issues. Monitor active connections vs pool size, and look for
|
|
1211
|
+
> long-running transactions that hold connections.
|
|
1212
|
+
> **Context:** Diagnosing database connectivity issues in production
|
|
1213
|
+
|
|
1214
|
+
From an architecture discussion:
|
|
1215
|
+
> **Title:** Event sourcing trade-offs for audit requirements
|
|
1216
|
+
> **Learning:** Event sourcing adds complexity but provides natural audit trails. For systems
|
|
1217
|
+
> where audit is the primary driver, consider a simpler append-only log table with the main
|
|
1218
|
+
> data model unchanged - you get audit without the full event sourcing overhead.
|
|
1219
|
+
> **Context:** Evaluating architecture patterns when audit trails are required
|
|
1220
|
+
|
|
1221
|
+
## Examples of What NOT to Save
|
|
1222
|
+
|
|
1223
|
+
- "The user's API endpoint was returning 500 errors" (specific incident, not insight)
|
|
1224
|
+
- "React is a popular frontend framework" (common knowledge)
|
|
1225
|
+
- "We discussed three options for the database" (summary, no insight)
|
|
1226
|
+
- "Always write tests" (too vague to be actionable)
|
|
1227
|
+
|
|
1228
|
+
""")
|
|
1229
|
+
|
|
1230
|
+
if existing_summary:
|
|
1231
|
+
system_prompt += f"""## Already Saved (DO NOT DUPLICATE)
|
|
1232
|
+
|
|
1233
|
+
These insights are already in the knowledge base. Do not save variations of these:
|
|
1234
|
+
|
|
1235
|
+
{existing_summary}
|
|
1236
|
+
|
|
1237
|
+
"""
|
|
1238
|
+
|
|
1239
|
+
system_prompt += dedent("""\
|
|
1240
|
+
## Your Task
|
|
1241
|
+
|
|
1242
|
+
Review the conversation below. If - and only if - it contains a genuinely reusable insight
|
|
1243
|
+
that isn't already captured, save it using the save_learning tool.
|
|
1244
|
+
|
|
1245
|
+
**Important:**
|
|
1246
|
+
- Most conversations will NOT produce a learning. That's expected and correct.
|
|
1247
|
+
- When in doubt, don't save. Quality over quantity.
|
|
1248
|
+
- One excellent learning is worth more than five mediocre ones.
|
|
1249
|
+
- It's perfectly fine to do nothing if there's nothing worth saving.\
|
|
1250
|
+
""")
|
|
1251
|
+
|
|
1252
|
+
return [
|
|
1253
|
+
Message(role="system", content=system_prompt),
|
|
1254
|
+
Message(role="user", content=f"Review this conversation for reusable insights:\n\n{conversation_text}"),
|
|
1255
|
+
]
|
|
1256
|
+
|
|
1257
|
+
def _get_extraction_tools(
|
|
1258
|
+
self,
|
|
1259
|
+
user_id: Optional[str] = None,
|
|
1260
|
+
agent_id: Optional[str] = None,
|
|
1261
|
+
team_id: Optional[str] = None,
|
|
1262
|
+
namespace: Optional[str] = None,
|
|
1263
|
+
) -> List[Callable]:
|
|
1264
|
+
"""Get sync extraction tools."""
|
|
1265
|
+
effective_namespace = namespace or "global"
|
|
1266
|
+
|
|
1267
|
+
def save_learning(
|
|
1268
|
+
title: str,
|
|
1269
|
+
learning: str,
|
|
1270
|
+
context: Optional[str] = None,
|
|
1271
|
+
tags: Optional[List[str]] = None,
|
|
1272
|
+
) -> str:
|
|
1273
|
+
"""Save a genuinely reusable insight discovered in this conversation.
|
|
1274
|
+
|
|
1275
|
+
Only call this if you've identified something that:
|
|
1276
|
+
- Required investigation or reasoning to discover
|
|
1277
|
+
- Would help with similar future tasks
|
|
1278
|
+
- Isn't already captured in existing learnings
|
|
1279
|
+
- Is specific and actionable enough to apply directly
|
|
1280
|
+
|
|
1281
|
+
Args:
|
|
1282
|
+
title: Concise, searchable title that captures the topic.
|
|
1283
|
+
learning: The insight itself - specific enough to apply, general enough to reuse.
|
|
1284
|
+
context: When/where this applies (helps with future relevance matching).
|
|
1285
|
+
tags: Categories for organization.
|
|
1286
|
+
|
|
1287
|
+
Returns:
|
|
1288
|
+
Confirmation message.
|
|
1289
|
+
"""
|
|
1290
|
+
success = self.save(
|
|
1291
|
+
title=title,
|
|
1292
|
+
learning=learning,
|
|
1293
|
+
context=context,
|
|
1294
|
+
tags=tags,
|
|
1295
|
+
user_id=user_id,
|
|
1296
|
+
agent_id=agent_id,
|
|
1297
|
+
team_id=team_id,
|
|
1298
|
+
namespace=effective_namespace,
|
|
1299
|
+
)
|
|
1300
|
+
return f"Saved: {title}" if success else "Failed to save"
|
|
1301
|
+
|
|
1302
|
+
return [save_learning]
|
|
1303
|
+
|
|
1304
|
+
def _aget_extraction_tools(
|
|
1305
|
+
self,
|
|
1306
|
+
user_id: Optional[str] = None,
|
|
1307
|
+
agent_id: Optional[str] = None,
|
|
1308
|
+
team_id: Optional[str] = None,
|
|
1309
|
+
namespace: Optional[str] = None,
|
|
1310
|
+
) -> List[Callable]:
|
|
1311
|
+
"""Get async extraction tools."""
|
|
1312
|
+
effective_namespace = namespace or "global"
|
|
1313
|
+
|
|
1314
|
+
async def save_learning(
|
|
1315
|
+
title: str,
|
|
1316
|
+
learning: str,
|
|
1317
|
+
context: Optional[str] = None,
|
|
1318
|
+
tags: Optional[List[str]] = None,
|
|
1319
|
+
) -> str:
|
|
1320
|
+
"""Save a genuinely reusable insight discovered in this conversation.
|
|
1321
|
+
|
|
1322
|
+
Only call this if you've identified something that:
|
|
1323
|
+
- Required investigation or reasoning to discover
|
|
1324
|
+
- Would help with similar future tasks
|
|
1325
|
+
- Isn't already captured in existing learnings
|
|
1326
|
+
- Is specific and actionable enough to apply directly
|
|
1327
|
+
|
|
1328
|
+
Args:
|
|
1329
|
+
title: Concise, searchable title that captures the topic.
|
|
1330
|
+
learning: The insight itself - specific enough to apply, general enough to reuse.
|
|
1331
|
+
context: When/where this applies (helps with future relevance matching).
|
|
1332
|
+
tags: Categories for organization.
|
|
1333
|
+
|
|
1334
|
+
Returns:
|
|
1335
|
+
Confirmation message.
|
|
1336
|
+
"""
|
|
1337
|
+
success = await self.asave(
|
|
1338
|
+
title=title,
|
|
1339
|
+
learning=learning,
|
|
1340
|
+
context=context,
|
|
1341
|
+
tags=tags,
|
|
1342
|
+
user_id=user_id,
|
|
1343
|
+
agent_id=agent_id,
|
|
1344
|
+
team_id=team_id,
|
|
1345
|
+
namespace=effective_namespace,
|
|
1346
|
+
)
|
|
1347
|
+
return f"Saved: {title}" if success else "Failed to save"
|
|
1348
|
+
|
|
1349
|
+
return [save_learning]
|
|
1350
|
+
|
|
1351
|
+
def _build_functions_for_model(self, tools: List[Callable]) -> List[Any]:
|
|
1352
|
+
"""Convert callables to Functions for model."""
|
|
1353
|
+
from agno.tools.function import Function
|
|
1354
|
+
|
|
1355
|
+
functions = []
|
|
1356
|
+
seen_names = set()
|
|
1357
|
+
|
|
1358
|
+
for tool in tools:
|
|
1359
|
+
try:
|
|
1360
|
+
name = tool.__name__
|
|
1361
|
+
if name in seen_names:
|
|
1362
|
+
continue
|
|
1363
|
+
seen_names.add(name)
|
|
1364
|
+
|
|
1365
|
+
func = Function.from_callable(tool, strict=True)
|
|
1366
|
+
func.strict = True
|
|
1367
|
+
functions.append(func)
|
|
1368
|
+
except Exception as e:
|
|
1369
|
+
log_warning(f"Could not add function {tool}: {e}")
|
|
1370
|
+
|
|
1371
|
+
return functions
|
|
1372
|
+
|
|
1373
|
+
def _messages_to_text(self, messages: List[Any]) -> str:
|
|
1374
|
+
"""Convert messages to text for extraction."""
|
|
1375
|
+
parts = []
|
|
1376
|
+
for msg in messages:
|
|
1377
|
+
if msg.role == "user":
|
|
1378
|
+
content = msg.get_content_string() if hasattr(msg, "get_content_string") else str(msg.content)
|
|
1379
|
+
if content and content.strip():
|
|
1380
|
+
parts.append(f"User: {content}")
|
|
1381
|
+
elif msg.role in ["assistant", "model"]:
|
|
1382
|
+
content = msg.get_content_string() if hasattr(msg, "get_content_string") else str(msg.content)
|
|
1383
|
+
if content and content.strip():
|
|
1384
|
+
parts.append(f"Assistant: {content}")
|
|
1385
|
+
return "\n".join(parts)
|
|
1386
|
+
|
|
1387
|
+
def _summarize_existing(self, learnings: List[Any]) -> str:
|
|
1388
|
+
"""Summarize existing learnings to help avoid duplicates."""
|
|
1389
|
+
if not learnings:
|
|
1390
|
+
return ""
|
|
1391
|
+
|
|
1392
|
+
parts = []
|
|
1393
|
+
for learning in learnings[:5]:
|
|
1394
|
+
if hasattr(learning, "title") and hasattr(learning, "learning"):
|
|
1395
|
+
parts.append(f"- {learning.title}: {learning.learning[:100]}...")
|
|
1396
|
+
return "\n".join(parts)
|
|
1397
|
+
|
|
1398
|
+
# =========================================================================
|
|
1399
|
+
# Private Helpers
|
|
1400
|
+
# =========================================================================
|
|
1401
|
+
|
|
1402
|
+
def _build_learning_id(self, title: str) -> str:
|
|
1403
|
+
"""Build a unique learning ID from title."""
|
|
1404
|
+
return f"learning_{title.lower().replace(' ', '_')[:32]}"
|
|
1405
|
+
|
|
1406
|
+
def _parse_result(self, result: Any) -> Optional[Any]:
|
|
1407
|
+
"""Parse a search result into a learning object."""
|
|
1408
|
+
import json
|
|
1409
|
+
|
|
1410
|
+
try:
|
|
1411
|
+
content = None
|
|
1412
|
+
|
|
1413
|
+
if isinstance(result, dict):
|
|
1414
|
+
content = result.get("content") or result.get("text") or result
|
|
1415
|
+
elif hasattr(result, "content"):
|
|
1416
|
+
content = result.content
|
|
1417
|
+
elif hasattr(result, "text"):
|
|
1418
|
+
content = result.text
|
|
1419
|
+
elif isinstance(result, str):
|
|
1420
|
+
content = result
|
|
1421
|
+
|
|
1422
|
+
if not content:
|
|
1423
|
+
return None
|
|
1424
|
+
|
|
1425
|
+
if isinstance(content, str):
|
|
1426
|
+
try:
|
|
1427
|
+
content = json.loads(content)
|
|
1428
|
+
except json.JSONDecodeError:
|
|
1429
|
+
return self.schema(title="Learning", learning=content)
|
|
1430
|
+
|
|
1431
|
+
if isinstance(content, dict):
|
|
1432
|
+
from dataclasses import fields
|
|
1433
|
+
|
|
1434
|
+
field_names = {f.name for f in fields(self.schema)}
|
|
1435
|
+
filtered = {k: v for k, v in content.items() if k in field_names}
|
|
1436
|
+
return self.schema(**filtered)
|
|
1437
|
+
|
|
1438
|
+
return None
|
|
1439
|
+
|
|
1440
|
+
except Exception as e:
|
|
1441
|
+
log_warning(f"LearnedKnowledgeStore._parse_result failed: {e}")
|
|
1442
|
+
return None
|
|
1443
|
+
|
|
1444
|
+
def _to_text_content(self, learning: Any) -> str:
|
|
1445
|
+
"""Convert a learning object to text content for storage."""
|
|
1446
|
+
import json
|
|
1447
|
+
|
|
1448
|
+
learning_dict = to_dict_safe(learning)
|
|
1449
|
+
return json.dumps(learning_dict, ensure_ascii=False)
|
|
1450
|
+
|
|
1451
|
+
def _format_single_learning(self, learning: Any) -> str:
|
|
1452
|
+
"""Format a single learning for display."""
|
|
1453
|
+
parts = []
|
|
1454
|
+
|
|
1455
|
+
if hasattr(learning, "title") and learning.title:
|
|
1456
|
+
parts.append(f"**{learning.title}**")
|
|
1457
|
+
|
|
1458
|
+
if hasattr(learning, "learning") and learning.learning:
|
|
1459
|
+
parts.append(learning.learning)
|
|
1460
|
+
|
|
1461
|
+
if hasattr(learning, "context") and learning.context:
|
|
1462
|
+
parts.append(f"_Context: {learning.context}_")
|
|
1463
|
+
|
|
1464
|
+
if hasattr(learning, "tags") and learning.tags:
|
|
1465
|
+
tags_str = ", ".join(learning.tags)
|
|
1466
|
+
parts.append(f"_Tags: {tags_str}_")
|
|
1467
|
+
|
|
1468
|
+
if hasattr(learning, "namespace") and learning.namespace and learning.namespace != "global":
|
|
1469
|
+
parts.append(f"_Namespace: {learning.namespace}_")
|
|
1470
|
+
|
|
1471
|
+
return "\n ".join(parts)
|
|
1472
|
+
|
|
1473
|
+
def _format_learnings_list(self, learnings: List[Any]) -> str:
|
|
1474
|
+
"""Format a list of learnings for tool output."""
|
|
1475
|
+
parts = []
|
|
1476
|
+
for i, learning in enumerate(learnings, 1):
|
|
1477
|
+
formatted = self._format_single_learning(learning=learning)
|
|
1478
|
+
if formatted:
|
|
1479
|
+
parts.append(f"{i}. {formatted}")
|
|
1480
|
+
return "\n".join(parts)
|
|
1481
|
+
|
|
1482
|
+
# =========================================================================
|
|
1483
|
+
# Representation
|
|
1484
|
+
# =========================================================================
|
|
1485
|
+
|
|
1486
|
+
def __repr__(self) -> str:
|
|
1487
|
+
"""String representation for debugging."""
|
|
1488
|
+
has_knowledge = self.knowledge is not None
|
|
1489
|
+
has_model = self.model is not None
|
|
1490
|
+
return (
|
|
1491
|
+
f"LearnedKnowledgeStore("
|
|
1492
|
+
f"mode={self.config.mode.value}, "
|
|
1493
|
+
f"knowledge={has_knowledge}, "
|
|
1494
|
+
f"model={has_model}, "
|
|
1495
|
+
f"enable_agent_tools={self.config.enable_agent_tools})"
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
def print(
|
|
1499
|
+
self,
|
|
1500
|
+
query: str,
|
|
1501
|
+
*,
|
|
1502
|
+
user_id: Optional[str] = None,
|
|
1503
|
+
namespace: Optional[str] = None,
|
|
1504
|
+
limit: int = 10,
|
|
1505
|
+
raw: bool = False,
|
|
1506
|
+
) -> None:
|
|
1507
|
+
"""Print formatted learned knowledge search results.
|
|
1508
|
+
|
|
1509
|
+
Args:
|
|
1510
|
+
query: Search query to find relevant learnings.
|
|
1511
|
+
user_id: User ID for "user" namespace scoping.
|
|
1512
|
+
namespace: Namespace to filter by.
|
|
1513
|
+
limit: Maximum number of learnings to display.
|
|
1514
|
+
raw: If True, print raw list using pprint instead of formatted panel.
|
|
1515
|
+
|
|
1516
|
+
Example:
|
|
1517
|
+
>>> store.print(query="API design")
|
|
1518
|
+
╭───────────── Learned Knowledge ──────────────╮
|
|
1519
|
+
│ 1. PostgreSQL JSONB indexing │
|
|
1520
|
+
│ For frequently queried nested JSONB... │
|
|
1521
|
+
│ Context: When query performance degrades │
|
|
1522
|
+
│ Tags: postgresql, performance │
|
|
1523
|
+
│ │
|
|
1524
|
+
│ 2. Handling rate limits in async clients │
|
|
1525
|
+
│ Implement exponential backoff with... │
|
|
1526
|
+
│ Context: When building API clients │
|
|
1527
|
+
│ Tags: api, async, rate-limiting │
|
|
1528
|
+
╰──────────────── query: API ──────────────────╯
|
|
1529
|
+
"""
|
|
1530
|
+
from agno.learn.utils import print_panel
|
|
1531
|
+
|
|
1532
|
+
learnings = self.search(
|
|
1533
|
+
query=query,
|
|
1534
|
+
user_id=user_id,
|
|
1535
|
+
namespace=namespace,
|
|
1536
|
+
limit=limit,
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
lines = []
|
|
1540
|
+
|
|
1541
|
+
for i, learning in enumerate(learnings, 1):
|
|
1542
|
+
if i > 1:
|
|
1543
|
+
lines.append("") # Separator between learnings
|
|
1544
|
+
|
|
1545
|
+
# Title
|
|
1546
|
+
title = getattr(learning, "title", None)
|
|
1547
|
+
if title:
|
|
1548
|
+
lines.append(f"[bold]{i}. {title}[/bold]")
|
|
1549
|
+
else:
|
|
1550
|
+
lines.append(f"[bold]{i}. (untitled)[/bold]")
|
|
1551
|
+
|
|
1552
|
+
# Learning content
|
|
1553
|
+
content = getattr(learning, "learning", None)
|
|
1554
|
+
if content:
|
|
1555
|
+
# Truncate long content for display
|
|
1556
|
+
if len(content) > 200:
|
|
1557
|
+
content = content[:200] + "..."
|
|
1558
|
+
lines.append(f" {content}")
|
|
1559
|
+
|
|
1560
|
+
# Context
|
|
1561
|
+
context = getattr(learning, "context", None)
|
|
1562
|
+
if context:
|
|
1563
|
+
lines.append(f" [dim]Context: {context}[/dim]")
|
|
1564
|
+
|
|
1565
|
+
# Tags
|
|
1566
|
+
tags = getattr(learning, "tags", None)
|
|
1567
|
+
if tags:
|
|
1568
|
+
tags_str = ", ".join(tags)
|
|
1569
|
+
lines.append(f" [dim]Tags: {tags_str}[/dim]")
|
|
1570
|
+
|
|
1571
|
+
# Namespace (if not global)
|
|
1572
|
+
ns = getattr(learning, "namespace", None)
|
|
1573
|
+
if ns and ns != "global":
|
|
1574
|
+
lines.append(f" [dim]Namespace: {ns}[/dim]")
|
|
1575
|
+
|
|
1576
|
+
print_panel(
|
|
1577
|
+
title="Learned Knowledge",
|
|
1578
|
+
subtitle=f"query: {query[:30]}{'...' if len(query) > 30 else ''}",
|
|
1579
|
+
lines=lines,
|
|
1580
|
+
empty_message="No learnings found",
|
|
1581
|
+
raw_data=learnings,
|
|
1582
|
+
raw=raw,
|
|
1583
|
+
)
|