agno 2.3.4__py3-none-any.whl → 2.3.5__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 (112) hide show
  1. agno/agent/agent.py +177 -41
  2. agno/culture/manager.py +2 -2
  3. agno/db/base.py +330 -8
  4. agno/db/dynamo/dynamo.py +722 -2
  5. agno/db/dynamo/schemas.py +127 -0
  6. agno/db/firestore/firestore.py +573 -1
  7. agno/db/firestore/schemas.py +40 -0
  8. agno/db/gcs_json/gcs_json_db.py +446 -1
  9. agno/db/in_memory/in_memory_db.py +143 -1
  10. agno/db/json/json_db.py +438 -1
  11. agno/db/mongo/async_mongo.py +522 -0
  12. agno/db/mongo/mongo.py +523 -1
  13. agno/db/mongo/schemas.py +29 -0
  14. agno/db/mysql/mysql.py +536 -3
  15. agno/db/mysql/schemas.py +38 -0
  16. agno/db/postgres/async_postgres.py +541 -13
  17. agno/db/postgres/postgres.py +535 -2
  18. agno/db/postgres/schemas.py +38 -0
  19. agno/db/redis/redis.py +468 -1
  20. agno/db/redis/schemas.py +32 -0
  21. agno/db/singlestore/schemas.py +38 -0
  22. agno/db/singlestore/singlestore.py +523 -1
  23. agno/db/sqlite/async_sqlite.py +548 -9
  24. agno/db/sqlite/schemas.py +38 -0
  25. agno/db/sqlite/sqlite.py +537 -5
  26. agno/db/sqlite/utils.py +6 -8
  27. agno/db/surrealdb/models.py +25 -0
  28. agno/db/surrealdb/surrealdb.py +548 -1
  29. agno/eval/accuracy.py +10 -4
  30. agno/eval/performance.py +10 -4
  31. agno/eval/reliability.py +22 -13
  32. agno/exceptions.py +11 -0
  33. agno/hooks/__init__.py +3 -0
  34. agno/hooks/decorator.py +164 -0
  35. agno/knowledge/chunking/semantic.py +2 -2
  36. agno/models/aimlapi/aimlapi.py +2 -3
  37. agno/models/anthropic/claude.py +18 -13
  38. agno/models/aws/bedrock.py +3 -4
  39. agno/models/aws/claude.py +5 -1
  40. agno/models/azure/ai_foundry.py +2 -2
  41. agno/models/azure/openai_chat.py +8 -0
  42. agno/models/cerebras/cerebras.py +63 -11
  43. agno/models/cerebras/cerebras_openai.py +2 -3
  44. agno/models/cohere/chat.py +1 -5
  45. agno/models/cometapi/cometapi.py +2 -3
  46. agno/models/dashscope/dashscope.py +2 -3
  47. agno/models/deepinfra/deepinfra.py +2 -3
  48. agno/models/deepseek/deepseek.py +2 -3
  49. agno/models/fireworks/fireworks.py +2 -3
  50. agno/models/google/gemini.py +9 -7
  51. agno/models/groq/groq.py +2 -3
  52. agno/models/huggingface/huggingface.py +1 -5
  53. agno/models/ibm/watsonx.py +1 -5
  54. agno/models/internlm/internlm.py +2 -3
  55. agno/models/langdb/langdb.py +6 -4
  56. agno/models/litellm/chat.py +2 -2
  57. agno/models/litellm/litellm_openai.py +2 -3
  58. agno/models/meta/llama.py +1 -5
  59. agno/models/meta/llama_openai.py +4 -5
  60. agno/models/mistral/mistral.py +1 -5
  61. agno/models/nebius/nebius.py +2 -3
  62. agno/models/nvidia/nvidia.py +4 -5
  63. agno/models/openai/chat.py +14 -3
  64. agno/models/openai/responses.py +14 -3
  65. agno/models/openrouter/openrouter.py +4 -5
  66. agno/models/perplexity/perplexity.py +2 -3
  67. agno/models/portkey/portkey.py +7 -6
  68. agno/models/requesty/requesty.py +4 -5
  69. agno/models/response.py +2 -1
  70. agno/models/sambanova/sambanova.py +4 -5
  71. agno/models/siliconflow/siliconflow.py +3 -4
  72. agno/models/together/together.py +4 -5
  73. agno/models/vercel/v0.py +4 -5
  74. agno/models/vllm/vllm.py +19 -14
  75. agno/models/xai/xai.py +4 -5
  76. agno/os/app.py +104 -0
  77. agno/os/config.py +13 -0
  78. agno/os/interfaces/whatsapp/router.py +0 -1
  79. agno/os/mcp.py +1 -0
  80. agno/os/router.py +31 -0
  81. agno/os/routers/traces/__init__.py +3 -0
  82. agno/os/routers/traces/schemas.py +414 -0
  83. agno/os/routers/traces/traces.py +499 -0
  84. agno/os/schema.py +10 -1
  85. agno/os/utils.py +57 -0
  86. agno/run/agent.py +1 -0
  87. agno/run/base.py +17 -0
  88. agno/run/team.py +4 -0
  89. agno/session/team.py +1 -0
  90. agno/table.py +10 -0
  91. agno/team/team.py +214 -65
  92. agno/tools/function.py +10 -8
  93. agno/tools/nano_banana.py +1 -1
  94. agno/tracing/__init__.py +12 -0
  95. agno/tracing/exporter.py +157 -0
  96. agno/tracing/schemas.py +276 -0
  97. agno/tracing/setup.py +111 -0
  98. agno/utils/agent.py +4 -4
  99. agno/utils/hooks.py +56 -1
  100. agno/vectordb/qdrant/qdrant.py +22 -22
  101. agno/workflow/condition.py +8 -0
  102. agno/workflow/loop.py +8 -0
  103. agno/workflow/parallel.py +8 -0
  104. agno/workflow/router.py +8 -0
  105. agno/workflow/step.py +20 -0
  106. agno/workflow/steps.py +8 -0
  107. agno/workflow/workflow.py +83 -17
  108. {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/METADATA +2 -2
  109. {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/RECORD +112 -102
  110. {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/WHEEL +0 -0
  111. {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/licenses/LICENSE +0 -0
  112. {agno-2.3.4.dist-info → agno-2.3.5.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,12 @@
1
1
  import json
2
2
  import time
3
3
  from datetime import date, datetime, timedelta, timezone
4
- from typing import Any, Dict, List, Optional, Tuple, Union
4
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
5
5
  from uuid import uuid4
6
6
 
7
+ if TYPE_CHECKING:
8
+ from agno.tracing.schemas import Span, Trace
9
+
7
10
  from agno.db.base import BaseDb, SessionType
8
11
  from agno.db.gcs_json.utils import (
9
12
  apply_sorting,
@@ -38,6 +41,8 @@ class GcsJsonDb(BaseDb):
38
41
  eval_table: Optional[str] = None,
39
42
  knowledge_table: Optional[str] = None,
40
43
  culture_table: Optional[str] = None,
44
+ traces_table: Optional[str] = None,
45
+ spans_table: Optional[str] = None,
41
46
  project: Optional[str] = None,
42
47
  credentials: Optional[Any] = None,
43
48
  id: Optional[str] = None,
@@ -54,6 +59,8 @@ class GcsJsonDb(BaseDb):
54
59
  eval_table (Optional[str]): Name of the JSON file to store evaluation runs.
55
60
  knowledge_table (Optional[str]): Name of the JSON file to store knowledge content.
56
61
  culture_table (Optional[str]): Name of the JSON file to store cultural knowledge.
62
+ traces_table (Optional[str]): Name of the JSON file to store traces.
63
+ spans_table (Optional[str]): Name of the JSON file to store spans.
57
64
  project (Optional[str]): GCP project ID. If None, uses default project.
58
65
  location (Optional[str]): GCS bucket location. If None, uses default location.
59
66
  credentials (Optional[Any]): GCP credentials. If None, uses default credentials.
@@ -72,6 +79,8 @@ class GcsJsonDb(BaseDb):
72
79
  eval_table=eval_table,
73
80
  knowledge_table=knowledge_table,
74
81
  culture_table=culture_table,
82
+ traces_table=traces_table,
83
+ spans_table=spans_table,
75
84
  )
76
85
 
77
86
  self.bucket_name = bucket_name
@@ -1344,3 +1353,439 @@ class GcsJsonDb(BaseDb):
1344
1353
  except Exception as e:
1345
1354
  log_warning(f"Error upserting cultural knowledge: {e}")
1346
1355
  raise e
1356
+
1357
+ # --- Traces ---
1358
+ def create_trace(self, trace: "Trace") -> None:
1359
+ """Create a single trace record in the database.
1360
+
1361
+ Args:
1362
+ trace: The Trace object to store (one per trace_id).
1363
+ """
1364
+ try:
1365
+ traces = self._read_json_file(self.trace_table_name, create_table_if_not_found=True)
1366
+
1367
+ # Check if trace exists
1368
+ existing_idx = None
1369
+ for i, existing in enumerate(traces):
1370
+ if existing.get("trace_id") == trace.trace_id:
1371
+ existing_idx = i
1372
+ break
1373
+
1374
+ if existing_idx is not None:
1375
+ existing = traces[existing_idx]
1376
+
1377
+ # workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
1378
+ def get_component_level(workflow_id: Any, team_id: Any, agent_id: Any, name: str) -> int:
1379
+ is_root_name = ".run" in name or ".arun" in name
1380
+ if not is_root_name:
1381
+ return 0
1382
+ elif workflow_id:
1383
+ return 3
1384
+ elif team_id:
1385
+ return 2
1386
+ elif agent_id:
1387
+ return 1
1388
+ else:
1389
+ return 0
1390
+
1391
+ existing_level = get_component_level(
1392
+ existing.get("workflow_id"),
1393
+ existing.get("team_id"),
1394
+ existing.get("agent_id"),
1395
+ existing.get("name", ""),
1396
+ )
1397
+ new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
1398
+ should_update_name = new_level > existing_level
1399
+
1400
+ # Parse existing start_time to calculate correct duration
1401
+ existing_start_time_str = existing.get("start_time")
1402
+ if isinstance(existing_start_time_str, str):
1403
+ existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
1404
+ else:
1405
+ existing_start_time = trace.start_time
1406
+
1407
+ recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
1408
+
1409
+ # Update existing trace
1410
+ existing["end_time"] = trace.end_time.isoformat()
1411
+ existing["duration_ms"] = recalculated_duration_ms
1412
+ existing["status"] = trace.status
1413
+ if should_update_name:
1414
+ existing["name"] = trace.name
1415
+
1416
+ # Update context fields only if new value is not None
1417
+ if trace.run_id is not None:
1418
+ existing["run_id"] = trace.run_id
1419
+ if trace.session_id is not None:
1420
+ existing["session_id"] = trace.session_id
1421
+ if trace.user_id is not None:
1422
+ existing["user_id"] = trace.user_id
1423
+ if trace.agent_id is not None:
1424
+ existing["agent_id"] = trace.agent_id
1425
+ if trace.team_id is not None:
1426
+ existing["team_id"] = trace.team_id
1427
+ if trace.workflow_id is not None:
1428
+ existing["workflow_id"] = trace.workflow_id
1429
+
1430
+ traces[existing_idx] = existing
1431
+ else:
1432
+ # Add new trace
1433
+ trace_dict = trace.to_dict()
1434
+ trace_dict.pop("total_spans", None)
1435
+ trace_dict.pop("error_count", None)
1436
+ traces.append(trace_dict)
1437
+
1438
+ self._write_json_file(self.trace_table_name, traces)
1439
+
1440
+ except Exception as e:
1441
+ log_error(f"Error creating trace: {e}")
1442
+
1443
+ def get_trace(
1444
+ self,
1445
+ trace_id: Optional[str] = None,
1446
+ run_id: Optional[str] = None,
1447
+ ):
1448
+ """Get a single trace by trace_id or other filters.
1449
+
1450
+ Args:
1451
+ trace_id: The unique trace identifier.
1452
+ run_id: Filter by run ID (returns first match).
1453
+
1454
+ Returns:
1455
+ Optional[Trace]: The trace if found, None otherwise.
1456
+
1457
+ Note:
1458
+ If multiple filters are provided, trace_id takes precedence.
1459
+ For other filters, the most recent trace is returned.
1460
+ """
1461
+ try:
1462
+ from agno.tracing.schemas import Trace
1463
+
1464
+ traces = self._read_json_file(self.trace_table_name, create_table_if_not_found=False)
1465
+ if not traces:
1466
+ return None
1467
+
1468
+ # Get spans for calculating total_spans and error_count
1469
+ spans = self._read_json_file(self.span_table_name, create_table_if_not_found=False)
1470
+
1471
+ # Filter traces
1472
+ filtered = []
1473
+ for t in traces:
1474
+ if trace_id and t.get("trace_id") == trace_id:
1475
+ filtered.append(t)
1476
+ break
1477
+ elif run_id and t.get("run_id") == run_id:
1478
+ filtered.append(t)
1479
+
1480
+ if not filtered:
1481
+ return None
1482
+
1483
+ # Sort by start_time desc and get first
1484
+ filtered.sort(key=lambda x: x.get("start_time", ""), reverse=True)
1485
+ trace_data = filtered[0]
1486
+
1487
+ # Calculate total_spans and error_count
1488
+ trace_spans = [s for s in spans if s.get("trace_id") == trace_data.get("trace_id")]
1489
+ trace_data["total_spans"] = len(trace_spans)
1490
+ trace_data["error_count"] = sum(1 for s in trace_spans if s.get("status_code") == "ERROR")
1491
+
1492
+ return Trace.from_dict(trace_data)
1493
+
1494
+ except Exception as e:
1495
+ log_error(f"Error getting trace: {e}")
1496
+ return None
1497
+
1498
+ def get_traces(
1499
+ self,
1500
+ run_id: Optional[str] = None,
1501
+ session_id: Optional[str] = None,
1502
+ user_id: Optional[str] = None,
1503
+ agent_id: Optional[str] = None,
1504
+ team_id: Optional[str] = None,
1505
+ workflow_id: Optional[str] = None,
1506
+ status: Optional[str] = None,
1507
+ start_time: Optional[datetime] = None,
1508
+ end_time: Optional[datetime] = None,
1509
+ limit: Optional[int] = 20,
1510
+ page: Optional[int] = 1,
1511
+ ) -> tuple[List, int]:
1512
+ """Get traces matching the provided filters with pagination.
1513
+
1514
+ Args:
1515
+ run_id: Filter by run ID.
1516
+ session_id: Filter by session ID.
1517
+ user_id: Filter by user ID.
1518
+ agent_id: Filter by agent ID.
1519
+ team_id: Filter by team ID.
1520
+ workflow_id: Filter by workflow ID.
1521
+ status: Filter by status (OK, ERROR, UNSET).
1522
+ start_time: Filter traces starting after this datetime.
1523
+ end_time: Filter traces ending before this datetime.
1524
+ limit: Maximum number of traces to return per page.
1525
+ page: Page number (1-indexed).
1526
+
1527
+ Returns:
1528
+ tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
1529
+ """
1530
+ try:
1531
+ from agno.tracing.schemas import Trace
1532
+
1533
+ traces = self._read_json_file(self.trace_table_name, create_table_if_not_found=False)
1534
+ if not traces:
1535
+ return [], 0
1536
+
1537
+ # Get spans for calculating total_spans and error_count
1538
+ spans = self._read_json_file(self.span_table_name, create_table_if_not_found=False)
1539
+
1540
+ # Apply filters
1541
+ filtered = []
1542
+ for t in traces:
1543
+ if run_id and t.get("run_id") != run_id:
1544
+ continue
1545
+ if session_id and t.get("session_id") != session_id:
1546
+ continue
1547
+ if user_id and t.get("user_id") != user_id:
1548
+ continue
1549
+ if agent_id and t.get("agent_id") != agent_id:
1550
+ continue
1551
+ if team_id and t.get("team_id") != team_id:
1552
+ continue
1553
+ if workflow_id and t.get("workflow_id") != workflow_id:
1554
+ continue
1555
+ if status and t.get("status") != status:
1556
+ continue
1557
+ if start_time:
1558
+ trace_start = t.get("start_time", "")
1559
+ if trace_start < start_time.isoformat():
1560
+ continue
1561
+ if end_time:
1562
+ trace_end = t.get("end_time", "")
1563
+ if trace_end > end_time.isoformat():
1564
+ continue
1565
+ filtered.append(t)
1566
+
1567
+ total_count = len(filtered)
1568
+
1569
+ # Sort by start_time desc
1570
+ filtered.sort(key=lambda x: x.get("start_time", ""), reverse=True)
1571
+
1572
+ # Apply pagination
1573
+ if limit and page:
1574
+ start_idx = (page - 1) * limit
1575
+ filtered = filtered[start_idx : start_idx + limit]
1576
+
1577
+ # Add total_spans and error_count to each trace
1578
+ result_traces = []
1579
+ for t in filtered:
1580
+ trace_spans = [s for s in spans if s.get("trace_id") == t.get("trace_id")]
1581
+ t["total_spans"] = len(trace_spans)
1582
+ t["error_count"] = sum(1 for s in trace_spans if s.get("status_code") == "ERROR")
1583
+ result_traces.append(Trace.from_dict(t))
1584
+
1585
+ return result_traces, total_count
1586
+
1587
+ except Exception as e:
1588
+ log_error(f"Error getting traces: {e}")
1589
+ return [], 0
1590
+
1591
+ def get_trace_stats(
1592
+ self,
1593
+ user_id: Optional[str] = None,
1594
+ agent_id: Optional[str] = None,
1595
+ team_id: Optional[str] = None,
1596
+ workflow_id: Optional[str] = None,
1597
+ start_time: Optional[datetime] = None,
1598
+ end_time: Optional[datetime] = None,
1599
+ limit: Optional[int] = 20,
1600
+ page: Optional[int] = 1,
1601
+ ) -> tuple[List[Dict[str, Any]], int]:
1602
+ """Get trace statistics grouped by session.
1603
+
1604
+ Args:
1605
+ user_id: Filter by user ID.
1606
+ agent_id: Filter by agent ID.
1607
+ team_id: Filter by team ID.
1608
+ workflow_id: Filter by workflow ID.
1609
+ start_time: Filter sessions with traces created after this datetime.
1610
+ end_time: Filter sessions with traces created before this datetime.
1611
+ limit: Maximum number of sessions to return per page.
1612
+ page: Page number (1-indexed).
1613
+
1614
+ Returns:
1615
+ tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
1616
+ Each dict contains: session_id, user_id, agent_id, team_id, workflow_id, total_traces,
1617
+ first_trace_at, last_trace_at.
1618
+ """
1619
+ try:
1620
+ traces = self._read_json_file(self.trace_table_name, create_table_if_not_found=False)
1621
+ if not traces:
1622
+ return [], 0
1623
+
1624
+ # Group by session_id
1625
+ session_stats: Dict[str, Dict[str, Any]] = {}
1626
+
1627
+ for t in traces:
1628
+ trace_session_id = t.get("session_id")
1629
+ if not trace_session_id:
1630
+ continue
1631
+
1632
+ # Apply filters
1633
+ if user_id and t.get("user_id") != user_id:
1634
+ continue
1635
+ if agent_id and t.get("agent_id") != agent_id:
1636
+ continue
1637
+ if team_id and t.get("team_id") != team_id:
1638
+ continue
1639
+ if workflow_id and t.get("workflow_id") != workflow_id:
1640
+ continue
1641
+
1642
+ created_at = t.get("created_at", "")
1643
+ if start_time and created_at < start_time.isoformat():
1644
+ continue
1645
+ if end_time and created_at > end_time.isoformat():
1646
+ continue
1647
+
1648
+ if trace_session_id not in session_stats:
1649
+ session_stats[trace_session_id] = {
1650
+ "session_id": trace_session_id,
1651
+ "user_id": t.get("user_id"),
1652
+ "agent_id": t.get("agent_id"),
1653
+ "team_id": t.get("team_id"),
1654
+ "workflow_id": t.get("workflow_id"),
1655
+ "total_traces": 0,
1656
+ "first_trace_at": created_at,
1657
+ "last_trace_at": created_at,
1658
+ }
1659
+
1660
+ session_stats[trace_session_id]["total_traces"] += 1
1661
+ if created_at and session_stats[trace_session_id]["first_trace_at"]:
1662
+ if created_at < session_stats[trace_session_id]["first_trace_at"]:
1663
+ session_stats[trace_session_id]["first_trace_at"] = created_at
1664
+ if created_at and session_stats[trace_session_id]["last_trace_at"]:
1665
+ if created_at > session_stats[trace_session_id]["last_trace_at"]:
1666
+ session_stats[trace_session_id]["last_trace_at"] = created_at
1667
+
1668
+ stats_list = list(session_stats.values())
1669
+ total_count = len(stats_list)
1670
+
1671
+ # Sort by last_trace_at desc
1672
+ stats_list.sort(key=lambda x: x.get("last_trace_at", ""), reverse=True)
1673
+
1674
+ # Apply pagination
1675
+ if limit and page:
1676
+ start_idx = (page - 1) * limit
1677
+ stats_list = stats_list[start_idx : start_idx + limit]
1678
+
1679
+ # Convert ISO strings to datetime objects
1680
+ for stat in stats_list:
1681
+ first_at = stat.get("first_trace_at", "")
1682
+ last_at = stat.get("last_trace_at", "")
1683
+ if first_at:
1684
+ stat["first_trace_at"] = datetime.fromisoformat(first_at.replace("Z", "+00:00"))
1685
+ if last_at:
1686
+ stat["last_trace_at"] = datetime.fromisoformat(last_at.replace("Z", "+00:00"))
1687
+
1688
+ return stats_list, total_count
1689
+
1690
+ except Exception as e:
1691
+ log_error(f"Error getting trace stats: {e}")
1692
+ return [], 0
1693
+
1694
+ # --- Spans ---
1695
+ def create_span(self, span: "Span") -> None:
1696
+ """Create a single span in the database.
1697
+
1698
+ Args:
1699
+ span: The Span object to store.
1700
+ """
1701
+ try:
1702
+ spans = self._read_json_file(self.span_table_name, create_table_if_not_found=True)
1703
+ spans.append(span.to_dict())
1704
+ self._write_json_file(self.span_table_name, spans)
1705
+
1706
+ except Exception as e:
1707
+ log_error(f"Error creating span: {e}")
1708
+
1709
+ def create_spans(self, spans: List) -> None:
1710
+ """Create multiple spans in the database as a batch.
1711
+
1712
+ Args:
1713
+ spans: List of Span objects to store.
1714
+ """
1715
+ if not spans:
1716
+ return
1717
+
1718
+ try:
1719
+ existing_spans = self._read_json_file(self.span_table_name, create_table_if_not_found=True)
1720
+ for span in spans:
1721
+ existing_spans.append(span.to_dict())
1722
+ self._write_json_file(self.span_table_name, existing_spans)
1723
+
1724
+ except Exception as e:
1725
+ log_error(f"Error creating spans batch: {e}")
1726
+
1727
+ def get_span(self, span_id: str):
1728
+ """Get a single span by its span_id.
1729
+
1730
+ Args:
1731
+ span_id: The unique span identifier.
1732
+
1733
+ Returns:
1734
+ Optional[Span]: The span if found, None otherwise.
1735
+ """
1736
+ try:
1737
+ from agno.tracing.schemas import Span
1738
+
1739
+ spans = self._read_json_file(self.span_table_name, create_table_if_not_found=False)
1740
+
1741
+ for s in spans:
1742
+ if s.get("span_id") == span_id:
1743
+ return Span.from_dict(s)
1744
+
1745
+ return None
1746
+
1747
+ except Exception as e:
1748
+ log_error(f"Error getting span: {e}")
1749
+ return None
1750
+
1751
+ def get_spans(
1752
+ self,
1753
+ trace_id: Optional[str] = None,
1754
+ parent_span_id: Optional[str] = None,
1755
+ limit: Optional[int] = 1000,
1756
+ ) -> List:
1757
+ """Get spans matching the provided filters.
1758
+
1759
+ Args:
1760
+ trace_id: Filter by trace ID.
1761
+ parent_span_id: Filter by parent span ID.
1762
+ limit: Maximum number of spans to return.
1763
+
1764
+ Returns:
1765
+ List[Span]: List of matching spans.
1766
+ """
1767
+ try:
1768
+ from agno.tracing.schemas import Span
1769
+
1770
+ spans = self._read_json_file(self.span_table_name, create_table_if_not_found=False)
1771
+ if not spans:
1772
+ return []
1773
+
1774
+ # Apply filters
1775
+ filtered = []
1776
+ for s in spans:
1777
+ if trace_id and s.get("trace_id") != trace_id:
1778
+ continue
1779
+ if parent_span_id and s.get("parent_span_id") != parent_span_id:
1780
+ continue
1781
+ filtered.append(s)
1782
+
1783
+ # Apply limit
1784
+ if limit:
1785
+ filtered = filtered[:limit]
1786
+
1787
+ return [Span.from_dict(s) for s in filtered]
1788
+
1789
+ except Exception as e:
1790
+ log_error(f"Error getting spans: {e}")
1791
+ return []
@@ -1,7 +1,7 @@
1
1
  import time
2
2
  from copy import deepcopy
3
3
  from datetime import date, datetime, timedelta, timezone
4
- from typing import Any, Dict, List, Optional, Tuple, Union
4
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
5
5
  from uuid import uuid4
6
6
 
7
7
  from agno.db.base import BaseDb, SessionType
@@ -20,6 +20,9 @@ from agno.db.schemas.memory import UserMemory
20
20
  from agno.session import AgentSession, Session, TeamSession, WorkflowSession
21
21
  from agno.utils.log import log_debug, log_error, log_info, log_warning
22
22
 
23
+ if TYPE_CHECKING:
24
+ from agno.tracing.schemas import Span, Trace
25
+
23
26
 
24
27
  class InMemoryDb(BaseDb):
25
28
  def __init__(self):
@@ -1168,3 +1171,142 @@ class InMemoryDb(BaseDb):
1168
1171
  except Exception as e:
1169
1172
  log_error(f"Error upserting cultural knowledge: {e}")
1170
1173
  raise e
1174
+
1175
+ # --- Traces ---
1176
+ def create_trace(self, trace: "Trace") -> None:
1177
+ """Create a single trace record in the database.
1178
+
1179
+ Args:
1180
+ trace: The Trace object to store (one per trace_id).
1181
+ """
1182
+ raise NotImplementedError
1183
+
1184
+ def get_trace(
1185
+ self,
1186
+ trace_id: Optional[str] = None,
1187
+ run_id: Optional[str] = None,
1188
+ ):
1189
+ """Get a single trace by trace_id or other filters.
1190
+
1191
+ Args:
1192
+ trace_id: The unique trace identifier.
1193
+ run_id: Filter by run ID (returns first match).
1194
+
1195
+ Returns:
1196
+ Optional[Trace]: The trace if found, None otherwise.
1197
+
1198
+ Note:
1199
+ If multiple filters are provided, trace_id takes precedence.
1200
+ For other filters, the most recent trace is returned.
1201
+ """
1202
+ raise NotImplementedError
1203
+
1204
+ def get_traces(
1205
+ self,
1206
+ run_id: Optional[str] = None,
1207
+ session_id: Optional[str] = None,
1208
+ user_id: Optional[str] = None,
1209
+ agent_id: Optional[str] = None,
1210
+ team_id: Optional[str] = None,
1211
+ workflow_id: Optional[str] = None,
1212
+ status: Optional[str] = None,
1213
+ start_time: Optional[datetime] = None,
1214
+ end_time: Optional[datetime] = None,
1215
+ limit: Optional[int] = 20,
1216
+ page: Optional[int] = 1,
1217
+ ) -> tuple[List, int]:
1218
+ """Get traces matching the provided filters.
1219
+
1220
+ Args:
1221
+ run_id: Filter by run ID.
1222
+ session_id: Filter by session ID.
1223
+ user_id: Filter by user ID.
1224
+ agent_id: Filter by agent ID.
1225
+ team_id: Filter by team ID.
1226
+ workflow_id: Filter by workflow ID.
1227
+ status: Filter by status (OK, ERROR, UNSET).
1228
+ start_time: Filter traces starting after this datetime.
1229
+ end_time: Filter traces ending before this datetime.
1230
+ limit: Maximum number of traces to return per page.
1231
+ page: Page number (1-indexed).
1232
+
1233
+ Returns:
1234
+ tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
1235
+ """
1236
+ raise NotImplementedError
1237
+
1238
+ def get_trace_stats(
1239
+ self,
1240
+ user_id: Optional[str] = None,
1241
+ agent_id: Optional[str] = None,
1242
+ team_id: Optional[str] = None,
1243
+ workflow_id: Optional[str] = None,
1244
+ start_time: Optional[datetime] = None,
1245
+ end_time: Optional[datetime] = None,
1246
+ limit: Optional[int] = 20,
1247
+ page: Optional[int] = 1,
1248
+ ) -> tuple[List[Dict[str, Any]], int]:
1249
+ """Get trace statistics grouped by session.
1250
+
1251
+ Args:
1252
+ user_id: Filter by user ID.
1253
+ agent_id: Filter by agent ID.
1254
+ team_id: Filter by team ID.
1255
+ workflow_id: Filter by workflow ID.
1256
+ start_time: Filter sessions with traces created after this datetime.
1257
+ end_time: Filter sessions with traces created before this datetime.
1258
+ limit: Maximum number of sessions to return per page.
1259
+ page: Page number (1-indexed).
1260
+
1261
+ Returns:
1262
+ tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
1263
+ Each dict contains: session_id, user_id, agent_id, team_id, workflow_id, total_traces,
1264
+ first_trace_at, last_trace_at.
1265
+ """
1266
+ raise NotImplementedError
1267
+
1268
+ # --- Spans ---
1269
+ def create_span(self, span: "Span") -> None:
1270
+ """Create a single span in the database.
1271
+
1272
+ Args:
1273
+ span: The Span object to store.
1274
+ """
1275
+ raise NotImplementedError
1276
+
1277
+ def create_spans(self, spans: List) -> None:
1278
+ """Create multiple spans in the database as a batch.
1279
+
1280
+ Args:
1281
+ spans: List of Span objects to store.
1282
+ """
1283
+ raise NotImplementedError
1284
+
1285
+ def get_span(self, span_id: str):
1286
+ """Get a single span by its span_id.
1287
+
1288
+ Args:
1289
+ span_id: The unique span identifier.
1290
+
1291
+ Returns:
1292
+ Optional[Span]: The span if found, None otherwise.
1293
+ """
1294
+ raise NotImplementedError
1295
+
1296
+ def get_spans(
1297
+ self,
1298
+ trace_id: Optional[str] = None,
1299
+ parent_span_id: Optional[str] = None,
1300
+ limit: Optional[int] = 1000,
1301
+ ) -> List:
1302
+ """Get spans matching the provided filters.
1303
+
1304
+ Args:
1305
+ trace_id: Filter by trace ID.
1306
+ parent_span_id: Filter by parent span ID.
1307
+ limit: Maximum number of spans to return.
1308
+
1309
+ Returns:
1310
+ List[Span]: List of matching spans.
1311
+ """
1312
+ raise NotImplementedError