agno 2.2.13__py3-none-any.whl → 2.4.3__py3-none-any.whl

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