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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. agno/agent/agent.py +357 -28
  2. agno/db/base.py +214 -0
  3. agno/db/dynamo/dynamo.py +47 -0
  4. agno/db/firestore/firestore.py +47 -0
  5. agno/db/gcs_json/gcs_json_db.py +47 -0
  6. agno/db/in_memory/in_memory_db.py +47 -0
  7. agno/db/json/json_db.py +47 -0
  8. agno/db/mongo/async_mongo.py +229 -0
  9. agno/db/mongo/mongo.py +47 -0
  10. agno/db/mongo/schemas.py +16 -0
  11. agno/db/mysql/async_mysql.py +47 -0
  12. agno/db/mysql/mysql.py +47 -0
  13. agno/db/postgres/async_postgres.py +231 -0
  14. agno/db/postgres/postgres.py +239 -0
  15. agno/db/postgres/schemas.py +19 -0
  16. agno/db/redis/redis.py +47 -0
  17. agno/db/singlestore/singlestore.py +47 -0
  18. agno/db/sqlite/async_sqlite.py +242 -0
  19. agno/db/sqlite/schemas.py +18 -0
  20. agno/db/sqlite/sqlite.py +239 -0
  21. agno/db/surrealdb/surrealdb.py +47 -0
  22. agno/knowledge/chunking/code.py +90 -0
  23. agno/knowledge/chunking/document.py +62 -2
  24. agno/knowledge/chunking/strategy.py +14 -0
  25. agno/knowledge/knowledge.py +7 -1
  26. agno/knowledge/reader/arxiv_reader.py +1 -0
  27. agno/knowledge/reader/csv_reader.py +1 -0
  28. agno/knowledge/reader/docx_reader.py +1 -0
  29. agno/knowledge/reader/firecrawl_reader.py +1 -0
  30. agno/knowledge/reader/json_reader.py +1 -0
  31. agno/knowledge/reader/markdown_reader.py +1 -0
  32. agno/knowledge/reader/pdf_reader.py +1 -0
  33. agno/knowledge/reader/pptx_reader.py +1 -0
  34. agno/knowledge/reader/s3_reader.py +1 -0
  35. agno/knowledge/reader/tavily_reader.py +1 -0
  36. agno/knowledge/reader/text_reader.py +1 -0
  37. agno/knowledge/reader/web_search_reader.py +1 -0
  38. agno/knowledge/reader/website_reader.py +1 -0
  39. agno/knowledge/reader/wikipedia_reader.py +1 -0
  40. agno/knowledge/reader/youtube_reader.py +1 -0
  41. agno/knowledge/utils.py +1 -0
  42. agno/learn/__init__.py +65 -0
  43. agno/learn/config.py +463 -0
  44. agno/learn/curate.py +185 -0
  45. agno/learn/machine.py +690 -0
  46. agno/learn/schemas.py +1043 -0
  47. agno/learn/stores/__init__.py +35 -0
  48. agno/learn/stores/entity_memory.py +3275 -0
  49. agno/learn/stores/learned_knowledge.py +1583 -0
  50. agno/learn/stores/protocol.py +117 -0
  51. agno/learn/stores/session_context.py +1217 -0
  52. agno/learn/stores/user_memory.py +1495 -0
  53. agno/learn/stores/user_profile.py +1220 -0
  54. agno/learn/utils.py +209 -0
  55. agno/models/base.py +59 -0
  56. agno/os/routers/agents/router.py +4 -4
  57. agno/os/routers/knowledge/knowledge.py +7 -0
  58. agno/os/routers/teams/router.py +3 -3
  59. agno/os/routers/workflows/router.py +5 -5
  60. agno/os/utils.py +55 -3
  61. agno/team/team.py +131 -0
  62. agno/tools/browserbase.py +78 -6
  63. agno/tools/google_bigquery.py +11 -2
  64. agno/utils/agent.py +30 -1
  65. agno/workflow/workflow.py +198 -0
  66. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/METADATA +24 -2
  67. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/RECORD +70 -56
  68. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/WHEEL +0 -0
  69. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/licenses/LICENSE +0 -0
  70. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/top_level.txt +0 -0
@@ -58,6 +58,7 @@ class AsyncPostgresDb(AsyncBaseDb):
58
58
  traces_table: Optional[str] = None,
59
59
  spans_table: Optional[str] = None,
60
60
  versions_table: Optional[str] = None,
61
+ learnings_table: Optional[str] = None,
61
62
  create_schema: bool = True,
62
63
  db_id: Optional[str] = None, # Deprecated, use id instead.
63
64
  ):
@@ -92,6 +93,7 @@ class AsyncPostgresDb(AsyncBaseDb):
92
93
  traces_table (Optional[str]): Name of the table to store run traces.
93
94
  spans_table (Optional[str]): Name of the table to store span events.
94
95
  versions_table (Optional[str]): Name of the table to store schema versions.
96
+ learnings_table (Optional[str]): Name of the table to store learnings.
95
97
  create_schema (bool): Whether to automatically create the database schema if it doesn't exist.
96
98
  Set to False if schema is managed externally (e.g., via migrations). Defaults to True.
97
99
  db_id: Deprecated, use id instead.
@@ -118,6 +120,7 @@ class AsyncPostgresDb(AsyncBaseDb):
118
120
  traces_table=traces_table,
119
121
  spans_table=spans_table,
120
122
  versions_table=versions_table,
123
+ learnings_table=learnings_table,
121
124
  )
122
125
 
123
126
  _engine: Optional[AsyncEngine] = db_engine
@@ -173,6 +176,7 @@ class AsyncPostgresDb(AsyncBaseDb):
173
176
  (self.eval_table_name, "evals"),
174
177
  (self.knowledge_table_name, "knowledge"),
175
178
  (self.versions_table_name, "versions"),
179
+ (self.learnings_table_name, "learnings"),
176
180
  ]
177
181
 
178
182
  for table_name, table_type in tables_to_create:
@@ -372,6 +376,15 @@ class AsyncPostgresDb(AsyncBaseDb):
372
376
  )
373
377
  return self.spans_table
374
378
 
379
+ if table_type == "learnings":
380
+ if not hasattr(self, "learnings_table"):
381
+ self.learnings_table = await self._get_or_create_table(
382
+ table_name=self.learnings_table_name,
383
+ table_type="learnings",
384
+ create_table_if_not_found=create_table_if_not_found,
385
+ )
386
+ return self.learnings_table
387
+
375
388
  raise ValueError(f"Unknown table type: {table_type}")
376
389
 
377
390
  async def _get_or_create_table(
@@ -2716,3 +2729,221 @@ class AsyncPostgresDb(AsyncBaseDb):
2716
2729
  except Exception as e:
2717
2730
  log_error(f"Error getting spans: {e}")
2718
2731
  return []
2732
+
2733
+ # -- Learning methods --
2734
+ async def get_learning(
2735
+ self,
2736
+ learning_type: str,
2737
+ user_id: Optional[str] = None,
2738
+ agent_id: Optional[str] = None,
2739
+ team_id: Optional[str] = None,
2740
+ session_id: Optional[str] = None,
2741
+ namespace: Optional[str] = None,
2742
+ entity_id: Optional[str] = None,
2743
+ entity_type: Optional[str] = None,
2744
+ ) -> Optional[Dict[str, Any]]:
2745
+ """Async retrieve a learning record.
2746
+
2747
+ Args:
2748
+ learning_type: Type of learning ('user_profile', 'session_context', etc.)
2749
+ user_id: Filter by user ID.
2750
+ agent_id: Filter by agent ID.
2751
+ team_id: Filter by team ID.
2752
+ session_id: Filter by session ID.
2753
+ namespace: Filter by namespace ('user', 'global', or custom).
2754
+ entity_id: Filter by entity ID (for entity-specific learnings).
2755
+ entity_type: Filter by entity type ('person', 'company', etc.).
2756
+
2757
+ Returns:
2758
+ Dict with 'content' key containing the learning data, or None.
2759
+ """
2760
+ try:
2761
+ table = await self._get_table(table_type="learnings")
2762
+ if table is None:
2763
+ return None
2764
+
2765
+ async with self.async_session_factory() as sess:
2766
+ stmt = select(table).where(table.c.learning_type == learning_type)
2767
+
2768
+ if user_id is not None:
2769
+ stmt = stmt.where(table.c.user_id == user_id)
2770
+ if agent_id is not None:
2771
+ stmt = stmt.where(table.c.agent_id == agent_id)
2772
+ if team_id is not None:
2773
+ stmt = stmt.where(table.c.team_id == team_id)
2774
+ if session_id is not None:
2775
+ stmt = stmt.where(table.c.session_id == session_id)
2776
+ if namespace is not None:
2777
+ stmt = stmt.where(table.c.namespace == namespace)
2778
+ if entity_id is not None:
2779
+ stmt = stmt.where(table.c.entity_id == entity_id)
2780
+ if entity_type is not None:
2781
+ stmt = stmt.where(table.c.entity_type == entity_type)
2782
+
2783
+ result = await sess.execute(stmt)
2784
+ row = result.fetchone()
2785
+ if row is None:
2786
+ return None
2787
+
2788
+ row_dict = dict(row._mapping)
2789
+ return {"content": row_dict.get("content")}
2790
+
2791
+ except Exception as e:
2792
+ log_debug(f"Error retrieving learning: {e}")
2793
+ return None
2794
+
2795
+ async def upsert_learning(
2796
+ self,
2797
+ id: str,
2798
+ learning_type: str,
2799
+ content: Dict[str, Any],
2800
+ user_id: Optional[str] = None,
2801
+ agent_id: Optional[str] = None,
2802
+ team_id: Optional[str] = None,
2803
+ session_id: Optional[str] = None,
2804
+ namespace: Optional[str] = None,
2805
+ entity_id: Optional[str] = None,
2806
+ entity_type: Optional[str] = None,
2807
+ metadata: Optional[Dict[str, Any]] = None,
2808
+ ) -> None:
2809
+ """Async insert or update a learning record.
2810
+
2811
+ Args:
2812
+ id: Unique identifier for the learning.
2813
+ learning_type: Type of learning ('user_profile', 'session_context', etc.)
2814
+ content: The learning content as a dict.
2815
+ user_id: Associated user ID.
2816
+ agent_id: Associated agent ID.
2817
+ team_id: Associated team ID.
2818
+ session_id: Associated session ID.
2819
+ namespace: Namespace for scoping ('user', 'global', or custom).
2820
+ entity_id: Associated entity ID (for entity-specific learnings).
2821
+ entity_type: Entity type ('person', 'company', etc.).
2822
+ metadata: Optional metadata.
2823
+ """
2824
+ try:
2825
+ table = await self._get_table(table_type="learnings", create_table_if_not_found=True)
2826
+ if table is None:
2827
+ return
2828
+
2829
+ current_time = int(time.time())
2830
+
2831
+ async with self.async_session_factory() as sess, sess.begin():
2832
+ stmt = postgresql.insert(table).values(
2833
+ learning_id=id,
2834
+ learning_type=learning_type,
2835
+ namespace=namespace,
2836
+ user_id=user_id,
2837
+ agent_id=agent_id,
2838
+ team_id=team_id,
2839
+ session_id=session_id,
2840
+ entity_id=entity_id,
2841
+ entity_type=entity_type,
2842
+ content=content,
2843
+ metadata=metadata,
2844
+ created_at=current_time,
2845
+ updated_at=current_time,
2846
+ )
2847
+ stmt = stmt.on_conflict_do_update(
2848
+ index_elements=["learning_id"],
2849
+ set_=dict(
2850
+ content=content,
2851
+ metadata=metadata,
2852
+ updated_at=current_time,
2853
+ ),
2854
+ )
2855
+ await sess.execute(stmt)
2856
+
2857
+ log_debug(f"Upserted learning: {id}")
2858
+
2859
+ except Exception as e:
2860
+ log_debug(f"Error upserting learning: {e}")
2861
+
2862
+ async def delete_learning(self, id: str) -> bool:
2863
+ """Async delete a learning record.
2864
+
2865
+ Args:
2866
+ id: The learning ID to delete.
2867
+
2868
+ Returns:
2869
+ True if deleted, False otherwise.
2870
+ """
2871
+ try:
2872
+ table = await self._get_table(table_type="learnings")
2873
+ if table is None:
2874
+ return False
2875
+
2876
+ async with self.async_session_factory() as sess, sess.begin():
2877
+ stmt = table.delete().where(table.c.learning_id == id)
2878
+ result = await sess.execute(stmt)
2879
+ return result.rowcount > 0
2880
+
2881
+ except Exception as e:
2882
+ log_debug(f"Error deleting learning: {e}")
2883
+ return False
2884
+
2885
+ async def get_learnings(
2886
+ self,
2887
+ learning_type: Optional[str] = None,
2888
+ user_id: Optional[str] = None,
2889
+ agent_id: Optional[str] = None,
2890
+ team_id: Optional[str] = None,
2891
+ session_id: Optional[str] = None,
2892
+ namespace: Optional[str] = None,
2893
+ entity_id: Optional[str] = None,
2894
+ entity_type: Optional[str] = None,
2895
+ limit: Optional[int] = None,
2896
+ ) -> List[Dict[str, Any]]:
2897
+ """Async get multiple learning records.
2898
+
2899
+ Args:
2900
+ learning_type: Filter by learning type.
2901
+ user_id: Filter by user ID.
2902
+ agent_id: Filter by agent ID.
2903
+ team_id: Filter by team ID.
2904
+ session_id: Filter by session ID.
2905
+ namespace: Filter by namespace ('user', 'global', or custom).
2906
+ entity_id: Filter by entity ID (for entity-specific learnings).
2907
+ entity_type: Filter by entity type ('person', 'company', etc.).
2908
+ limit: Maximum number of records to return.
2909
+
2910
+ Returns:
2911
+ List of learning records.
2912
+ """
2913
+ try:
2914
+ table = await self._get_table(table_type="learnings")
2915
+ if table is None:
2916
+ return []
2917
+
2918
+ async with self.async_session_factory() as sess:
2919
+ stmt = select(table)
2920
+
2921
+ if learning_type is not None:
2922
+ stmt = stmt.where(table.c.learning_type == learning_type)
2923
+ if user_id is not None:
2924
+ stmt = stmt.where(table.c.user_id == user_id)
2925
+ if agent_id is not None:
2926
+ stmt = stmt.where(table.c.agent_id == agent_id)
2927
+ if team_id is not None:
2928
+ stmt = stmt.where(table.c.team_id == team_id)
2929
+ if session_id is not None:
2930
+ stmt = stmt.where(table.c.session_id == session_id)
2931
+ if namespace is not None:
2932
+ stmt = stmt.where(table.c.namespace == namespace)
2933
+ if entity_id is not None:
2934
+ stmt = stmt.where(table.c.entity_id == entity_id)
2935
+ if entity_type is not None:
2936
+ stmt = stmt.where(table.c.entity_type == entity_type)
2937
+
2938
+ stmt = stmt.order_by(table.c.updated_at.desc())
2939
+
2940
+ if limit is not None:
2941
+ stmt = stmt.limit(limit)
2942
+
2943
+ result = await sess.execute(stmt)
2944
+ rows = result.fetchall()
2945
+ return [dict(row._mapping) for row in rows]
2946
+
2947
+ except Exception as e:
2948
+ log_debug(f"Error getting learnings: {e}")
2949
+ return []
@@ -57,6 +57,7 @@ class PostgresDb(BaseDb):
57
57
  traces_table: Optional[str] = None,
58
58
  spans_table: Optional[str] = None,
59
59
  versions_table: Optional[str] = None,
60
+ learnings_table: Optional[str] = None,
60
61
  id: Optional[str] = None,
61
62
  create_schema: bool = True,
62
63
  ):
@@ -81,6 +82,7 @@ class PostgresDb(BaseDb):
81
82
  traces_table (Optional[str]): Name of the table to store run traces.
82
83
  spans_table (Optional[str]): Name of the table to store span events.
83
84
  versions_table (Optional[str]): Name of the table to store schema versions.
85
+ learnings_table (Optional[str]): Name of the table to store learnings.
84
86
  id (Optional[str]): ID of the database.
85
87
  create_schema (bool): Whether to automatically create the database schema if it doesn't exist.
86
88
  Set to False if schema is managed externally (e.g., via migrations). Defaults to True.
@@ -119,6 +121,7 @@ class PostgresDb(BaseDb):
119
121
  traces_table=traces_table,
120
122
  spans_table=spans_table,
121
123
  versions_table=versions_table,
124
+ learnings_table=learnings_table,
122
125
  )
123
126
 
124
127
  self.db_schema: str = db_schema if db_schema is not None else "ai"
@@ -159,6 +162,7 @@ class PostgresDb(BaseDb):
159
162
  (self.eval_table_name, "evals"),
160
163
  (self.knowledge_table_name, "knowledge"),
161
164
  (self.versions_table_name, "versions"),
165
+ (self.learnings_table_name, "learnings"),
162
166
  ]
163
167
 
164
168
  for table_name, table_type in tables_to_create:
@@ -344,6 +348,14 @@ class PostgresDb(BaseDb):
344
348
  )
345
349
  return self.spans_table
346
350
 
351
+ if table_type == "learnings":
352
+ self.learnings_table = self._get_or_create_table(
353
+ table_name=self.learnings_table_name,
354
+ table_type="learnings",
355
+ create_table_if_not_found=create_table_if_not_found,
356
+ )
357
+ return self.learnings_table
358
+
347
359
  raise ValueError(f"Unknown table type: {table_type}")
348
360
 
349
361
  def _get_or_create_table(
@@ -3025,3 +3037,230 @@ class PostgresDb(BaseDb):
3025
3037
  except Exception as e:
3026
3038
  log_error(f"Error getting spans: {e}")
3027
3039
  return []
3040
+
3041
+ # -- Learning methods --
3042
+ def get_learning(
3043
+ self,
3044
+ learning_type: str,
3045
+ user_id: Optional[str] = None,
3046
+ agent_id: Optional[str] = None,
3047
+ team_id: Optional[str] = None,
3048
+ workflow_id: Optional[str] = None,
3049
+ session_id: Optional[str] = None,
3050
+ namespace: Optional[str] = None,
3051
+ entity_id: Optional[str] = None,
3052
+ entity_type: Optional[str] = None,
3053
+ ) -> Optional[Dict[str, Any]]:
3054
+ """Retrieve a learning record.
3055
+
3056
+ Args:
3057
+ learning_type: Type of learning ('user_profile', 'session_context', etc.)
3058
+ user_id: Filter by user ID.
3059
+ agent_id: Filter by agent ID.
3060
+ team_id: Filter by team ID.
3061
+ workflow_id: Filter by workflow ID.
3062
+ session_id: Filter by session ID.
3063
+ namespace: Filter by namespace ('user', 'global', or custom).
3064
+ entity_id: Filter by entity ID (for entity-specific learnings).
3065
+ entity_type: Filter by entity type ('person', 'company', etc.).
3066
+
3067
+ Returns:
3068
+ Dict with 'content' key containing the learning data, or None.
3069
+ """
3070
+ try:
3071
+ table = self._get_table(table_type="learnings")
3072
+ if table is None:
3073
+ return None
3074
+
3075
+ with self.Session() as sess:
3076
+ stmt = select(table).where(table.c.learning_type == learning_type)
3077
+
3078
+ if user_id is not None:
3079
+ stmt = stmt.where(table.c.user_id == user_id)
3080
+ if agent_id is not None:
3081
+ stmt = stmt.where(table.c.agent_id == agent_id)
3082
+ if team_id is not None:
3083
+ stmt = stmt.where(table.c.team_id == team_id)
3084
+ if workflow_id is not None:
3085
+ stmt = stmt.where(table.c.workflow_id == workflow_id)
3086
+ if session_id is not None:
3087
+ stmt = stmt.where(table.c.session_id == session_id)
3088
+ if namespace is not None:
3089
+ stmt = stmt.where(table.c.namespace == namespace)
3090
+ if entity_id is not None:
3091
+ stmt = stmt.where(table.c.entity_id == entity_id)
3092
+ if entity_type is not None:
3093
+ stmt = stmt.where(table.c.entity_type == entity_type)
3094
+
3095
+ result = sess.execute(stmt).fetchone()
3096
+ if result is None:
3097
+ return None
3098
+
3099
+ row = dict(result._mapping)
3100
+ return {"content": row.get("content")}
3101
+
3102
+ except Exception as e:
3103
+ log_debug(f"Error retrieving learning: {e}")
3104
+ return None
3105
+
3106
+ def upsert_learning(
3107
+ self,
3108
+ id: str,
3109
+ learning_type: str,
3110
+ content: Dict[str, Any],
3111
+ user_id: Optional[str] = None,
3112
+ agent_id: Optional[str] = None,
3113
+ team_id: Optional[str] = None,
3114
+ workflow_id: Optional[str] = None,
3115
+ session_id: Optional[str] = None,
3116
+ namespace: Optional[str] = None,
3117
+ entity_id: Optional[str] = None,
3118
+ entity_type: Optional[str] = None,
3119
+ metadata: Optional[Dict[str, Any]] = None,
3120
+ ) -> None:
3121
+ """Insert or update a learning record.
3122
+
3123
+ Args:
3124
+ id: Unique identifier for the learning.
3125
+ learning_type: Type of learning ('user_profile', 'session_context', etc.)
3126
+ content: The learning content as a dict.
3127
+ user_id: Associated user ID.
3128
+ agent_id: Associated agent ID.
3129
+ team_id: Associated team ID.
3130
+ workflow_id: Associated workflow ID.
3131
+ session_id: Associated session ID.
3132
+ namespace: Namespace for scoping ('user', 'global', or custom).
3133
+ entity_id: Associated entity ID (for entity-specific learnings).
3134
+ entity_type: Entity type ('person', 'company', etc.).
3135
+ metadata: Optional metadata.
3136
+ """
3137
+ try:
3138
+ table = self._get_table(table_type="learnings", create_table_if_not_found=True)
3139
+ if table is None:
3140
+ return
3141
+
3142
+ current_time = int(time.time())
3143
+
3144
+ with self.Session() as sess, sess.begin():
3145
+ stmt = postgresql.insert(table).values(
3146
+ learning_id=id,
3147
+ learning_type=learning_type,
3148
+ namespace=namespace,
3149
+ user_id=user_id,
3150
+ agent_id=agent_id,
3151
+ team_id=team_id,
3152
+ workflow_id=workflow_id,
3153
+ session_id=session_id,
3154
+ entity_id=entity_id,
3155
+ entity_type=entity_type,
3156
+ content=content,
3157
+ metadata=metadata,
3158
+ created_at=current_time,
3159
+ updated_at=current_time,
3160
+ )
3161
+ stmt = stmt.on_conflict_do_update(
3162
+ index_elements=["learning_id"],
3163
+ set_=dict(
3164
+ content=content,
3165
+ metadata=metadata,
3166
+ updated_at=current_time,
3167
+ ),
3168
+ )
3169
+ sess.execute(stmt)
3170
+
3171
+ log_debug(f"Upserted learning: {id}")
3172
+
3173
+ except Exception as e:
3174
+ log_debug(f"Error upserting learning: {e}")
3175
+
3176
+ def delete_learning(self, id: str) -> bool:
3177
+ """Delete a learning record.
3178
+
3179
+ Args:
3180
+ id: The learning ID to delete.
3181
+
3182
+ Returns:
3183
+ True if deleted, False otherwise.
3184
+ """
3185
+ try:
3186
+ table = self._get_table(table_type="learnings")
3187
+ if table is None:
3188
+ return False
3189
+
3190
+ with self.Session() as sess, sess.begin():
3191
+ stmt = table.delete().where(table.c.learning_id == id)
3192
+ result = sess.execute(stmt)
3193
+ return result.rowcount > 0
3194
+
3195
+ except Exception as e:
3196
+ log_debug(f"Error deleting learning: {e}")
3197
+ return False
3198
+
3199
+ def get_learnings(
3200
+ self,
3201
+ learning_type: Optional[str] = None,
3202
+ user_id: Optional[str] = None,
3203
+ agent_id: Optional[str] = None,
3204
+ team_id: Optional[str] = None,
3205
+ workflow_id: Optional[str] = None,
3206
+ session_id: Optional[str] = None,
3207
+ namespace: Optional[str] = None,
3208
+ entity_id: Optional[str] = None,
3209
+ entity_type: Optional[str] = None,
3210
+ limit: Optional[int] = None,
3211
+ ) -> List[Dict[str, Any]]:
3212
+ """Get multiple learning records.
3213
+
3214
+ Args:
3215
+ learning_type: Filter by learning type.
3216
+ user_id: Filter by user ID.
3217
+ agent_id: Filter by agent ID.
3218
+ team_id: Filter by team ID.
3219
+ workflow_id: Filter by workflow ID.
3220
+ session_id: Filter by session ID.
3221
+ namespace: Filter by namespace ('user', 'global', or custom).
3222
+ entity_id: Filter by entity ID (for entity-specific learnings).
3223
+ entity_type: Filter by entity type ('person', 'company', etc.).
3224
+ limit: Maximum number of records to return.
3225
+
3226
+ Returns:
3227
+ List of learning records.
3228
+ """
3229
+ try:
3230
+ table = self._get_table(table_type="learnings")
3231
+ if table is None:
3232
+ return []
3233
+
3234
+ with self.Session() as sess:
3235
+ stmt = select(table)
3236
+
3237
+ if learning_type is not None:
3238
+ stmt = stmt.where(table.c.learning_type == learning_type)
3239
+ if user_id is not None:
3240
+ stmt = stmt.where(table.c.user_id == user_id)
3241
+ if agent_id is not None:
3242
+ stmt = stmt.where(table.c.agent_id == agent_id)
3243
+ if team_id is not None:
3244
+ stmt = stmt.where(table.c.team_id == team_id)
3245
+ if workflow_id is not None:
3246
+ stmt = stmt.where(table.c.workflow_id == workflow_id)
3247
+ if session_id is not None:
3248
+ stmt = stmt.where(table.c.session_id == session_id)
3249
+ if namespace is not None:
3250
+ stmt = stmt.where(table.c.namespace == namespace)
3251
+ if entity_id is not None:
3252
+ stmt = stmt.where(table.c.entity_id == entity_id)
3253
+ if entity_type is not None:
3254
+ stmt = stmt.where(table.c.entity_type == entity_type)
3255
+
3256
+ stmt = stmt.order_by(table.c.updated_at.desc())
3257
+
3258
+ if limit is not None:
3259
+ stmt = stmt.limit(limit)
3260
+
3261
+ result = sess.execute(stmt).fetchall()
3262
+ return [dict(row._mapping) for row in result]
3263
+
3264
+ except Exception as e:
3265
+ log_debug(f"Error getting learnings: {e}")
3266
+ return []
@@ -169,6 +169,24 @@ def _get_span_table_schema(traces_table_name: str = "agno_traces", db_schema: st
169
169
  }
170
170
 
171
171
 
172
+ LEARNINGS_TABLE_SCHEMA = {
173
+ "learning_id": {"type": String, "primary_key": True, "nullable": False},
174
+ "learning_type": {"type": String, "nullable": False, "index": True},
175
+ "namespace": {"type": String, "nullable": True, "index": True},
176
+ "user_id": {"type": String, "nullable": True, "index": True},
177
+ "agent_id": {"type": String, "nullable": True, "index": True},
178
+ "team_id": {"type": String, "nullable": True, "index": True},
179
+ "workflow_id": {"type": String, "nullable": True, "index": True},
180
+ "session_id": {"type": String, "nullable": True, "index": True},
181
+ "entity_id": {"type": String, "nullable": True, "index": True},
182
+ "entity_type": {"type": String, "nullable": True, "index": True},
183
+ "content": {"type": JSONB, "nullable": False},
184
+ "metadata": {"type": JSONB, "nullable": True},
185
+ "created_at": {"type": BigInteger, "nullable": False, "index": True},
186
+ "updated_at": {"type": BigInteger, "nullable": True},
187
+ }
188
+
189
+
172
190
  def get_table_schema_definition(
173
191
  table_type: str, traces_table_name: str = "agno_traces", db_schema: str = "agno"
174
192
  ) -> dict[str, Any]:
@@ -196,6 +214,7 @@ def get_table_schema_definition(
196
214
  "culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
197
215
  "versions": VERSIONS_TABLE_SCHEMA,
198
216
  "traces": TRACE_TABLE_SCHEMA,
217
+ "learnings": LEARNINGS_TABLE_SCHEMA,
199
218
  }
200
219
 
201
220
  schema = schemas.get(table_type, {})
agno/db/redis/redis.py CHANGED
@@ -2139,3 +2139,50 @@ class RedisDb(BaseDb):
2139
2139
  except Exception as e:
2140
2140
  log_error(f"Error getting spans: {e}")
2141
2141
  return []
2142
+
2143
+ # -- Learning methods (stubs) --
2144
+ def get_learning(
2145
+ self,
2146
+ learning_type: str,
2147
+ user_id: Optional[str] = None,
2148
+ agent_id: Optional[str] = None,
2149
+ team_id: Optional[str] = None,
2150
+ session_id: Optional[str] = None,
2151
+ namespace: Optional[str] = None,
2152
+ entity_id: Optional[str] = None,
2153
+ entity_type: Optional[str] = None,
2154
+ ) -> Optional[Dict[str, Any]]:
2155
+ raise NotImplementedError("Learning methods not yet implemented for RedisDb")
2156
+
2157
+ def upsert_learning(
2158
+ self,
2159
+ id: str,
2160
+ learning_type: str,
2161
+ content: Dict[str, Any],
2162
+ user_id: Optional[str] = None,
2163
+ agent_id: Optional[str] = None,
2164
+ team_id: Optional[str] = None,
2165
+ session_id: Optional[str] = None,
2166
+ namespace: Optional[str] = None,
2167
+ entity_id: Optional[str] = None,
2168
+ entity_type: Optional[str] = None,
2169
+ metadata: Optional[Dict[str, Any]] = None,
2170
+ ) -> None:
2171
+ raise NotImplementedError("Learning methods not yet implemented for RedisDb")
2172
+
2173
+ def delete_learning(self, id: str) -> bool:
2174
+ raise NotImplementedError("Learning methods not yet implemented for RedisDb")
2175
+
2176
+ def get_learnings(
2177
+ self,
2178
+ learning_type: Optional[str] = None,
2179
+ user_id: Optional[str] = None,
2180
+ agent_id: Optional[str] = None,
2181
+ team_id: Optional[str] = None,
2182
+ session_id: Optional[str] = None,
2183
+ namespace: Optional[str] = None,
2184
+ entity_id: Optional[str] = None,
2185
+ entity_type: Optional[str] = None,
2186
+ limit: Optional[int] = None,
2187
+ ) -> List[Dict[str, Any]]:
2188
+ raise NotImplementedError("Learning methods not yet implemented for RedisDb")
@@ -2886,3 +2886,50 @@ class SingleStoreDb(BaseDb):
2886
2886
  except Exception as e:
2887
2887
  log_error(f"Error getting spans: {e}")
2888
2888
  return []
2889
+
2890
+ # -- Learning methods (stubs) --
2891
+ def get_learning(
2892
+ self,
2893
+ learning_type: str,
2894
+ user_id: Optional[str] = None,
2895
+ agent_id: Optional[str] = None,
2896
+ team_id: Optional[str] = None,
2897
+ session_id: Optional[str] = None,
2898
+ namespace: Optional[str] = None,
2899
+ entity_id: Optional[str] = None,
2900
+ entity_type: Optional[str] = None,
2901
+ ) -> Optional[Dict[str, Any]]:
2902
+ raise NotImplementedError("Learning methods not yet implemented for SingleStoreDb")
2903
+
2904
+ def upsert_learning(
2905
+ self,
2906
+ id: str,
2907
+ learning_type: str,
2908
+ content: Dict[str, Any],
2909
+ user_id: Optional[str] = None,
2910
+ agent_id: Optional[str] = None,
2911
+ team_id: Optional[str] = None,
2912
+ session_id: Optional[str] = None,
2913
+ namespace: Optional[str] = None,
2914
+ entity_id: Optional[str] = None,
2915
+ entity_type: Optional[str] = None,
2916
+ metadata: Optional[Dict[str, Any]] = None,
2917
+ ) -> None:
2918
+ raise NotImplementedError("Learning methods not yet implemented for SingleStoreDb")
2919
+
2920
+ def delete_learning(self, id: str) -> bool:
2921
+ raise NotImplementedError("Learning methods not yet implemented for SingleStoreDb")
2922
+
2923
+ def get_learnings(
2924
+ self,
2925
+ learning_type: Optional[str] = None,
2926
+ user_id: Optional[str] = None,
2927
+ agent_id: Optional[str] = None,
2928
+ team_id: Optional[str] = None,
2929
+ session_id: Optional[str] = None,
2930
+ namespace: Optional[str] = None,
2931
+ entity_id: Optional[str] = None,
2932
+ entity_type: Optional[str] = None,
2933
+ limit: Optional[int] = None,
2934
+ ) -> List[Dict[str, Any]]:
2935
+ raise NotImplementedError("Learning methods not yet implemented for SingleStoreDb")