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
@@ -54,6 +54,7 @@ class AsyncSqliteDb(AsyncBaseDb):
54
54
  traces_table: Optional[str] = None,
55
55
  spans_table: Optional[str] = None,
56
56
  versions_table: Optional[str] = None,
57
+ learnings_table: Optional[str] = None,
57
58
  id: Optional[str] = None,
58
59
  ):
59
60
  """
@@ -78,6 +79,7 @@ class AsyncSqliteDb(AsyncBaseDb):
78
79
  traces_table (Optional[str]): Name of the table to store run traces.
79
80
  spans_table (Optional[str]): Name of the table to store span events.
80
81
  versions_table (Optional[str]): Name of the table to store schema versions.
82
+ learnings_table (Optional[str]): Name of the table to store learning records.
81
83
  id (Optional[str]): ID of the database.
82
84
 
83
85
  Raises:
@@ -98,6 +100,7 @@ class AsyncSqliteDb(AsyncBaseDb):
98
100
  traces_table=traces_table,
99
101
  spans_table=spans_table,
100
102
  versions_table=versions_table,
103
+ learnings_table=learnings_table,
101
104
  )
102
105
 
103
106
  _engine: Optional[AsyncEngine] = db_engine
@@ -155,6 +158,7 @@ class AsyncSqliteDb(AsyncBaseDb):
155
158
  (self.eval_table_name, "evals"),
156
159
  (self.knowledge_table_name, "knowledge"),
157
160
  (self.versions_table_name, "versions"),
161
+ (self.learnings_table_name, "learnings"),
158
162
  ]
159
163
 
160
164
  for table_name, table_type in tables_to_create:
@@ -341,6 +345,15 @@ class AsyncSqliteDb(AsyncBaseDb):
341
345
  )
342
346
  return self.spans_table
343
347
 
348
+ elif table_type == "learnings":
349
+ if not hasattr(self, "learnings_table"):
350
+ self.learnings_table = await self._get_or_create_table(
351
+ table_name=self.learnings_table_name,
352
+ table_type="learnings",
353
+ create_table_if_not_found=create_table_if_not_found,
354
+ )
355
+ return self.learnings_table
356
+
344
357
  else:
345
358
  raise ValueError(f"Unknown table type: '{table_type}'")
346
359
 
@@ -2923,3 +2936,232 @@ class AsyncSqliteDb(AsyncBaseDb):
2923
2936
  except Exception as e:
2924
2937
  log_error(f"Error getting spans: {e}")
2925
2938
  return []
2939
+
2940
+ # -- Learning methods --
2941
+ async def get_learning(
2942
+ self,
2943
+ learning_type: str,
2944
+ user_id: Optional[str] = None,
2945
+ agent_id: Optional[str] = None,
2946
+ team_id: Optional[str] = None,
2947
+ workflow_id: Optional[str] = None,
2948
+ session_id: Optional[str] = None,
2949
+ namespace: Optional[str] = None,
2950
+ entity_id: Optional[str] = None,
2951
+ entity_type: Optional[str] = None,
2952
+ ) -> Optional[Dict[str, Any]]:
2953
+ """Retrieve a learning record.
2954
+
2955
+ Args:
2956
+ learning_type: Type of learning ('user_profile', 'session_context', etc.)
2957
+ user_id: Filter by user ID.
2958
+ agent_id: Filter by agent ID.
2959
+ team_id: Filter by team ID.
2960
+ workflow_id: Filter by workflow ID.
2961
+ session_id: Filter by session ID.
2962
+ namespace: Filter by namespace ('user', 'global', or custom).
2963
+ entity_id: Filter by entity ID (for entity-specific learnings).
2964
+ entity_type: Filter by entity type ('person', 'company', etc.).
2965
+
2966
+ Returns:
2967
+ Dict with 'content' key containing the learning data, or None.
2968
+ """
2969
+ try:
2970
+ table = await self._get_table(table_type="learnings")
2971
+ if table is None:
2972
+ return None
2973
+
2974
+ async with self.async_session_factory() as sess:
2975
+ stmt = select(table).where(table.c.learning_type == learning_type)
2976
+
2977
+ if user_id is not None:
2978
+ stmt = stmt.where(table.c.user_id == user_id)
2979
+ if agent_id is not None:
2980
+ stmt = stmt.where(table.c.agent_id == agent_id)
2981
+ if team_id is not None:
2982
+ stmt = stmt.where(table.c.team_id == team_id)
2983
+ if workflow_id is not None:
2984
+ stmt = stmt.where(table.c.workflow_id == workflow_id)
2985
+ if session_id is not None:
2986
+ stmt = stmt.where(table.c.session_id == session_id)
2987
+ if namespace is not None:
2988
+ stmt = stmt.where(table.c.namespace == namespace)
2989
+ if entity_id is not None:
2990
+ stmt = stmt.where(table.c.entity_id == entity_id)
2991
+ if entity_type is not None:
2992
+ stmt = stmt.where(table.c.entity_type == entity_type)
2993
+
2994
+ result = await sess.execute(stmt)
2995
+ row = result.fetchone()
2996
+ if row is None:
2997
+ return None
2998
+
2999
+ row_dict = dict(row._mapping)
3000
+ return {"content": row_dict.get("content")}
3001
+
3002
+ except Exception as e:
3003
+ log_debug(f"Error retrieving learning: {e}")
3004
+ return None
3005
+
3006
+ async def upsert_learning(
3007
+ self,
3008
+ id: str,
3009
+ learning_type: str,
3010
+ content: Dict[str, Any],
3011
+ user_id: Optional[str] = None,
3012
+ agent_id: Optional[str] = None,
3013
+ team_id: Optional[str] = None,
3014
+ workflow_id: Optional[str] = None,
3015
+ session_id: Optional[str] = None,
3016
+ namespace: Optional[str] = None,
3017
+ entity_id: Optional[str] = None,
3018
+ entity_type: Optional[str] = None,
3019
+ metadata: Optional[Dict[str, Any]] = None,
3020
+ ) -> None:
3021
+ """Insert or update a learning record.
3022
+
3023
+ Args:
3024
+ id: Unique identifier for the learning.
3025
+ learning_type: Type of learning ('user_profile', 'session_context', etc.)
3026
+ content: The learning content as a dict.
3027
+ user_id: Associated user ID.
3028
+ agent_id: Associated agent ID.
3029
+ team_id: Associated team ID.
3030
+ workflow_id: Associated workflow ID.
3031
+ session_id: Associated session ID.
3032
+ namespace: Namespace for scoping ('user', 'global', or custom).
3033
+ entity_id: Associated entity ID (for entity-specific learnings).
3034
+ entity_type: Entity type ('person', 'company', etc.).
3035
+ metadata: Optional metadata.
3036
+ """
3037
+ try:
3038
+ table = await self._get_table(table_type="learnings", create_table_if_not_found=True)
3039
+ if table is None:
3040
+ return
3041
+
3042
+ current_time = int(time.time())
3043
+
3044
+ async with self.async_session_factory() as sess, sess.begin():
3045
+ stmt = sqlite.insert(table).values(
3046
+ learning_id=id,
3047
+ learning_type=learning_type,
3048
+ namespace=namespace,
3049
+ user_id=user_id,
3050
+ agent_id=agent_id,
3051
+ team_id=team_id,
3052
+ workflow_id=workflow_id,
3053
+ session_id=session_id,
3054
+ entity_id=entity_id,
3055
+ entity_type=entity_type,
3056
+ content=content,
3057
+ metadata=metadata,
3058
+ created_at=current_time,
3059
+ updated_at=current_time,
3060
+ )
3061
+ stmt = stmt.on_conflict_do_update(
3062
+ index_elements=["learning_id"],
3063
+ set_=dict(
3064
+ content=content,
3065
+ metadata=metadata,
3066
+ updated_at=current_time,
3067
+ ),
3068
+ )
3069
+ await sess.execute(stmt)
3070
+
3071
+ log_debug(f"Upserted learning: {id}")
3072
+
3073
+ except Exception as e:
3074
+ log_debug(f"Error upserting learning: {e}")
3075
+
3076
+ async def delete_learning(self, id: str) -> bool:
3077
+ """Delete a learning record.
3078
+
3079
+ Args:
3080
+ id: The learning ID to delete.
3081
+
3082
+ Returns:
3083
+ True if deleted, False otherwise.
3084
+ """
3085
+ try:
3086
+ table = await self._get_table(table_type="learnings")
3087
+ if table is None:
3088
+ return False
3089
+
3090
+ async with self.async_session_factory() as sess, sess.begin():
3091
+ stmt = table.delete().where(table.c.learning_id == id)
3092
+ result = await sess.execute(stmt)
3093
+ return result.rowcount > 0
3094
+
3095
+ except Exception as e:
3096
+ log_debug(f"Error deleting learning: {e}")
3097
+ return False
3098
+
3099
+ async def get_learnings(
3100
+ self,
3101
+ learning_type: Optional[str] = None,
3102
+ user_id: Optional[str] = None,
3103
+ agent_id: Optional[str] = None,
3104
+ team_id: Optional[str] = None,
3105
+ workflow_id: Optional[str] = None,
3106
+ session_id: Optional[str] = None,
3107
+ namespace: Optional[str] = None,
3108
+ entity_id: Optional[str] = None,
3109
+ entity_type: Optional[str] = None,
3110
+ limit: Optional[int] = None,
3111
+ ) -> List[Dict[str, Any]]:
3112
+ """Get multiple learning records.
3113
+
3114
+ Args:
3115
+ learning_type: Filter by learning type.
3116
+ user_id: Filter by user ID.
3117
+ agent_id: Filter by agent ID.
3118
+ team_id: Filter by team ID.
3119
+ workflow_id: Filter by workflow ID.
3120
+ session_id: Filter by session ID.
3121
+ namespace: Filter by namespace ('user', 'global', or custom).
3122
+ entity_id: Filter by entity ID (for entity-specific learnings).
3123
+ entity_type: Filter by entity type ('person', 'company', etc.).
3124
+ limit: Maximum number of records to return.
3125
+
3126
+ Returns:
3127
+ List of learning records.
3128
+ """
3129
+ try:
3130
+ table = await self._get_table(table_type="learnings")
3131
+ if table is None:
3132
+ return []
3133
+
3134
+ async with self.async_session_factory() as sess:
3135
+ stmt = select(table)
3136
+
3137
+ if learning_type is not None:
3138
+ stmt = stmt.where(table.c.learning_type == learning_type)
3139
+ if user_id is not None:
3140
+ stmt = stmt.where(table.c.user_id == user_id)
3141
+ if agent_id is not None:
3142
+ stmt = stmt.where(table.c.agent_id == agent_id)
3143
+ if team_id is not None:
3144
+ stmt = stmt.where(table.c.team_id == team_id)
3145
+ if workflow_id is not None:
3146
+ stmt = stmt.where(table.c.workflow_id == workflow_id)
3147
+ if session_id is not None:
3148
+ stmt = stmt.where(table.c.session_id == session_id)
3149
+ if namespace is not None:
3150
+ stmt = stmt.where(table.c.namespace == namespace)
3151
+ if entity_id is not None:
3152
+ stmt = stmt.where(table.c.entity_id == entity_id)
3153
+ if entity_type is not None:
3154
+ stmt = stmt.where(table.c.entity_type == entity_type)
3155
+
3156
+ stmt = stmt.order_by(table.c.updated_at.desc())
3157
+
3158
+ if limit is not None:
3159
+ stmt = stmt.limit(limit)
3160
+
3161
+ result = await sess.execute(stmt)
3162
+ results = result.fetchall()
3163
+ return [dict(row._mapping) for row in results]
3164
+
3165
+ except Exception as e:
3166
+ log_debug(f"Error getting learnings: {e}")
3167
+ return []
agno/db/sqlite/schemas.py CHANGED
@@ -162,6 +162,23 @@ VERSIONS_TABLE_SCHEMA = {
162
162
  "updated_at": {"type": String, "nullable": True},
163
163
  }
164
164
 
165
+ LEARNINGS_TABLE_SCHEMA = {
166
+ "learning_id": {"type": String, "primary_key": True, "nullable": False},
167
+ "learning_type": {"type": String, "nullable": False, "index": True},
168
+ "namespace": {"type": String, "nullable": True, "index": True},
169
+ "user_id": {"type": String, "nullable": True, "index": True},
170
+ "agent_id": {"type": String, "nullable": True, "index": True},
171
+ "team_id": {"type": String, "nullable": True, "index": True},
172
+ "workflow_id": {"type": String, "nullable": True, "index": True},
173
+ "session_id": {"type": String, "nullable": True, "index": True},
174
+ "entity_id": {"type": String, "nullable": True, "index": True},
175
+ "entity_type": {"type": String, "nullable": True, "index": True},
176
+ "content": {"type": JSON, "nullable": False},
177
+ "metadata": {"type": JSON, "nullable": True},
178
+ "created_at": {"type": BigInteger, "nullable": False, "index": True},
179
+ "updated_at": {"type": BigInteger, "nullable": True},
180
+ }
181
+
165
182
 
166
183
  def get_table_schema_definition(table_type: str, traces_table_name: str = "agno_traces") -> dict[str, Any]:
167
184
  """
@@ -187,6 +204,7 @@ def get_table_schema_definition(table_type: str, traces_table_name: str = "agno_
187
204
  "traces": TRACE_TABLE_SCHEMA,
188
205
  "culture": CULTURAL_KNOWLEDGE_TABLE_SCHEMA,
189
206
  "versions": VERSIONS_TABLE_SCHEMA,
207
+ "learnings": LEARNINGS_TABLE_SCHEMA,
190
208
  }
191
209
  schema = schemas.get(table_type, {})
192
210
 
agno/db/sqlite/sqlite.py CHANGED
@@ -55,6 +55,7 @@ class SqliteDb(BaseDb):
55
55
  traces_table: Optional[str] = None,
56
56
  spans_table: Optional[str] = None,
57
57
  versions_table: Optional[str] = None,
58
+ learnings_table: Optional[str] = None,
58
59
  id: Optional[str] = None,
59
60
  ):
60
61
  """
@@ -79,6 +80,7 @@ class SqliteDb(BaseDb):
79
80
  traces_table (Optional[str]): Name of the table to store run traces.
80
81
  spans_table (Optional[str]): Name of the table to store span events.
81
82
  versions_table (Optional[str]): Name of the table to store schema versions.
83
+ learnings_table (Optional[str]): Name of the table to store learning records.
82
84
  id (Optional[str]): ID of the database.
83
85
 
84
86
  Raises:
@@ -99,6 +101,7 @@ class SqliteDb(BaseDb):
99
101
  traces_table=traces_table,
100
102
  spans_table=spans_table,
101
103
  versions_table=versions_table,
104
+ learnings_table=learnings_table,
102
105
  )
103
106
 
104
107
  _engine: Optional[Engine] = db_engine
@@ -156,6 +159,7 @@ class SqliteDb(BaseDb):
156
159
  (self.eval_table_name, "evals"),
157
160
  (self.knowledge_table_name, "knowledge"),
158
161
  (self.versions_table_name, "versions"),
162
+ (self.learnings_table_name, "learnings"),
159
163
  ]
160
164
 
161
165
  for table_name, table_type in tables_to_create:
@@ -334,6 +338,14 @@ class SqliteDb(BaseDb):
334
338
  )
335
339
  return self.versions_table
336
340
 
341
+ elif table_type == "learnings":
342
+ self.learnings_table = self._get_or_create_table(
343
+ table_name=self.learnings_table_name,
344
+ table_type="learnings",
345
+ create_table_if_not_found=create_table_if_not_found,
346
+ )
347
+ return self.learnings_table
348
+
337
349
  else:
338
350
  raise ValueError(f"Unknown table type: '{table_type}'")
339
351
 
@@ -2912,3 +2924,230 @@ class SqliteDb(BaseDb):
2912
2924
  except Exception as e:
2913
2925
  log_error(f"Error upserting cultural knowledge: {e}")
2914
2926
  raise e
2927
+
2928
+ # -- Learning methods --
2929
+ def get_learning(
2930
+ self,
2931
+ learning_type: str,
2932
+ user_id: Optional[str] = None,
2933
+ agent_id: Optional[str] = None,
2934
+ team_id: Optional[str] = None,
2935
+ workflow_id: Optional[str] = None,
2936
+ session_id: Optional[str] = None,
2937
+ namespace: Optional[str] = None,
2938
+ entity_id: Optional[str] = None,
2939
+ entity_type: Optional[str] = None,
2940
+ ) -> Optional[Dict[str, Any]]:
2941
+ """Retrieve a learning record.
2942
+
2943
+ Args:
2944
+ learning_type: Type of learning ('user_profile', 'session_context', etc.)
2945
+ user_id: Filter by user ID.
2946
+ agent_id: Filter by agent ID.
2947
+ team_id: Filter by team ID.
2948
+ workflow_id: Filter by workflow ID.
2949
+ session_id: Filter by session ID.
2950
+ namespace: Filter by namespace ('user', 'global', or custom).
2951
+ entity_id: Filter by entity ID (for entity-specific learnings).
2952
+ entity_type: Filter by entity type ('person', 'company', etc.).
2953
+
2954
+ Returns:
2955
+ Dict with 'content' key containing the learning data, or None.
2956
+ """
2957
+ try:
2958
+ table = self._get_table(table_type="learnings")
2959
+ if table is None:
2960
+ return None
2961
+
2962
+ with self.Session() as sess:
2963
+ stmt = select(table).where(table.c.learning_type == learning_type)
2964
+
2965
+ if user_id is not None:
2966
+ stmt = stmt.where(table.c.user_id == user_id)
2967
+ if agent_id is not None:
2968
+ stmt = stmt.where(table.c.agent_id == agent_id)
2969
+ if team_id is not None:
2970
+ stmt = stmt.where(table.c.team_id == team_id)
2971
+ if workflow_id is not None:
2972
+ stmt = stmt.where(table.c.workflow_id == workflow_id)
2973
+ if session_id is not None:
2974
+ stmt = stmt.where(table.c.session_id == session_id)
2975
+ if namespace is not None:
2976
+ stmt = stmt.where(table.c.namespace == namespace)
2977
+ if entity_id is not None:
2978
+ stmt = stmt.where(table.c.entity_id == entity_id)
2979
+ if entity_type is not None:
2980
+ stmt = stmt.where(table.c.entity_type == entity_type)
2981
+
2982
+ result = sess.execute(stmt).fetchone()
2983
+ if result is None:
2984
+ return None
2985
+
2986
+ row = dict(result._mapping)
2987
+ return {"content": row.get("content")}
2988
+
2989
+ except Exception as e:
2990
+ log_debug(f"Error retrieving learning: {e}")
2991
+ return None
2992
+
2993
+ def upsert_learning(
2994
+ self,
2995
+ id: str,
2996
+ learning_type: str,
2997
+ content: Dict[str, Any],
2998
+ user_id: Optional[str] = None,
2999
+ agent_id: Optional[str] = None,
3000
+ team_id: Optional[str] = None,
3001
+ workflow_id: Optional[str] = None,
3002
+ session_id: Optional[str] = None,
3003
+ namespace: Optional[str] = None,
3004
+ entity_id: Optional[str] = None,
3005
+ entity_type: Optional[str] = None,
3006
+ metadata: Optional[Dict[str, Any]] = None,
3007
+ ) -> None:
3008
+ """Insert or update a learning record.
3009
+
3010
+ Args:
3011
+ id: Unique identifier for the learning.
3012
+ learning_type: Type of learning ('user_profile', 'session_context', etc.)
3013
+ content: The learning content as a dict.
3014
+ user_id: Associated user ID.
3015
+ agent_id: Associated agent ID.
3016
+ team_id: Associated team ID.
3017
+ workflow_id: Associated workflow ID.
3018
+ session_id: Associated session ID.
3019
+ namespace: Namespace for scoping ('user', 'global', or custom).
3020
+ entity_id: Associated entity ID (for entity-specific learnings).
3021
+ entity_type: Entity type ('person', 'company', etc.).
3022
+ metadata: Optional metadata.
3023
+ """
3024
+ try:
3025
+ table = self._get_table(table_type="learnings", create_table_if_not_found=True)
3026
+ if table is None:
3027
+ return
3028
+
3029
+ current_time = int(time.time())
3030
+
3031
+ with self.Session() as sess, sess.begin():
3032
+ stmt = sqlite.insert(table).values(
3033
+ learning_id=id,
3034
+ learning_type=learning_type,
3035
+ namespace=namespace,
3036
+ user_id=user_id,
3037
+ agent_id=agent_id,
3038
+ team_id=team_id,
3039
+ workflow_id=workflow_id,
3040
+ session_id=session_id,
3041
+ entity_id=entity_id,
3042
+ entity_type=entity_type,
3043
+ content=content,
3044
+ metadata=metadata,
3045
+ created_at=current_time,
3046
+ updated_at=current_time,
3047
+ )
3048
+ stmt = stmt.on_conflict_do_update(
3049
+ index_elements=["learning_id"],
3050
+ set_=dict(
3051
+ content=content,
3052
+ metadata=metadata,
3053
+ updated_at=current_time,
3054
+ ),
3055
+ )
3056
+ sess.execute(stmt)
3057
+
3058
+ log_debug(f"Upserted learning: {id}")
3059
+
3060
+ except Exception as e:
3061
+ log_debug(f"Error upserting learning: {e}")
3062
+
3063
+ def delete_learning(self, id: str) -> bool:
3064
+ """Delete a learning record.
3065
+
3066
+ Args:
3067
+ id: The learning ID to delete.
3068
+
3069
+ Returns:
3070
+ True if deleted, False otherwise.
3071
+ """
3072
+ try:
3073
+ table = self._get_table(table_type="learnings")
3074
+ if table is None:
3075
+ return False
3076
+
3077
+ with self.Session() as sess, sess.begin():
3078
+ stmt = table.delete().where(table.c.learning_id == id)
3079
+ result = sess.execute(stmt)
3080
+ return result.rowcount > 0
3081
+
3082
+ except Exception as e:
3083
+ log_debug(f"Error deleting learning: {e}")
3084
+ return False
3085
+
3086
+ def get_learnings(
3087
+ self,
3088
+ learning_type: Optional[str] = None,
3089
+ user_id: Optional[str] = None,
3090
+ agent_id: Optional[str] = None,
3091
+ team_id: Optional[str] = None,
3092
+ workflow_id: Optional[str] = None,
3093
+ session_id: Optional[str] = None,
3094
+ namespace: Optional[str] = None,
3095
+ entity_id: Optional[str] = None,
3096
+ entity_type: Optional[str] = None,
3097
+ limit: Optional[int] = None,
3098
+ ) -> List[Dict[str, Any]]:
3099
+ """Get multiple learning records.
3100
+
3101
+ Args:
3102
+ learning_type: Filter by learning type.
3103
+ user_id: Filter by user ID.
3104
+ agent_id: Filter by agent ID.
3105
+ team_id: Filter by team ID.
3106
+ workflow_id: Filter by workflow ID.
3107
+ session_id: Filter by session ID.
3108
+ namespace: Filter by namespace ('user', 'global', or custom).
3109
+ entity_id: Filter by entity ID (for entity-specific learnings).
3110
+ entity_type: Filter by entity type ('person', 'company', etc.).
3111
+ limit: Maximum number of records to return.
3112
+
3113
+ Returns:
3114
+ List of learning records.
3115
+ """
3116
+ try:
3117
+ table = self._get_table(table_type="learnings")
3118
+ if table is None:
3119
+ return []
3120
+
3121
+ with self.Session() as sess:
3122
+ stmt = select(table)
3123
+
3124
+ if learning_type is not None:
3125
+ stmt = stmt.where(table.c.learning_type == learning_type)
3126
+ if user_id is not None:
3127
+ stmt = stmt.where(table.c.user_id == user_id)
3128
+ if agent_id is not None:
3129
+ stmt = stmt.where(table.c.agent_id == agent_id)
3130
+ if team_id is not None:
3131
+ stmt = stmt.where(table.c.team_id == team_id)
3132
+ if workflow_id is not None:
3133
+ stmt = stmt.where(table.c.workflow_id == workflow_id)
3134
+ if session_id is not None:
3135
+ stmt = stmt.where(table.c.session_id == session_id)
3136
+ if namespace is not None:
3137
+ stmt = stmt.where(table.c.namespace == namespace)
3138
+ if entity_id is not None:
3139
+ stmt = stmt.where(table.c.entity_id == entity_id)
3140
+ if entity_type is not None:
3141
+ stmt = stmt.where(table.c.entity_type == entity_type)
3142
+
3143
+ stmt = stmt.order_by(table.c.updated_at.desc())
3144
+
3145
+ if limit is not None:
3146
+ stmt = stmt.limit(limit)
3147
+
3148
+ results = sess.execute(stmt).fetchall()
3149
+ return [dict(row._mapping) for row in results]
3150
+
3151
+ except Exception as e:
3152
+ log_debug(f"Error getting learnings: {e}")
3153
+ return []
@@ -1906,3 +1906,50 @@ class SurrealDb(BaseDb):
1906
1906
  span_data[field] = span_data[field].isoformat()
1907
1907
 
1908
1908
  return Span.from_dict(span_data)
1909
+
1910
+ # -- Learning methods (stubs) --
1911
+ def get_learning(
1912
+ self,
1913
+ learning_type: str,
1914
+ user_id: Optional[str] = None,
1915
+ agent_id: Optional[str] = None,
1916
+ team_id: Optional[str] = None,
1917
+ session_id: Optional[str] = None,
1918
+ namespace: Optional[str] = None,
1919
+ entity_id: Optional[str] = None,
1920
+ entity_type: Optional[str] = None,
1921
+ ) -> Optional[Dict[str, Any]]:
1922
+ raise NotImplementedError("Learning methods not yet implemented for SurrealDb")
1923
+
1924
+ def upsert_learning(
1925
+ self,
1926
+ id: str,
1927
+ learning_type: str,
1928
+ content: Dict[str, Any],
1929
+ user_id: Optional[str] = None,
1930
+ agent_id: Optional[str] = None,
1931
+ team_id: Optional[str] = None,
1932
+ session_id: Optional[str] = None,
1933
+ namespace: Optional[str] = None,
1934
+ entity_id: Optional[str] = None,
1935
+ entity_type: Optional[str] = None,
1936
+ metadata: Optional[Dict[str, Any]] = None,
1937
+ ) -> None:
1938
+ raise NotImplementedError("Learning methods not yet implemented for SurrealDb")
1939
+
1940
+ def delete_learning(self, id: str) -> bool:
1941
+ raise NotImplementedError("Learning methods not yet implemented for SurrealDb")
1942
+
1943
+ def get_learnings(
1944
+ self,
1945
+ learning_type: Optional[str] = None,
1946
+ user_id: Optional[str] = None,
1947
+ agent_id: Optional[str] = None,
1948
+ team_id: Optional[str] = None,
1949
+ session_id: Optional[str] = None,
1950
+ namespace: Optional[str] = None,
1951
+ entity_id: Optional[str] = None,
1952
+ entity_type: Optional[str] = None,
1953
+ limit: Optional[int] = None,
1954
+ ) -> List[Dict[str, Any]]:
1955
+ raise NotImplementedError("Learning methods not yet implemented for SurrealDb")