agno 2.2.0__py3-none-any.whl → 2.2.2__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 (71) hide show
  1. agno/agent/agent.py +751 -575
  2. agno/culture/manager.py +22 -24
  3. agno/db/async_postgres/__init__.py +1 -1
  4. agno/db/dynamo/dynamo.py +0 -2
  5. agno/db/firestore/firestore.py +0 -2
  6. agno/db/gcs_json/gcs_json_db.py +0 -4
  7. agno/db/gcs_json/utils.py +0 -24
  8. agno/db/in_memory/in_memory_db.py +0 -3
  9. agno/db/json/json_db.py +4 -10
  10. agno/db/json/utils.py +0 -24
  11. agno/db/mongo/mongo.py +0 -2
  12. agno/db/mysql/mysql.py +0 -3
  13. agno/db/postgres/__init__.py +1 -1
  14. agno/db/{async_postgres → postgres}/async_postgres.py +19 -22
  15. agno/db/postgres/postgres.py +7 -10
  16. agno/db/postgres/utils.py +106 -2
  17. agno/db/redis/redis.py +0 -2
  18. agno/db/singlestore/singlestore.py +0 -3
  19. agno/db/sqlite/__init__.py +2 -1
  20. agno/db/sqlite/async_sqlite.py +2269 -0
  21. agno/db/sqlite/sqlite.py +0 -2
  22. agno/db/sqlite/utils.py +96 -0
  23. agno/db/surrealdb/surrealdb.py +0 -6
  24. agno/knowledge/knowledge.py +14 -3
  25. agno/knowledge/reader/pptx_reader.py +101 -0
  26. agno/knowledge/reader/reader_factory.py +30 -0
  27. agno/knowledge/reader/tavily_reader.py +194 -0
  28. agno/knowledge/types.py +1 -0
  29. agno/memory/manager.py +28 -25
  30. agno/models/anthropic/claude.py +63 -6
  31. agno/models/base.py +255 -36
  32. agno/models/response.py +69 -0
  33. agno/os/router.py +7 -5
  34. agno/os/routers/memory/memory.py +2 -1
  35. agno/os/routers/memory/schemas.py +5 -2
  36. agno/os/schema.py +26 -20
  37. agno/os/utils.py +9 -2
  38. agno/run/agent.py +28 -30
  39. agno/run/base.py +17 -1
  40. agno/run/team.py +28 -29
  41. agno/run/workflow.py +32 -17
  42. agno/session/agent.py +3 -0
  43. agno/session/summary.py +4 -1
  44. agno/session/team.py +1 -1
  45. agno/team/team.py +620 -374
  46. agno/tools/dalle.py +2 -4
  47. agno/tools/eleven_labs.py +23 -25
  48. agno/tools/function.py +40 -0
  49. agno/tools/mcp/__init__.py +10 -0
  50. agno/tools/mcp/mcp.py +324 -0
  51. agno/tools/mcp/multi_mcp.py +347 -0
  52. agno/tools/mcp/params.py +24 -0
  53. agno/tools/slack.py +18 -3
  54. agno/tools/tavily.py +146 -0
  55. agno/utils/agent.py +366 -1
  56. agno/utils/mcp.py +92 -2
  57. agno/utils/media.py +166 -1
  58. agno/utils/message.py +60 -0
  59. agno/utils/print_response/workflow.py +17 -1
  60. agno/utils/team.py +89 -1
  61. agno/workflow/step.py +0 -1
  62. agno/workflow/types.py +10 -15
  63. agno/workflow/workflow.py +86 -1
  64. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/METADATA +31 -25
  65. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/RECORD +68 -64
  66. agno/db/async_postgres/schemas.py +0 -139
  67. agno/db/async_postgres/utils.py +0 -347
  68. agno/tools/mcp.py +0 -679
  69. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/WHEEL +0 -0
  70. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/licenses/LICENSE +0 -0
  71. {agno-2.2.0.dist-info → agno-2.2.2.dist-info}/top_level.txt +0 -0
agno/culture/manager.py CHANGED
@@ -78,8 +78,6 @@ class CultureManager:
78
78
  self.delete_knowledge = delete_knowledge
79
79
  self.clear_knowledge = clear_knowledge
80
80
  self.debug_mode = debug_mode
81
- self._tools_for_model: Optional[List[Dict[str, Any]]] = None
82
- self._functions_for_model: Optional[Dict[str, Function]] = None
83
81
 
84
82
  def get_model(self) -> Model:
85
83
  if self.model is None:
@@ -316,23 +314,27 @@ class CultureManager:
316
314
  return response
317
315
 
318
316
  # -*- Utility Functions -*-
319
- def _determine_tools_for_model(self, tools: List[Callable]) -> None:
317
+ def _determine_tools_for_model(self, tools: List[Callable]) -> List[Union[Function, dict]]:
320
318
  # Have to reset each time, because of different user IDs
321
- self._tools_for_model = []
322
- self._functions_for_model = {}
319
+
320
+ _function_names = []
321
+ _functions: List[Union[Function, dict]] = []
323
322
 
324
323
  for tool in tools:
325
324
  try:
326
325
  function_name = tool.__name__
327
- if function_name not in self._functions_for_model:
328
- func = Function.from_callable(tool, strict=True) # type: ignore
329
- func.strict = True
330
- self._functions_for_model[func.name] = func
331
- self._tools_for_model.append({"type": "function", "function": func.to_dict()})
332
- log_debug(f"Added function {func.name}")
326
+ if function_name in _function_names:
327
+ continue
328
+ _function_names.append(function_name)
329
+ func = Function.from_callable(tool, strict=True) # type: ignore
330
+ func.strict = True
331
+ _functions.append(func)
332
+ log_debug(f"Added function {func.name}")
333
333
  except Exception as e:
334
334
  log_warning(f"Could not add function {tool}: {e}")
335
335
 
336
+ return _functions
337
+
336
338
  def get_system_message(
337
339
  self,
338
340
  existing_knowledge: Optional[List[Dict[str, Any]]] = None,
@@ -460,7 +462,7 @@ class CultureManager:
460
462
 
461
463
  model_copy = deepcopy(self.model)
462
464
  # Update the Model (set defaults, add logit etc.)
463
- self._determine_tools_for_model(
465
+ _tools = self._determine_tools_for_model(
464
466
  self._get_db_tools(
465
467
  db,
466
468
  enable_add_knowledge=add_knowledge,
@@ -485,8 +487,7 @@ class CultureManager:
485
487
  # Generate a response from the Model (includes running function calls)
486
488
  response = model_copy.response(
487
489
  messages=messages_for_model,
488
- tools=self._tools_for_model,
489
- functions=self._functions_for_model,
490
+ tools=_tools,
490
491
  )
491
492
 
492
493
  if response.tool_calls is not None and len(response.tool_calls) > 0:
@@ -513,7 +514,7 @@ class CultureManager:
513
514
  model_copy = deepcopy(self.model)
514
515
  db = cast(AsyncBaseDb, db)
515
516
 
516
- self._determine_tools_for_model(
517
+ _tools = self._determine_tools_for_model(
517
518
  await self._aget_db_tools(
518
519
  db,
519
520
  enable_update_knowledge=update_knowledge,
@@ -535,8 +536,7 @@ class CultureManager:
535
536
  # Generate a response from the Model (includes running function calls)
536
537
  response = await model_copy.aresponse(
537
538
  messages=messages_for_model,
538
- tools=self._tools_for_model,
539
- functions=self._functions_for_model,
539
+ tools=_tools,
540
540
  )
541
541
 
542
542
  if response.tool_calls is not None and len(response.tool_calls) > 0:
@@ -564,7 +564,7 @@ class CultureManager:
564
564
 
565
565
  model_copy = deepcopy(self.model)
566
566
  # Update the Model (set defaults, add logit etc.)
567
- self._determine_tools_for_model(
567
+ _tools = self._determine_tools_for_model(
568
568
  self._get_db_tools(
569
569
  db,
570
570
  enable_delete_knowledge=delete_knowledge,
@@ -590,8 +590,7 @@ class CultureManager:
590
590
  # Generate a response from the Model (includes running function calls)
591
591
  response = model_copy.response(
592
592
  messages=messages_for_model,
593
- tools=self._tools_for_model,
594
- functions=self._functions_for_model,
593
+ tools=_tools,
595
594
  )
596
595
 
597
596
  if response.tool_calls is not None and len(response.tool_calls) > 0:
@@ -620,7 +619,7 @@ class CultureManager:
620
619
  model_copy = deepcopy(self.model)
621
620
  # Update the Model (set defaults, add logit etc.)
622
621
  if isinstance(db, AsyncBaseDb):
623
- self._determine_tools_for_model(
622
+ _tools = self._determine_tools_for_model(
624
623
  await self._aget_db_tools(
625
624
  db,
626
625
  enable_delete_knowledge=delete_knowledge,
@@ -630,7 +629,7 @@ class CultureManager:
630
629
  ),
631
630
  )
632
631
  else:
633
- self._determine_tools_for_model(
632
+ _tools = self._determine_tools_for_model(
634
633
  self._get_db_tools(
635
634
  db,
636
635
  enable_delete_knowledge=delete_knowledge,
@@ -656,8 +655,7 @@ class CultureManager:
656
655
  # Generate a response from the Model (includes running function calls)
657
656
  response = await model_copy.aresponse(
658
657
  messages=messages_for_model,
659
- tools=self._tools_for_model,
660
- functions=self._functions_for_model,
658
+ tools=_tools,
661
659
  )
662
660
 
663
661
  if response.tool_calls is not None and len(response.tool_calls) > 0:
@@ -1,3 +1,3 @@
1
- from agno.db.async_postgres.async_postgres import AsyncPostgresDb
1
+ from agno.db.postgres import AsyncPostgresDb
2
2
 
3
3
  __all__ = ["AsyncPostgresDb"]
agno/db/dynamo/dynamo.py CHANGED
@@ -277,8 +277,6 @@ class DynamoDb(BaseDb):
277
277
 
278
278
  session = deserialize_from_dynamodb_item(item)
279
279
 
280
- if session.get("session_type") != session_type.value:
281
- return None
282
280
  if user_id and session.get("user_id") != user_id:
283
281
  return None
284
282
 
@@ -280,8 +280,6 @@ class FirestoreDb(BaseDb):
280
280
 
281
281
  if user_id is not None:
282
282
  query = query.where(filter=FieldFilter("user_id", "==", user_id))
283
- if session_type is not None:
284
- query = query.where(filter=FieldFilter("session_type", "==", session_type.value))
285
283
 
286
284
  docs = query.stream()
287
285
  result = None
@@ -220,10 +220,6 @@ class GcsJsonDb(BaseDb):
220
220
  if user_id is not None and session_data.get("user_id") != user_id:
221
221
  continue
222
222
 
223
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
224
- if session_data.get("session_type") != session_type_value:
225
- continue
226
-
227
223
  if not deserialize:
228
224
  return session_data
229
225
 
agno/db/gcs_json/utils.py CHANGED
@@ -5,34 +5,10 @@ from datetime import date, datetime, timedelta, timezone
5
5
  from typing import Any, Dict, List, Optional
6
6
  from uuid import uuid4
7
7
 
8
- from agno.db.base import SessionType
9
8
  from agno.db.schemas.culture import CulturalKnowledge
10
- from agno.run.agent import RunOutput
11
- from agno.run.team import TeamRunOutput
12
- from agno.session.summary import SessionSummary
13
9
  from agno.utils.log import log_debug
14
10
 
15
11
 
16
- def hydrate_session(session: dict) -> dict:
17
- """Convert nested dictionaries to their corresponding object types.
18
-
19
- Args:
20
- session (dict): The session dictionary to hydrate.
21
-
22
- Returns:
23
- dict: The hydrated session dictionary.
24
- """
25
- if session.get("summary") is not None:
26
- session["summary"] = SessionSummary.from_dict(session["summary"])
27
- if session.get("runs") is not None:
28
- if session["session_type"] == SessionType.AGENT:
29
- session["runs"] = [RunOutput.from_dict(run) for run in session["runs"]]
30
- elif session["session_type"] == SessionType.TEAM:
31
- session["runs"] = [TeamRunOutput.from_dict(run) for run in session["runs"]]
32
-
33
- return session
34
-
35
-
36
12
  def apply_sorting(
37
13
  data: List[Dict[str, Any]], sort_by: Optional[str] = None, sort_order: Optional[str] = None
38
14
  ) -> List[Dict[str, Any]]:
@@ -108,9 +108,6 @@ class InMemoryDb(BaseDb):
108
108
  if session_data.get("session_id") == session_id:
109
109
  if user_id is not None and session_data.get("user_id") != user_id:
110
110
  continue
111
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
112
- if session_data.get("session_type") != session_type_value:
113
- continue
114
111
 
115
112
  session_data_copy = deepcopy(session_data)
116
113
 
agno/db/json/json_db.py CHANGED
@@ -13,7 +13,6 @@ from agno.db.json.utils import (
13
13
  deserialize_cultural_knowledge_from_db,
14
14
  fetch_all_sessions_data,
15
15
  get_dates_to_calculate_metrics_for,
16
- hydrate_session,
17
16
  serialize_cultural_knowledge_for_db,
18
17
  )
19
18
  from agno.db.schemas.culture import CulturalKnowledge
@@ -202,21 +201,16 @@ class JsonDb(BaseDb):
202
201
  if session_data.get("session_id") == session_id:
203
202
  if user_id is not None and session_data.get("user_id") != user_id:
204
203
  continue
205
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
206
- if session_data.get("session_type") != session_type_value:
207
- continue
208
-
209
- session = hydrate_session(session_data)
210
204
 
211
205
  if not deserialize:
212
- return session
206
+ return session_data
213
207
 
214
208
  if session_type == SessionType.AGENT:
215
- return AgentSession.from_dict(session)
209
+ return AgentSession.from_dict(session_data)
216
210
  elif session_type == SessionType.TEAM:
217
- return TeamSession.from_dict(session)
211
+ return TeamSession.from_dict(session_data)
218
212
  elif session_type == SessionType.WORKFLOW:
219
- return WorkflowSession.from_dict(session)
213
+ return WorkflowSession.from_dict(session_data)
220
214
  else:
221
215
  raise ValueError(f"Invalid session type: {session_type}")
222
216
 
agno/db/json/utils.py CHANGED
@@ -5,34 +5,10 @@ from datetime import date, datetime, timedelta, timezone
5
5
  from typing import Any, Dict, List, Optional
6
6
  from uuid import uuid4
7
7
 
8
- from agno.db.base import SessionType
9
8
  from agno.db.schemas.culture import CulturalKnowledge
10
- from agno.run.agent import RunOutput
11
- from agno.run.team import TeamRunOutput
12
- from agno.session.summary import SessionSummary
13
9
  from agno.utils.log import log_debug
14
10
 
15
11
 
16
- def hydrate_session(session: dict) -> dict:
17
- """Convert nested dictionaries to their corresponding object types.
18
-
19
- Args:
20
- session (dict): The session dictionary to hydrate.
21
-
22
- Returns:
23
- dict: The hydrated session dictionary.
24
- """
25
- if session.get("summary") is not None:
26
- session["summary"] = SessionSummary.from_dict(session["summary"])
27
- if session.get("runs") is not None:
28
- if session["session_type"] == SessionType.AGENT:
29
- session["runs"] = [RunOutput.from_dict(run) for run in session["runs"]]
30
- elif session["session_type"] == SessionType.TEAM:
31
- session["runs"] = [TeamRunOutput.from_dict(run) for run in session["runs"]]
32
-
33
- return session
34
-
35
-
36
12
  def apply_sorting(
37
13
  data: List[Dict[str, Any]], sort_by: Optional[str] = None, sort_order: Optional[str] = None
38
14
  ) -> List[Dict[str, Any]]:
agno/db/mongo/mongo.py CHANGED
@@ -291,8 +291,6 @@ class MongoDb(BaseDb):
291
291
  query = {"session_id": session_id}
292
292
  if user_id is not None:
293
293
  query["user_id"] = user_id
294
- if session_type is not None:
295
- query["session_type"] = session_type
296
294
 
297
295
  result = collection.find_one(query)
298
296
  if result is None:
agno/db/mysql/mysql.py CHANGED
@@ -387,9 +387,6 @@ class MySQLDb(BaseDb):
387
387
 
388
388
  if user_id is not None:
389
389
  stmt = stmt.where(table.c.user_id == user_id)
390
- if session_type is not None:
391
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
392
- stmt = stmt.where(table.c.session_type == session_type_value)
393
390
  result = sess.execute(stmt).fetchone()
394
391
  if result is None:
395
392
  return None
@@ -1,4 +1,4 @@
1
- from agno.db.async_postgres import AsyncPostgresDb
1
+ from agno.db.postgres.async_postgres import AsyncPostgresDb
2
2
  from agno.db.postgres.postgres import PostgresDb
3
3
 
4
4
  __all__ = ["PostgresDb", "AsyncPostgresDb"]
@@ -3,20 +3,20 @@ from datetime import date, datetime, timedelta, timezone
3
3
  from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
4
4
  from uuid import uuid4
5
5
 
6
- from agno.db.async_postgres.schemas import get_table_schema_definition
7
- from agno.db.async_postgres.utils import (
6
+ from agno.db.base import AsyncBaseDb, SessionType
7
+ from agno.db.postgres.schemas import get_table_schema_definition
8
+ from agno.db.postgres.utils import (
9
+ abulk_upsert_metrics,
10
+ acreate_schema,
11
+ ais_table_available,
12
+ ais_valid_table,
8
13
  apply_sorting,
9
- bulk_upsert_metrics,
10
14
  calculate_date_metrics,
11
- create_schema,
12
15
  deserialize_cultural_knowledge,
13
16
  fetch_all_sessions_data,
14
17
  get_dates_to_calculate_metrics_for,
15
- is_table_available,
16
- is_valid_table,
17
18
  serialize_cultural_knowledge,
18
19
  )
19
- from agno.db.base import AsyncBaseDb, SessionType
20
20
  from agno.db.schemas.culture import CulturalKnowledge
21
21
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
22
22
  from agno.db.schemas.knowledge import KnowledgeRow
@@ -148,7 +148,7 @@ class AsyncPostgresDb(AsyncBaseDb):
148
148
  table.append_constraint(Index(idx_name, idx_col))
149
149
 
150
150
  async with self.async_session_factory() as sess, sess.begin():
151
- await create_schema(session=sess, db_schema=db_schema)
151
+ await acreate_schema(session=sess, db_schema=db_schema)
152
152
 
153
153
  # Create table
154
154
  async with self.db_engine.begin() as conn:
@@ -241,12 +241,12 @@ class AsyncPostgresDb(AsyncBaseDb):
241
241
  """
242
242
 
243
243
  async with self.async_session_factory() as sess, sess.begin():
244
- table_is_available = await is_table_available(session=sess, table_name=table_name, db_schema=db_schema)
244
+ table_is_available = await ais_table_available(session=sess, table_name=table_name, db_schema=db_schema)
245
245
 
246
246
  if not table_is_available:
247
247
  return await self._create_table(table_name=table_name, table_type=table_type, db_schema=db_schema)
248
248
 
249
- if not await is_valid_table(
249
+ if not await ais_valid_table(
250
250
  db_engine=self.db_engine,
251
251
  table_name=table_name,
252
252
  table_type=table_type,
@@ -288,7 +288,7 @@ class AsyncPostgresDb(AsyncBaseDb):
288
288
  delete_stmt = table.delete().where(table.c.session_id == session_id)
289
289
  result = await sess.execute(delete_stmt)
290
290
 
291
- if result.rowcount == 0:
291
+ if result.rowcount == 0: # type: ignore
292
292
  log_debug(f"No session found to delete with session_id: {session_id} in table {table.name}")
293
293
  return False
294
294
 
@@ -317,7 +317,7 @@ class AsyncPostgresDb(AsyncBaseDb):
317
317
  delete_stmt = table.delete().where(table.c.session_id.in_(session_ids))
318
318
  result = await sess.execute(delete_stmt)
319
319
 
320
- log_debug(f"Successfully deleted {result.rowcount} sessions")
320
+ log_debug(f"Successfully deleted {result.rowcount} sessions") # type: ignore
321
321
 
322
322
  except Exception as e:
323
323
  log_error(f"Error deleting sessions: {e}")
@@ -354,9 +354,6 @@ class AsyncPostgresDb(AsyncBaseDb):
354
354
 
355
355
  if user_id is not None:
356
356
  stmt = stmt.where(table.c.user_id == user_id)
357
- if session_type is not None:
358
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
359
- stmt = stmt.where(table.c.session_type == session_type_value)
360
357
  result = await sess.execute(stmt)
361
358
  row = result.fetchone()
362
359
  if row is None:
@@ -712,7 +709,7 @@ class AsyncPostgresDb(AsyncBaseDb):
712
709
  delete_stmt = table.delete().where(table.c.memory_id == memory_id)
713
710
  result = await sess.execute(delete_stmt)
714
711
 
715
- success = result.rowcount > 0
712
+ success = result.rowcount > 0 # type: ignore
716
713
  if success:
717
714
  log_debug(f"Successfully deleted user memory id: {memory_id}")
718
715
  else:
@@ -737,10 +734,10 @@ class AsyncPostgresDb(AsyncBaseDb):
737
734
  delete_stmt = table.delete().where(table.c.memory_id.in_(memory_ids))
738
735
  result = await sess.execute(delete_stmt)
739
736
 
740
- if result.rowcount == 0:
737
+ if result.rowcount == 0: # type: ignore
741
738
  log_debug(f"No user memories found with ids: {memory_ids}")
742
739
  else:
743
- log_debug(f"Successfully deleted {result.rowcount} user memories")
740
+ log_debug(f"Successfully deleted {result.rowcount} user memories") # type: ignore
744
741
 
745
742
  except Exception as e:
746
743
  log_error(f"Error deleting user memories: {e}")
@@ -1387,7 +1384,7 @@ class AsyncPostgresDb(AsyncBaseDb):
1387
1384
 
1388
1385
  if metrics_records:
1389
1386
  async with self.async_session_factory() as sess, sess.begin():
1390
- results = await bulk_upsert_metrics(session=sess, table=table, metrics_records=metrics_records)
1387
+ results = await abulk_upsert_metrics(session=sess, table=table, metrics_records=metrics_records)
1391
1388
 
1392
1389
  log_debug("Updated metrics calculations")
1393
1390
 
@@ -1649,7 +1646,7 @@ class AsyncPostgresDb(AsyncBaseDb):
1649
1646
  stmt = table.delete().where(table.c.run_id == eval_run_id)
1650
1647
  result = await sess.execute(stmt)
1651
1648
 
1652
- if result.rowcount == 0:
1649
+ if result.rowcount == 0: # type: ignore
1653
1650
  log_warning(f"No eval run found with ID: {eval_run_id}")
1654
1651
  else:
1655
1652
  log_debug(f"Deleted eval run with ID: {eval_run_id}")
@@ -1670,10 +1667,10 @@ class AsyncPostgresDb(AsyncBaseDb):
1670
1667
  stmt = table.delete().where(table.c.run_id.in_(eval_run_ids))
1671
1668
  result = await sess.execute(stmt)
1672
1669
 
1673
- if result.rowcount == 0:
1670
+ if result.rowcount == 0: # type: ignore
1674
1671
  log_warning(f"No eval runs found with IDs: {eval_run_ids}")
1675
1672
  else:
1676
- log_debug(f"Deleted {result.rowcount} eval runs")
1673
+ log_debug(f"Deleted {result.rowcount} eval runs") # type: ignore
1677
1674
 
1678
1675
  except Exception as e:
1679
1676
  log_error(f"Error deleting eval runs {eval_run_ids}: {e}")
@@ -10,12 +10,12 @@ from agno.db.postgres.utils import (
10
10
  bulk_upsert_metrics,
11
11
  calculate_date_metrics,
12
12
  create_schema,
13
- deserialize_cultural_knowledge_from_db,
13
+ deserialize_cultural_knowledge,
14
14
  fetch_all_sessions_data,
15
15
  get_dates_to_calculate_metrics_for,
16
16
  is_table_available,
17
17
  is_valid_table,
18
- serialize_cultural_knowledge_for_db,
18
+ serialize_cultural_knowledge,
19
19
  )
20
20
  from agno.db.schemas.culture import CulturalKnowledge
21
21
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
@@ -383,9 +383,6 @@ class PostgresDb(BaseDb):
383
383
 
384
384
  if user_id is not None:
385
385
  stmt = stmt.where(table.c.user_id == user_id)
386
- if session_type is not None:
387
- session_type_value = session_type.value if isinstance(session_type, SessionType) else session_type
388
- stmt = stmt.where(table.c.session_type == session_type_value)
389
386
  result = sess.execute(stmt).fetchone()
390
387
  if result is None:
391
388
  return None
@@ -2030,7 +2027,7 @@ class PostgresDb(BaseDb):
2030
2027
  if not db_row or not deserialize:
2031
2028
  return db_row
2032
2029
 
2033
- return deserialize_cultural_knowledge_from_db(db_row)
2030
+ return deserialize_cultural_knowledge(db_row)
2034
2031
 
2035
2032
  except Exception as e:
2036
2033
  log_error(f"Exception reading from cultural knowledge table: {e}")
@@ -2104,7 +2101,7 @@ class PostgresDb(BaseDb):
2104
2101
  if not deserialize:
2105
2102
  return db_rows, total_count
2106
2103
 
2107
- return [deserialize_cultural_knowledge_from_db(row) for row in db_rows]
2104
+ return [deserialize_cultural_knowledge(row) for row in db_rows]
2108
2105
 
2109
2106
  except Exception as e:
2110
2107
  log_error(f"Error reading from cultural knowledge table: {e}")
@@ -2134,7 +2131,7 @@ class PostgresDb(BaseDb):
2134
2131
  cultural_knowledge.id = str(uuid4())
2135
2132
 
2136
2133
  # Serialize content, categories, and notes into a JSON dict for DB storage
2137
- content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
2134
+ content_dict = serialize_cultural_knowledge(cultural_knowledge)
2138
2135
 
2139
2136
  with self.Session() as sess, sess.begin():
2140
2137
  stmt = postgresql.insert(table).values(
@@ -2149,7 +2146,7 @@ class PostgresDb(BaseDb):
2149
2146
  agent_id=cultural_knowledge.agent_id,
2150
2147
  team_id=cultural_knowledge.team_id,
2151
2148
  )
2152
- stmt = stmt.on_conflict_do_update(
2149
+ stmt = stmt.on_conflict_do_update( # type: ignore
2153
2150
  index_elements=["id"],
2154
2151
  set_=dict(
2155
2152
  name=cultural_knowledge.name,
@@ -2173,7 +2170,7 @@ class PostgresDb(BaseDb):
2173
2170
  if not db_row or not deserialize:
2174
2171
  return db_row
2175
2172
 
2176
- return deserialize_cultural_knowledge_from_db(db_row)
2173
+ return deserialize_cultural_knowledge(db_row)
2177
2174
 
2178
2175
  except Exception as e:
2179
2176
  log_error(f"Error upserting cultural knowledge: {e}")
agno/db/postgres/utils.py CHANGED
@@ -6,6 +6,7 @@ from typing import Any, Dict, List, Optional
6
6
  from uuid import uuid4
7
7
 
8
8
  from sqlalchemy import Engine
9
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
9
10
 
10
11
  from agno.db.postgres.schemas import get_table_schema_definition
11
12
  from agno.db.schemas.culture import CulturalKnowledge
@@ -63,6 +64,20 @@ def create_schema(session: Session, db_schema: str) -> None:
63
64
  log_warning(f"Could not create schema {db_schema}: {e}")
64
65
 
65
66
 
67
+ async def acreate_schema(session: AsyncSession, db_schema: str) -> None:
68
+ """Create the database schema if it doesn't exist.
69
+
70
+ Args:
71
+ session: The SQLAlchemy session to use
72
+ db_schema (str): The definition of the database schema to create
73
+ """
74
+ try:
75
+ log_debug(f"Creating schema if not exists: {db_schema}")
76
+ await session.execute(text(f"CREATE SCHEMA IF NOT EXISTS {db_schema};"))
77
+ except Exception as e:
78
+ log_warning(f"Could not create schema {db_schema}: {e}")
79
+
80
+
66
81
  def is_table_available(session: Session, table_name: str, db_schema: str) -> bool:
67
82
  """
68
83
  Check if a table with the given name exists in the given schema.
@@ -82,6 +97,24 @@ def is_table_available(session: Session, table_name: str, db_schema: str) -> boo
82
97
  return False
83
98
 
84
99
 
100
+ async def ais_table_available(session: AsyncSession, table_name: str, db_schema: str) -> bool:
101
+ """
102
+ Check if a table with the given name exists in the given schema.
103
+
104
+ Returns:
105
+ bool: True if the table exists, False otherwise.
106
+ """
107
+ try:
108
+ exists_query = text(
109
+ "SELECT 1 FROM information_schema.tables WHERE table_schema = :schema AND table_name = :table"
110
+ )
111
+ exists = (await session.execute(exists_query, {"schema": db_schema, "table": table_name})).scalar() is not None
112
+ return exists
113
+ except Exception as e:
114
+ log_error(f"Error checking if table exists: {e}")
115
+ return False
116
+
117
+
85
118
  def is_valid_table(db_engine: Engine, table_name: str, table_type: str, db_schema: str) -> bool:
86
119
  """
87
120
  Check if the existing table has the expected column names.
@@ -114,6 +147,44 @@ def is_valid_table(db_engine: Engine, table_name: str, table_type: str, db_schem
114
147
  return False
115
148
 
116
149
 
150
+ async def ais_valid_table(db_engine: AsyncEngine, table_name: str, table_type: str, db_schema: str) -> bool:
151
+ """
152
+ Check if the existing table has the expected column names.
153
+
154
+ Args:
155
+ table_name (str): Name of the table to validate
156
+ schema (str): Database schema name
157
+
158
+ Returns:
159
+ bool: True if table has all expected columns, False otherwise
160
+ """
161
+ try:
162
+ expected_table_schema = get_table_schema_definition(table_type)
163
+ expected_columns = {col_name for col_name in expected_table_schema.keys() if not col_name.startswith("_")}
164
+
165
+ # Get existing columns from the async engine
166
+ async with db_engine.connect() as conn:
167
+ existing_columns = await conn.run_sync(_get_table_columns, table_name, db_schema)
168
+
169
+ # Check if all expected columns exist
170
+ missing_columns = expected_columns - existing_columns
171
+ if missing_columns:
172
+ log_warning(f"Missing columns {missing_columns} in table {db_schema}.{table_name}")
173
+ return False
174
+
175
+ return True
176
+ except Exception as e:
177
+ log_error(f"Error validating table schema for {db_schema}.{table_name}: {e}")
178
+ return False
179
+
180
+
181
+ def _get_table_columns(conn, table_name: str, db_schema: str) -> set[str]:
182
+ """Helper function to get table columns using sync inspector."""
183
+ inspector = inspect(conn)
184
+ columns_info = inspector.get_columns(table_name, schema=db_schema)
185
+ return {col["name"] for col in columns_info}
186
+
187
+
117
188
  # -- Metrics util methods --
118
189
  def bulk_upsert_metrics(session: Session, table: Table, metrics_records: list[dict]) -> list[dict]:
119
190
  """Bulk upsert metrics into the database.
@@ -148,6 +219,39 @@ def bulk_upsert_metrics(session: Session, table: Table, metrics_records: list[di
148
219
  return results # type: ignore
149
220
 
150
221
 
222
+ async def abulk_upsert_metrics(session: AsyncSession, table: Table, metrics_records: list[dict]) -> list[dict]:
223
+ """Bulk upsert metrics into the database.
224
+
225
+ Args:
226
+ table (Table): The table to upsert into.
227
+ metrics_records (list[dict]): The metrics records to upsert.
228
+
229
+ Returns:
230
+ list[dict]: The upserted metrics records.
231
+ """
232
+ if not metrics_records:
233
+ return []
234
+
235
+ results = []
236
+ stmt = postgresql.insert(table)
237
+
238
+ # Columns to update in case of conflict
239
+ update_columns = {
240
+ col.name: stmt.excluded[col.name]
241
+ for col in table.columns
242
+ if col.name not in ["id", "date", "created_at", "aggregation_period"]
243
+ }
244
+
245
+ stmt = stmt.on_conflict_do_update(index_elements=["date", "aggregation_period"], set_=update_columns).returning( # type: ignore
246
+ table
247
+ )
248
+ result = await session.execute(stmt, metrics_records)
249
+ results = [row._mapping for row in result.fetchall()]
250
+ await session.commit()
251
+
252
+ return results # type: ignore
253
+
254
+
151
255
  def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
152
256
  """Calculate metrics for the given single date.
153
257
 
@@ -282,7 +386,7 @@ def get_dates_to_calculate_metrics_for(starting_date: date) -> list[date]:
282
386
 
283
387
 
284
388
  # -- Cultural Knowledge util methods --
285
- def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
389
+ def serialize_cultural_knowledge(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
286
390
  """Serialize a CulturalKnowledge object for database storage.
287
391
 
288
392
  Converts the model's separate content, categories, and notes fields
@@ -305,7 +409,7 @@ def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -
305
409
  return content_dict if content_dict else {}
306
410
 
307
411
 
308
- def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
412
+ def deserialize_cultural_knowledge(db_row: Dict[str, Any]) -> CulturalKnowledge:
309
413
  """Deserialize a database row to a CulturalKnowledge object.
310
414
 
311
415
  The database stores content as a JSON dict containing content, categories, and notes.