agno 2.1.2__py3-none-any.whl → 2.3.13__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 (314) hide show
  1. agno/agent/agent.py +5540 -2273
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/compression/__init__.py +3 -0
  5. agno/compression/manager.py +247 -0
  6. agno/culture/__init__.py +3 -0
  7. agno/culture/manager.py +956 -0
  8. agno/db/async_postgres/__init__.py +3 -0
  9. agno/db/base.py +689 -6
  10. agno/db/dynamo/dynamo.py +933 -37
  11. agno/db/dynamo/schemas.py +174 -10
  12. agno/db/dynamo/utils.py +63 -4
  13. agno/db/firestore/firestore.py +831 -9
  14. agno/db/firestore/schemas.py +51 -0
  15. agno/db/firestore/utils.py +102 -4
  16. agno/db/gcs_json/gcs_json_db.py +660 -12
  17. agno/db/gcs_json/utils.py +60 -26
  18. agno/db/in_memory/in_memory_db.py +287 -14
  19. agno/db/in_memory/utils.py +60 -2
  20. agno/db/json/json_db.py +590 -14
  21. agno/db/json/utils.py +60 -26
  22. agno/db/migrations/manager.py +199 -0
  23. agno/db/migrations/v1_to_v2.py +43 -13
  24. agno/db/migrations/versions/__init__.py +0 -0
  25. agno/db/migrations/versions/v2_3_0.py +938 -0
  26. agno/db/mongo/__init__.py +15 -1
  27. agno/db/mongo/async_mongo.py +2760 -0
  28. agno/db/mongo/mongo.py +879 -11
  29. agno/db/mongo/schemas.py +42 -0
  30. agno/db/mongo/utils.py +80 -8
  31. agno/db/mysql/__init__.py +2 -1
  32. agno/db/mysql/async_mysql.py +2912 -0
  33. agno/db/mysql/mysql.py +946 -68
  34. agno/db/mysql/schemas.py +72 -10
  35. agno/db/mysql/utils.py +198 -7
  36. agno/db/postgres/__init__.py +2 -1
  37. agno/db/postgres/async_postgres.py +2579 -0
  38. agno/db/postgres/postgres.py +942 -57
  39. agno/db/postgres/schemas.py +81 -18
  40. agno/db/postgres/utils.py +164 -2
  41. agno/db/redis/redis.py +671 -7
  42. agno/db/redis/schemas.py +50 -0
  43. agno/db/redis/utils.py +65 -7
  44. agno/db/schemas/__init__.py +2 -1
  45. agno/db/schemas/culture.py +120 -0
  46. agno/db/schemas/evals.py +1 -0
  47. agno/db/schemas/memory.py +17 -2
  48. agno/db/singlestore/schemas.py +63 -0
  49. agno/db/singlestore/singlestore.py +949 -83
  50. agno/db/singlestore/utils.py +60 -2
  51. agno/db/sqlite/__init__.py +2 -1
  52. agno/db/sqlite/async_sqlite.py +2911 -0
  53. agno/db/sqlite/schemas.py +62 -0
  54. agno/db/sqlite/sqlite.py +965 -46
  55. agno/db/sqlite/utils.py +169 -8
  56. agno/db/surrealdb/__init__.py +3 -0
  57. agno/db/surrealdb/metrics.py +292 -0
  58. agno/db/surrealdb/models.py +334 -0
  59. agno/db/surrealdb/queries.py +71 -0
  60. agno/db/surrealdb/surrealdb.py +1908 -0
  61. agno/db/surrealdb/utils.py +147 -0
  62. agno/db/utils.py +2 -0
  63. agno/eval/__init__.py +10 -0
  64. agno/eval/accuracy.py +75 -55
  65. agno/eval/agent_as_judge.py +861 -0
  66. agno/eval/base.py +29 -0
  67. agno/eval/performance.py +16 -7
  68. agno/eval/reliability.py +28 -16
  69. agno/eval/utils.py +35 -17
  70. agno/exceptions.py +27 -2
  71. agno/filters.py +354 -0
  72. agno/guardrails/prompt_injection.py +1 -0
  73. agno/hooks/__init__.py +3 -0
  74. agno/hooks/decorator.py +164 -0
  75. agno/integrations/discord/client.py +1 -1
  76. agno/knowledge/chunking/agentic.py +13 -10
  77. agno/knowledge/chunking/fixed.py +4 -1
  78. agno/knowledge/chunking/semantic.py +9 -4
  79. agno/knowledge/chunking/strategy.py +59 -15
  80. agno/knowledge/embedder/fastembed.py +1 -1
  81. agno/knowledge/embedder/nebius.py +1 -1
  82. agno/knowledge/embedder/ollama.py +8 -0
  83. agno/knowledge/embedder/openai.py +8 -8
  84. agno/knowledge/embedder/sentence_transformer.py +6 -2
  85. agno/knowledge/embedder/vllm.py +262 -0
  86. agno/knowledge/knowledge.py +1618 -318
  87. agno/knowledge/reader/base.py +6 -2
  88. agno/knowledge/reader/csv_reader.py +8 -10
  89. agno/knowledge/reader/docx_reader.py +5 -6
  90. agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
  91. agno/knowledge/reader/json_reader.py +5 -4
  92. agno/knowledge/reader/markdown_reader.py +8 -8
  93. agno/knowledge/reader/pdf_reader.py +17 -19
  94. agno/knowledge/reader/pptx_reader.py +101 -0
  95. agno/knowledge/reader/reader_factory.py +32 -3
  96. agno/knowledge/reader/s3_reader.py +3 -3
  97. agno/knowledge/reader/tavily_reader.py +193 -0
  98. agno/knowledge/reader/text_reader.py +22 -10
  99. agno/knowledge/reader/web_search_reader.py +1 -48
  100. agno/knowledge/reader/website_reader.py +10 -10
  101. agno/knowledge/reader/wikipedia_reader.py +33 -1
  102. agno/knowledge/types.py +1 -0
  103. agno/knowledge/utils.py +72 -7
  104. agno/media.py +22 -6
  105. agno/memory/__init__.py +14 -1
  106. agno/memory/manager.py +544 -83
  107. agno/memory/strategies/__init__.py +15 -0
  108. agno/memory/strategies/base.py +66 -0
  109. agno/memory/strategies/summarize.py +196 -0
  110. agno/memory/strategies/types.py +37 -0
  111. agno/models/aimlapi/aimlapi.py +17 -0
  112. agno/models/anthropic/claude.py +515 -40
  113. agno/models/aws/bedrock.py +102 -21
  114. agno/models/aws/claude.py +131 -274
  115. agno/models/azure/ai_foundry.py +41 -19
  116. agno/models/azure/openai_chat.py +39 -8
  117. agno/models/base.py +1249 -525
  118. agno/models/cerebras/cerebras.py +91 -21
  119. agno/models/cerebras/cerebras_openai.py +21 -2
  120. agno/models/cohere/chat.py +40 -6
  121. agno/models/cometapi/cometapi.py +18 -1
  122. agno/models/dashscope/dashscope.py +2 -3
  123. agno/models/deepinfra/deepinfra.py +18 -1
  124. agno/models/deepseek/deepseek.py +69 -3
  125. agno/models/fireworks/fireworks.py +18 -1
  126. agno/models/google/gemini.py +877 -80
  127. agno/models/google/utils.py +22 -0
  128. agno/models/groq/groq.py +51 -18
  129. agno/models/huggingface/huggingface.py +17 -6
  130. agno/models/ibm/watsonx.py +16 -6
  131. agno/models/internlm/internlm.py +18 -1
  132. agno/models/langdb/langdb.py +13 -1
  133. agno/models/litellm/chat.py +44 -9
  134. agno/models/litellm/litellm_openai.py +18 -1
  135. agno/models/message.py +28 -5
  136. agno/models/meta/llama.py +47 -14
  137. agno/models/meta/llama_openai.py +22 -17
  138. agno/models/mistral/mistral.py +8 -4
  139. agno/models/nebius/nebius.py +6 -7
  140. agno/models/nvidia/nvidia.py +20 -3
  141. agno/models/ollama/chat.py +24 -8
  142. agno/models/openai/chat.py +104 -29
  143. agno/models/openai/responses.py +101 -81
  144. agno/models/openrouter/openrouter.py +60 -3
  145. agno/models/perplexity/perplexity.py +17 -1
  146. agno/models/portkey/portkey.py +7 -6
  147. agno/models/requesty/requesty.py +24 -4
  148. agno/models/response.py +73 -2
  149. agno/models/sambanova/sambanova.py +20 -3
  150. agno/models/siliconflow/siliconflow.py +19 -2
  151. agno/models/together/together.py +20 -3
  152. agno/models/utils.py +254 -8
  153. agno/models/vercel/v0.py +20 -3
  154. agno/models/vertexai/__init__.py +0 -0
  155. agno/models/vertexai/claude.py +190 -0
  156. agno/models/vllm/vllm.py +19 -14
  157. agno/models/xai/xai.py +19 -2
  158. agno/os/app.py +549 -152
  159. agno/os/auth.py +190 -3
  160. agno/os/config.py +23 -0
  161. agno/os/interfaces/a2a/router.py +8 -11
  162. agno/os/interfaces/a2a/utils.py +1 -1
  163. agno/os/interfaces/agui/router.py +18 -3
  164. agno/os/interfaces/agui/utils.py +152 -39
  165. agno/os/interfaces/slack/router.py +55 -37
  166. agno/os/interfaces/slack/slack.py +9 -1
  167. agno/os/interfaces/whatsapp/router.py +0 -1
  168. agno/os/interfaces/whatsapp/security.py +3 -1
  169. agno/os/mcp.py +110 -52
  170. agno/os/middleware/__init__.py +2 -0
  171. agno/os/middleware/jwt.py +676 -112
  172. agno/os/router.py +40 -1478
  173. agno/os/routers/agents/__init__.py +3 -0
  174. agno/os/routers/agents/router.py +599 -0
  175. agno/os/routers/agents/schema.py +261 -0
  176. agno/os/routers/evals/evals.py +96 -39
  177. agno/os/routers/evals/schemas.py +65 -33
  178. agno/os/routers/evals/utils.py +80 -10
  179. agno/os/routers/health.py +10 -4
  180. agno/os/routers/knowledge/knowledge.py +196 -38
  181. agno/os/routers/knowledge/schemas.py +82 -22
  182. agno/os/routers/memory/memory.py +279 -52
  183. agno/os/routers/memory/schemas.py +46 -17
  184. agno/os/routers/metrics/metrics.py +20 -8
  185. agno/os/routers/metrics/schemas.py +16 -16
  186. agno/os/routers/session/session.py +462 -34
  187. agno/os/routers/teams/__init__.py +3 -0
  188. agno/os/routers/teams/router.py +512 -0
  189. agno/os/routers/teams/schema.py +257 -0
  190. agno/os/routers/traces/__init__.py +3 -0
  191. agno/os/routers/traces/schemas.py +414 -0
  192. agno/os/routers/traces/traces.py +499 -0
  193. agno/os/routers/workflows/__init__.py +3 -0
  194. agno/os/routers/workflows/router.py +624 -0
  195. agno/os/routers/workflows/schema.py +75 -0
  196. agno/os/schema.py +256 -693
  197. agno/os/scopes.py +469 -0
  198. agno/os/utils.py +514 -36
  199. agno/reasoning/anthropic.py +80 -0
  200. agno/reasoning/gemini.py +73 -0
  201. agno/reasoning/openai.py +5 -0
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +155 -32
  205. agno/run/base.py +55 -3
  206. agno/run/requirement.py +181 -0
  207. agno/run/team.py +125 -38
  208. agno/run/workflow.py +72 -18
  209. agno/session/agent.py +102 -89
  210. agno/session/summary.py +56 -15
  211. agno/session/team.py +164 -90
  212. agno/session/workflow.py +405 -40
  213. agno/table.py +10 -0
  214. agno/team/team.py +3974 -1903
  215. agno/tools/dalle.py +2 -4
  216. agno/tools/eleven_labs.py +23 -25
  217. agno/tools/exa.py +21 -16
  218. agno/tools/file.py +153 -23
  219. agno/tools/file_generation.py +16 -10
  220. agno/tools/firecrawl.py +15 -7
  221. agno/tools/function.py +193 -38
  222. agno/tools/gmail.py +238 -14
  223. agno/tools/google_drive.py +271 -0
  224. agno/tools/googlecalendar.py +36 -8
  225. agno/tools/googlesheets.py +20 -5
  226. agno/tools/jira.py +20 -0
  227. agno/tools/mcp/__init__.py +10 -0
  228. agno/tools/mcp/mcp.py +331 -0
  229. agno/tools/mcp/multi_mcp.py +347 -0
  230. agno/tools/mcp/params.py +24 -0
  231. agno/tools/mcp_toolbox.py +3 -3
  232. agno/tools/models/nebius.py +5 -5
  233. agno/tools/models_labs.py +20 -10
  234. agno/tools/nano_banana.py +151 -0
  235. agno/tools/notion.py +204 -0
  236. agno/tools/parallel.py +314 -0
  237. agno/tools/postgres.py +76 -36
  238. agno/tools/redshift.py +406 -0
  239. agno/tools/scrapegraph.py +1 -1
  240. agno/tools/shopify.py +1519 -0
  241. agno/tools/slack.py +18 -3
  242. agno/tools/spotify.py +919 -0
  243. agno/tools/tavily.py +146 -0
  244. agno/tools/toolkit.py +25 -0
  245. agno/tools/workflow.py +8 -1
  246. agno/tools/yfinance.py +12 -11
  247. agno/tracing/__init__.py +12 -0
  248. agno/tracing/exporter.py +157 -0
  249. agno/tracing/schemas.py +276 -0
  250. agno/tracing/setup.py +111 -0
  251. agno/utils/agent.py +938 -0
  252. agno/utils/cryptography.py +22 -0
  253. agno/utils/dttm.py +33 -0
  254. agno/utils/events.py +151 -3
  255. agno/utils/gemini.py +15 -5
  256. agno/utils/hooks.py +118 -4
  257. agno/utils/http.py +113 -2
  258. agno/utils/knowledge.py +12 -5
  259. agno/utils/log.py +1 -0
  260. agno/utils/mcp.py +92 -2
  261. agno/utils/media.py +187 -1
  262. agno/utils/merge_dict.py +3 -3
  263. agno/utils/message.py +60 -0
  264. agno/utils/models/ai_foundry.py +9 -2
  265. agno/utils/models/claude.py +49 -14
  266. agno/utils/models/cohere.py +9 -2
  267. agno/utils/models/llama.py +9 -2
  268. agno/utils/models/mistral.py +4 -2
  269. agno/utils/print_response/agent.py +109 -16
  270. agno/utils/print_response/team.py +223 -30
  271. agno/utils/print_response/workflow.py +251 -34
  272. agno/utils/streamlit.py +1 -1
  273. agno/utils/team.py +98 -9
  274. agno/utils/tokens.py +657 -0
  275. agno/vectordb/base.py +39 -7
  276. agno/vectordb/cassandra/cassandra.py +21 -5
  277. agno/vectordb/chroma/chromadb.py +43 -12
  278. agno/vectordb/clickhouse/clickhousedb.py +21 -5
  279. agno/vectordb/couchbase/couchbase.py +29 -5
  280. agno/vectordb/lancedb/lance_db.py +92 -181
  281. agno/vectordb/langchaindb/langchaindb.py +24 -4
  282. agno/vectordb/lightrag/lightrag.py +17 -3
  283. agno/vectordb/llamaindex/llamaindexdb.py +25 -5
  284. agno/vectordb/milvus/milvus.py +50 -37
  285. agno/vectordb/mongodb/__init__.py +7 -1
  286. agno/vectordb/mongodb/mongodb.py +36 -30
  287. agno/vectordb/pgvector/pgvector.py +201 -77
  288. agno/vectordb/pineconedb/pineconedb.py +41 -23
  289. agno/vectordb/qdrant/qdrant.py +67 -54
  290. agno/vectordb/redis/__init__.py +9 -0
  291. agno/vectordb/redis/redisdb.py +682 -0
  292. agno/vectordb/singlestore/singlestore.py +50 -29
  293. agno/vectordb/surrealdb/surrealdb.py +31 -41
  294. agno/vectordb/upstashdb/upstashdb.py +34 -6
  295. agno/vectordb/weaviate/weaviate.py +53 -14
  296. agno/workflow/__init__.py +2 -0
  297. agno/workflow/agent.py +299 -0
  298. agno/workflow/condition.py +120 -18
  299. agno/workflow/loop.py +77 -10
  300. agno/workflow/parallel.py +231 -143
  301. agno/workflow/router.py +118 -17
  302. agno/workflow/step.py +609 -170
  303. agno/workflow/steps.py +73 -6
  304. agno/workflow/types.py +96 -21
  305. agno/workflow/workflow.py +2039 -262
  306. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
  307. agno-2.3.13.dist-info/RECORD +613 -0
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -679
  310. agno/tools/memori.py +0 -339
  311. agno-2.1.2.dist-info/RECORD +0 -543
  312. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
  313. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/db/redis/redis.py CHANGED
@@ -1,8 +1,11 @@
1
1
  import time
2
2
  from datetime import date, datetime, timedelta, timezone
3
- from typing import Any, Dict, List, Optional, Tuple, Union
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
4
4
  from uuid import uuid4
5
5
 
6
+ if TYPE_CHECKING:
7
+ from agno.tracing.schemas import Span, Trace
8
+
6
9
  from agno.db.base import BaseDb, SessionType
7
10
  from agno.db.redis.utils import (
8
11
  apply_filters,
@@ -10,14 +13,17 @@ from agno.db.redis.utils import (
10
13
  apply_sorting,
11
14
  calculate_date_metrics,
12
15
  create_index_entries,
16
+ deserialize_cultural_knowledge_from_db,
13
17
  deserialize_data,
14
18
  fetch_all_sessions_data,
15
19
  generate_redis_key,
16
20
  get_all_keys_for_table,
17
21
  get_dates_to_calculate_metrics_for,
18
22
  remove_index_entries,
23
+ serialize_cultural_knowledge_for_db,
19
24
  serialize_data,
20
25
  )
26
+ from agno.db.schemas.culture import CulturalKnowledge
21
27
  from agno.db.schemas.evals import EvalFilterType, EvalRunRecord, EvalType
22
28
  from agno.db.schemas.knowledge import KnowledgeRow
23
29
  from agno.db.schemas.memory import UserMemory
@@ -26,7 +32,7 @@ from agno.utils.log import log_debug, log_error, log_info
26
32
  from agno.utils.string import generate_id
27
33
 
28
34
  try:
29
- from redis import Redis
35
+ from redis import Redis, RedisCluster
30
36
  except ImportError:
31
37
  raise ImportError("`redis` not installed. Please install it using `pip install redis`")
32
38
 
@@ -35,7 +41,7 @@ class RedisDb(BaseDb):
35
41
  def __init__(
36
42
  self,
37
43
  id: Optional[str] = None,
38
- redis_client: Optional[Redis] = None,
44
+ redis_client: Optional[Union[Redis, RedisCluster]] = None,
39
45
  db_url: Optional[str] = None,
40
46
  db_prefix: str = "agno",
41
47
  expire: Optional[int] = None,
@@ -44,6 +50,9 @@ class RedisDb(BaseDb):
44
50
  metrics_table: Optional[str] = None,
45
51
  eval_table: Optional[str] = None,
46
52
  knowledge_table: Optional[str] = None,
53
+ culture_table: Optional[str] = None,
54
+ traces_table: Optional[str] = None,
55
+ spans_table: Optional[str] = None,
47
56
  ):
48
57
  """
49
58
  Interface for interacting with a Redis database.
@@ -53,6 +62,8 @@ class RedisDb(BaseDb):
53
62
  2. Use the db_url
54
63
  3. Raise an error if neither is provided
55
64
 
65
+ db_url only supports single-node Redis connections, if you need Redis Cluster support, provide a redis_client.
66
+
56
67
  Args:
57
68
  id (Optional[str]): The ID of the database.
58
69
  redis_client (Optional[Redis]): Redis client instance to use. If not provided a new client will be created.
@@ -64,6 +75,9 @@ class RedisDb(BaseDb):
64
75
  metrics_table (Optional[str]): Name of the table to store metrics
65
76
  eval_table (Optional[str]): Name of the table to store evaluation runs
66
77
  knowledge_table (Optional[str]): Name of the table to store knowledge documents
78
+ culture_table (Optional[str]): Name of the table to store cultural knowledge
79
+ traces_table (Optional[str]): Name of the table to store traces
80
+ spans_table (Optional[str]): Name of the table to store spans
67
81
 
68
82
  Raises:
69
83
  ValueError: If neither redis_client nor db_url is provided.
@@ -80,6 +94,9 @@ class RedisDb(BaseDb):
80
94
  metrics_table=metrics_table,
81
95
  eval_table=eval_table,
82
96
  knowledge_table=knowledge_table,
97
+ culture_table=culture_table,
98
+ traces_table=traces_table,
99
+ spans_table=spans_table,
83
100
  )
84
101
 
85
102
  self.db_prefix = db_prefix
@@ -94,6 +111,10 @@ class RedisDb(BaseDb):
94
111
 
95
112
  # -- DB methods --
96
113
 
114
+ def table_exists(self, table_name: str) -> bool:
115
+ """Redis implementation, always returns True."""
116
+ return True
117
+
97
118
  def _get_table_name(self, table_type: str) -> str:
98
119
  """Get the active table name for the given table type."""
99
120
  if table_type == "sessions":
@@ -111,6 +132,15 @@ class RedisDb(BaseDb):
111
132
  elif table_type == "knowledge":
112
133
  return self.knowledge_table_name
113
134
 
135
+ elif table_type == "culture":
136
+ return self.culture_table_name
137
+
138
+ elif table_type == "traces":
139
+ return self.trace_table_name
140
+
141
+ elif table_type == "spans":
142
+ return self.span_table_name
143
+
114
144
  else:
115
145
  raise ValueError(f"Unknown table type: {table_type}")
116
146
 
@@ -239,6 +269,14 @@ class RedisDb(BaseDb):
239
269
  log_error(f"Error getting all records for {table_type}: {e}")
240
270
  return []
241
271
 
272
+ def get_latest_schema_version(self):
273
+ """Get the latest version of the database schema."""
274
+ pass
275
+
276
+ def upsert_schema_version(self, version: str) -> None:
277
+ """Upsert the schema version into the database."""
278
+ pass
279
+
242
280
  # -- Session methods --
243
281
 
244
282
  def delete_session(self, session_id: str) -> bool:
@@ -318,8 +356,6 @@ class RedisDb(BaseDb):
318
356
  # Apply filters
319
357
  if user_id is not None and session.get("user_id") != user_id:
320
358
  return None
321
- if session_type is not None and session.get("session_type") != session_type:
322
- return None
323
359
 
324
360
  if not deserialize:
325
361
  return session
@@ -589,7 +625,7 @@ class RedisDb(BaseDb):
589
625
  raise e
590
626
 
591
627
  def upsert_sessions(
592
- self, sessions: List[Session], deserialize: Optional[bool] = True
628
+ self, sessions: List[Session], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
593
629
  ) -> List[Union[Session, Dict[str, Any]]]:
594
630
  """
595
631
  Bulk upsert multiple sessions for improved performance on large datasets.
@@ -820,12 +856,14 @@ class RedisDb(BaseDb):
820
856
  self,
821
857
  limit: Optional[int] = None,
822
858
  page: Optional[int] = None,
859
+ user_id: Optional[str] = None,
823
860
  ) -> Tuple[List[Dict[str, Any]], int]:
824
861
  """Get user memory stats from Redis.
825
862
 
826
863
  Args:
827
864
  limit (Optional[int]): The maximum number of stats to return.
828
865
  page (Optional[int]): The page number to return.
866
+ user_id (Optional[str]): User ID for filtering.
829
867
 
830
868
  Returns:
831
869
  Tuple[List[Dict[str, Any]], int]: A tuple containing the list of stats and the total number of stats.
@@ -840,6 +878,9 @@ class RedisDb(BaseDb):
840
878
  user_stats = {}
841
879
  for memory in all_memories:
842
880
  memory_user_id = memory.get("user_id")
881
+ # filter by user_id if provided
882
+ if user_id is not None and memory_user_id != user_id:
883
+ continue
843
884
  if memory_user_id is None:
844
885
  continue
845
886
 
@@ -892,6 +933,9 @@ class RedisDb(BaseDb):
892
933
  "memory_id": memory.memory_id,
893
934
  "memory": memory.memory,
894
935
  "topics": memory.topics,
936
+ "input": memory.input,
937
+ "feedback": memory.feedback,
938
+ "created_at": memory.created_at,
895
939
  "updated_at": int(time.time()),
896
940
  }
897
941
 
@@ -912,7 +956,7 @@ class RedisDb(BaseDb):
912
956
  raise e
913
957
 
914
958
  def upsert_memories(
915
- self, memories: List[UserMemory], deserialize: Optional[bool] = True
959
+ self, memories: List[UserMemory], deserialize: Optional[bool] = True, preserve_updated_at: bool = False
916
960
  ) -> List[Union[UserMemory, Dict[str, Any]]]:
917
961
  """
918
962
  Bulk upsert multiple user memories for improved performance on large datasets.
@@ -1475,3 +1519,623 @@ class RedisDb(BaseDb):
1475
1519
  except Exception as e:
1476
1520
  log_error(f"Error updating eval run name {eval_run_id}: {e}")
1477
1521
  raise
1522
+
1523
+ # -- Cultural Knowledge methods --
1524
+ def clear_cultural_knowledge(self) -> None:
1525
+ """Delete all cultural knowledge from the database.
1526
+
1527
+ Raises:
1528
+ Exception: If an error occurs during deletion.
1529
+ """
1530
+ try:
1531
+ keys = get_all_keys_for_table(redis_client=self.redis_client, prefix=self.db_prefix, table_type="culture")
1532
+
1533
+ if keys:
1534
+ self.redis_client.delete(*keys)
1535
+
1536
+ except Exception as e:
1537
+ log_error(f"Exception deleting all cultural knowledge: {e}")
1538
+ raise e
1539
+
1540
+ def delete_cultural_knowledge(self, id: str) -> None:
1541
+ """Delete cultural knowledge by ID.
1542
+
1543
+ Args:
1544
+ id (str): The ID of the cultural knowledge to delete.
1545
+
1546
+ Raises:
1547
+ Exception: If an error occurs during deletion.
1548
+ """
1549
+ try:
1550
+ if self._delete_record("culture", id, index_fields=["name", "agent_id", "team_id"]):
1551
+ log_debug(f"Successfully deleted cultural knowledge id: {id}")
1552
+ else:
1553
+ log_debug(f"No cultural knowledge found with id: {id}")
1554
+
1555
+ except Exception as e:
1556
+ log_error(f"Error deleting cultural knowledge: {e}")
1557
+ raise e
1558
+
1559
+ def get_cultural_knowledge(
1560
+ self, id: str, deserialize: Optional[bool] = True
1561
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1562
+ """Get cultural knowledge by ID.
1563
+
1564
+ Args:
1565
+ id (str): The ID of the cultural knowledge to retrieve.
1566
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge object. Defaults to True.
1567
+
1568
+ Returns:
1569
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The cultural knowledge if found, None otherwise.
1570
+
1571
+ Raises:
1572
+ Exception: If an error occurs during retrieval.
1573
+ """
1574
+ try:
1575
+ cultural_knowledge = self._get_record("culture", id)
1576
+
1577
+ if cultural_knowledge is None:
1578
+ return None
1579
+
1580
+ if not deserialize:
1581
+ return cultural_knowledge
1582
+
1583
+ return deserialize_cultural_knowledge_from_db(cultural_knowledge)
1584
+
1585
+ except Exception as e:
1586
+ log_error(f"Error getting cultural knowledge: {e}")
1587
+ raise e
1588
+
1589
+ def get_all_cultural_knowledge(
1590
+ self,
1591
+ agent_id: Optional[str] = None,
1592
+ team_id: Optional[str] = None,
1593
+ name: Optional[str] = None,
1594
+ limit: Optional[int] = None,
1595
+ page: Optional[int] = None,
1596
+ sort_by: Optional[str] = None,
1597
+ sort_order: Optional[str] = None,
1598
+ deserialize: Optional[bool] = True,
1599
+ ) -> Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1600
+ """Get all cultural knowledge with filtering and pagination.
1601
+
1602
+ Args:
1603
+ agent_id (Optional[str]): Filter by agent ID.
1604
+ team_id (Optional[str]): Filter by team ID.
1605
+ name (Optional[str]): Filter by name (case-insensitive partial match).
1606
+ limit (Optional[int]): Maximum number of results to return.
1607
+ page (Optional[int]): Page number for pagination.
1608
+ sort_by (Optional[str]): Field to sort by.
1609
+ sort_order (Optional[str]): Sort order ('asc' or 'desc').
1610
+ deserialize (Optional[bool]): Whether to deserialize to CulturalKnowledge objects. Defaults to True.
1611
+
1612
+ Returns:
1613
+ Union[List[CulturalKnowledge], Tuple[List[Dict[str, Any]], int]]:
1614
+ - When deserialize=True: List of CulturalKnowledge objects
1615
+ - When deserialize=False: Tuple with list of dictionaries and total count
1616
+
1617
+ Raises:
1618
+ Exception: If an error occurs during retrieval.
1619
+ """
1620
+ try:
1621
+ all_cultural_knowledge = self._get_all_records("culture")
1622
+
1623
+ # Apply filters
1624
+ filtered_items = []
1625
+ for item in all_cultural_knowledge:
1626
+ if agent_id is not None and item.get("agent_id") != agent_id:
1627
+ continue
1628
+ if team_id is not None and item.get("team_id") != team_id:
1629
+ continue
1630
+ if name is not None and name.lower() not in item.get("name", "").lower():
1631
+ continue
1632
+
1633
+ filtered_items.append(item)
1634
+
1635
+ sorted_items = apply_sorting(records=filtered_items, sort_by=sort_by, sort_order=sort_order)
1636
+ paginated_items = apply_pagination(records=sorted_items, limit=limit, page=page)
1637
+
1638
+ if not deserialize:
1639
+ return paginated_items, len(filtered_items)
1640
+
1641
+ return [deserialize_cultural_knowledge_from_db(item) for item in paginated_items]
1642
+
1643
+ except Exception as e:
1644
+ log_error(f"Error getting all cultural knowledge: {e}")
1645
+ raise e
1646
+
1647
+ def upsert_cultural_knowledge(
1648
+ self, cultural_knowledge: CulturalKnowledge, deserialize: Optional[bool] = True
1649
+ ) -> Optional[Union[CulturalKnowledge, Dict[str, Any]]]:
1650
+ """Upsert cultural knowledge in Redis.
1651
+
1652
+ Args:
1653
+ cultural_knowledge (CulturalKnowledge): The cultural knowledge to upsert.
1654
+ deserialize (Optional[bool]): Whether to deserialize the result. Defaults to True.
1655
+
1656
+ Returns:
1657
+ Optional[Union[CulturalKnowledge, Dict[str, Any]]]: The upserted cultural knowledge.
1658
+
1659
+ Raises:
1660
+ Exception: If an error occurs during upsert.
1661
+ """
1662
+ try:
1663
+ # Serialize content, categories, and notes into a dict for DB storage
1664
+ content_dict = serialize_cultural_knowledge_for_db(cultural_knowledge)
1665
+ item_id = cultural_knowledge.id or str(uuid4())
1666
+
1667
+ # Create the item dict with serialized content
1668
+ data = {
1669
+ "id": item_id,
1670
+ "name": cultural_knowledge.name,
1671
+ "summary": cultural_knowledge.summary,
1672
+ "content": content_dict if content_dict else None,
1673
+ "metadata": cultural_knowledge.metadata,
1674
+ "input": cultural_knowledge.input,
1675
+ "created_at": cultural_knowledge.created_at,
1676
+ "updated_at": int(time.time()),
1677
+ "agent_id": cultural_knowledge.agent_id,
1678
+ "team_id": cultural_knowledge.team_id,
1679
+ }
1680
+
1681
+ success = self._store_record("culture", item_id, data, index_fields=["name", "agent_id", "team_id"])
1682
+
1683
+ if not success:
1684
+ return None
1685
+
1686
+ if not deserialize:
1687
+ return data
1688
+
1689
+ return deserialize_cultural_knowledge_from_db(data)
1690
+
1691
+ except Exception as e:
1692
+ log_error(f"Error upserting cultural knowledge: {e}")
1693
+ raise e
1694
+
1695
+ # --- Traces ---
1696
+ def upsert_trace(self, trace: "Trace") -> None:
1697
+ """Create or update a single trace record in the database.
1698
+
1699
+ Args:
1700
+ trace: The Trace object to store (one per trace_id).
1701
+ """
1702
+ try:
1703
+ # Check if trace already exists
1704
+ existing = self._get_record("traces", trace.trace_id)
1705
+
1706
+ if existing:
1707
+ # workflow (level 3) > team (level 2) > agent (level 1) > child/unknown (level 0)
1708
+ def get_component_level(
1709
+ workflow_id: Optional[str], team_id: Optional[str], agent_id: Optional[str], name: str
1710
+ ) -> int:
1711
+ # Check if name indicates a root span
1712
+ is_root_name = ".run" in name or ".arun" in name
1713
+
1714
+ if not is_root_name:
1715
+ return 0 # Child span (not a root)
1716
+ elif workflow_id:
1717
+ return 3 # Workflow root
1718
+ elif team_id:
1719
+ return 2 # Team root
1720
+ elif agent_id:
1721
+ return 1 # Agent root
1722
+ else:
1723
+ return 0 # Unknown
1724
+
1725
+ existing_level = get_component_level(
1726
+ existing.get("workflow_id"),
1727
+ existing.get("team_id"),
1728
+ existing.get("agent_id"),
1729
+ existing.get("name", ""),
1730
+ )
1731
+ new_level = get_component_level(trace.workflow_id, trace.team_id, trace.agent_id, trace.name)
1732
+
1733
+ # Only update name if new trace is from a higher or equal level
1734
+ should_update_name = new_level > existing_level
1735
+
1736
+ # Parse existing start_time to calculate correct duration
1737
+ existing_start_time_str = existing.get("start_time")
1738
+ if isinstance(existing_start_time_str, str):
1739
+ existing_start_time = datetime.fromisoformat(existing_start_time_str.replace("Z", "+00:00"))
1740
+ else:
1741
+ existing_start_time = trace.start_time
1742
+
1743
+ recalculated_duration_ms = int((trace.end_time - existing_start_time).total_seconds() * 1000)
1744
+
1745
+ # Update existing record
1746
+ existing["end_time"] = trace.end_time.isoformat()
1747
+ existing["duration_ms"] = recalculated_duration_ms
1748
+ existing["status"] = trace.status
1749
+ if should_update_name:
1750
+ existing["name"] = trace.name
1751
+
1752
+ # Update context fields ONLY if new value is not None (preserve non-null values)
1753
+ if trace.run_id is not None:
1754
+ existing["run_id"] = trace.run_id
1755
+ if trace.session_id is not None:
1756
+ existing["session_id"] = trace.session_id
1757
+ if trace.user_id is not None:
1758
+ existing["user_id"] = trace.user_id
1759
+ if trace.agent_id is not None:
1760
+ existing["agent_id"] = trace.agent_id
1761
+ if trace.team_id is not None:
1762
+ existing["team_id"] = trace.team_id
1763
+ if trace.workflow_id is not None:
1764
+ existing["workflow_id"] = trace.workflow_id
1765
+
1766
+ log_debug(
1767
+ f" Updating trace with context: run_id={existing.get('run_id', 'unchanged')}, "
1768
+ f"session_id={existing.get('session_id', 'unchanged')}, "
1769
+ f"user_id={existing.get('user_id', 'unchanged')}, "
1770
+ f"agent_id={existing.get('agent_id', 'unchanged')}, "
1771
+ f"team_id={existing.get('team_id', 'unchanged')}, "
1772
+ )
1773
+
1774
+ self._store_record(
1775
+ "traces",
1776
+ trace.trace_id,
1777
+ existing,
1778
+ index_fields=["run_id", "session_id", "user_id", "agent_id", "team_id", "workflow_id", "status"],
1779
+ )
1780
+ else:
1781
+ trace_dict = trace.to_dict()
1782
+ trace_dict.pop("total_spans", None)
1783
+ trace_dict.pop("error_count", None)
1784
+ self._store_record(
1785
+ "traces",
1786
+ trace.trace_id,
1787
+ trace_dict,
1788
+ index_fields=["run_id", "session_id", "user_id", "agent_id", "team_id", "workflow_id", "status"],
1789
+ )
1790
+
1791
+ except Exception as e:
1792
+ log_error(f"Error creating trace: {e}")
1793
+ # Don't raise - tracing should not break the main application flow
1794
+
1795
+ def get_trace(
1796
+ self,
1797
+ trace_id: Optional[str] = None,
1798
+ run_id: Optional[str] = None,
1799
+ ):
1800
+ """Get a single trace by trace_id or other filters.
1801
+
1802
+ Args:
1803
+ trace_id: The unique trace identifier.
1804
+ run_id: Filter by run ID (returns first match).
1805
+
1806
+ Returns:
1807
+ Optional[Trace]: The trace if found, None otherwise.
1808
+
1809
+ Note:
1810
+ If multiple filters are provided, trace_id takes precedence.
1811
+ For other filters, the most recent trace is returned.
1812
+ """
1813
+ try:
1814
+ from agno.tracing.schemas import Trace as TraceSchema
1815
+
1816
+ if trace_id:
1817
+ result = self._get_record("traces", trace_id)
1818
+ if result:
1819
+ # Calculate total_spans and error_count
1820
+ all_spans = self._get_all_records("spans")
1821
+ trace_spans = [s for s in all_spans if s.get("trace_id") == trace_id]
1822
+ result["total_spans"] = len(trace_spans)
1823
+ result["error_count"] = len([s for s in trace_spans if s.get("status_code") == "ERROR"])
1824
+ return TraceSchema.from_dict(result)
1825
+ return None
1826
+
1827
+ elif run_id:
1828
+ all_traces = self._get_all_records("traces")
1829
+ matching = [t for t in all_traces if t.get("run_id") == run_id]
1830
+ if matching:
1831
+ # Sort by start_time descending and get most recent
1832
+ matching.sort(key=lambda x: x.get("start_time", ""), reverse=True)
1833
+ result = matching[0]
1834
+ # Calculate total_spans and error_count
1835
+ all_spans = self._get_all_records("spans")
1836
+ trace_spans = [s for s in all_spans if s.get("trace_id") == result.get("trace_id")]
1837
+ result["total_spans"] = len(trace_spans)
1838
+ result["error_count"] = len([s for s in trace_spans if s.get("status_code") == "ERROR"])
1839
+ return TraceSchema.from_dict(result)
1840
+ return None
1841
+
1842
+ else:
1843
+ log_debug("get_trace called without any filter parameters")
1844
+ return None
1845
+
1846
+ except Exception as e:
1847
+ log_error(f"Error getting trace: {e}")
1848
+ return None
1849
+
1850
+ def get_traces(
1851
+ self,
1852
+ run_id: Optional[str] = None,
1853
+ session_id: Optional[str] = None,
1854
+ user_id: Optional[str] = None,
1855
+ agent_id: Optional[str] = None,
1856
+ team_id: Optional[str] = None,
1857
+ workflow_id: Optional[str] = None,
1858
+ status: Optional[str] = None,
1859
+ start_time: Optional[datetime] = None,
1860
+ end_time: Optional[datetime] = None,
1861
+ limit: Optional[int] = 20,
1862
+ page: Optional[int] = 1,
1863
+ ) -> tuple[List, int]:
1864
+ """Get traces matching the provided filters.
1865
+
1866
+ Args:
1867
+ run_id: Filter by run ID.
1868
+ session_id: Filter by session ID.
1869
+ user_id: Filter by user ID.
1870
+ agent_id: Filter by agent ID.
1871
+ team_id: Filter by team ID.
1872
+ workflow_id: Filter by workflow ID.
1873
+ status: Filter by status (OK, ERROR, UNSET).
1874
+ start_time: Filter traces starting after this datetime.
1875
+ end_time: Filter traces ending before this datetime.
1876
+ limit: Maximum number of traces to return per page.
1877
+ page: Page number (1-indexed).
1878
+
1879
+ Returns:
1880
+ tuple[List[Trace], int]: Tuple of (list of matching traces, total count).
1881
+ """
1882
+ try:
1883
+ from agno.tracing.schemas import Trace as TraceSchema
1884
+
1885
+ log_debug(
1886
+ f"get_traces called with filters: run_id={run_id}, session_id={session_id}, "
1887
+ f"user_id={user_id}, agent_id={agent_id}, page={page}, limit={limit}"
1888
+ )
1889
+
1890
+ all_traces = self._get_all_records("traces")
1891
+ all_spans = self._get_all_records("spans")
1892
+
1893
+ # Apply filters
1894
+ filtered_traces = []
1895
+ for trace in all_traces:
1896
+ if run_id and trace.get("run_id") != run_id:
1897
+ continue
1898
+ if session_id and trace.get("session_id") != session_id:
1899
+ continue
1900
+ if user_id and trace.get("user_id") != user_id:
1901
+ continue
1902
+ if agent_id and trace.get("agent_id") != agent_id:
1903
+ continue
1904
+ if team_id and trace.get("team_id") != team_id:
1905
+ continue
1906
+ if workflow_id and trace.get("workflow_id") != workflow_id:
1907
+ continue
1908
+ if status and trace.get("status") != status:
1909
+ continue
1910
+ if start_time:
1911
+ trace_start = trace.get("start_time", "")
1912
+ if trace_start and trace_start < start_time.isoformat():
1913
+ continue
1914
+ if end_time:
1915
+ trace_end = trace.get("end_time", "")
1916
+ if trace_end and trace_end > end_time.isoformat():
1917
+ continue
1918
+
1919
+ filtered_traces.append(trace)
1920
+
1921
+ total_count = len(filtered_traces)
1922
+
1923
+ # Sort by start_time descending
1924
+ filtered_traces.sort(key=lambda x: x.get("start_time", ""), reverse=True)
1925
+
1926
+ # Apply pagination
1927
+ paginated_traces = apply_pagination(records=filtered_traces, limit=limit, page=page)
1928
+
1929
+ traces = []
1930
+ for row in paginated_traces:
1931
+ # Calculate total_spans and error_count
1932
+ trace_spans = [s for s in all_spans if s.get("trace_id") == row.get("trace_id")]
1933
+ row["total_spans"] = len(trace_spans)
1934
+ row["error_count"] = len([s for s in trace_spans if s.get("status_code") == "ERROR"])
1935
+ traces.append(TraceSchema.from_dict(row))
1936
+
1937
+ return traces, total_count
1938
+
1939
+ except Exception as e:
1940
+ log_error(f"Error getting traces: {e}")
1941
+ return [], 0
1942
+
1943
+ def get_trace_stats(
1944
+ self,
1945
+ user_id: Optional[str] = None,
1946
+ agent_id: Optional[str] = None,
1947
+ team_id: Optional[str] = None,
1948
+ workflow_id: Optional[str] = None,
1949
+ start_time: Optional[datetime] = None,
1950
+ end_time: Optional[datetime] = None,
1951
+ limit: Optional[int] = 20,
1952
+ page: Optional[int] = 1,
1953
+ ) -> tuple[List[Dict[str, Any]], int]:
1954
+ """Get trace statistics grouped by session.
1955
+
1956
+ Args:
1957
+ user_id: Filter by user ID.
1958
+ agent_id: Filter by agent ID.
1959
+ team_id: Filter by team ID.
1960
+ workflow_id: Filter by workflow ID.
1961
+ start_time: Filter sessions with traces created after this datetime.
1962
+ end_time: Filter sessions with traces created before this datetime.
1963
+ limit: Maximum number of sessions to return per page.
1964
+ page: Page number (1-indexed).
1965
+
1966
+ Returns:
1967
+ tuple[List[Dict], int]: Tuple of (list of session stats dicts, total count).
1968
+ Each dict contains: session_id, user_id, agent_id, team_id, total_traces,
1969
+ first_trace_at, last_trace_at.
1970
+ """
1971
+ try:
1972
+ log_debug(
1973
+ f"get_trace_stats called with filters: user_id={user_id}, agent_id={agent_id}, "
1974
+ f"workflow_id={workflow_id}, team_id={team_id}, "
1975
+ f"start_time={start_time}, end_time={end_time}, page={page}, limit={limit}"
1976
+ )
1977
+
1978
+ all_traces = self._get_all_records("traces")
1979
+
1980
+ # Filter traces and group by session_id
1981
+ session_stats: Dict[str, Dict[str, Any]] = {}
1982
+ for trace in all_traces:
1983
+ trace_session_id = trace.get("session_id")
1984
+ if not trace_session_id:
1985
+ continue
1986
+
1987
+ # Apply filters
1988
+ if user_id and trace.get("user_id") != user_id:
1989
+ continue
1990
+ if agent_id and trace.get("agent_id") != agent_id:
1991
+ continue
1992
+ if team_id and trace.get("team_id") != team_id:
1993
+ continue
1994
+ if workflow_id and trace.get("workflow_id") != workflow_id:
1995
+ continue
1996
+
1997
+ created_at = trace.get("created_at", "")
1998
+ if start_time and created_at < start_time.isoformat():
1999
+ continue
2000
+ if end_time and created_at > end_time.isoformat():
2001
+ continue
2002
+
2003
+ if trace_session_id not in session_stats:
2004
+ session_stats[trace_session_id] = {
2005
+ "session_id": trace_session_id,
2006
+ "user_id": trace.get("user_id"),
2007
+ "agent_id": trace.get("agent_id"),
2008
+ "team_id": trace.get("team_id"),
2009
+ "workflow_id": trace.get("workflow_id"),
2010
+ "total_traces": 0,
2011
+ "first_trace_at": created_at,
2012
+ "last_trace_at": created_at,
2013
+ }
2014
+
2015
+ session_stats[trace_session_id]["total_traces"] += 1
2016
+ if created_at < session_stats[trace_session_id]["first_trace_at"]:
2017
+ session_stats[trace_session_id]["first_trace_at"] = created_at
2018
+ if created_at > session_stats[trace_session_id]["last_trace_at"]:
2019
+ session_stats[trace_session_id]["last_trace_at"] = created_at
2020
+
2021
+ # Convert to list and sort by last_trace_at descending
2022
+ stats_list = list(session_stats.values())
2023
+ stats_list.sort(key=lambda x: x.get("last_trace_at", ""), reverse=True)
2024
+
2025
+ total_count = len(stats_list)
2026
+
2027
+ # Apply pagination
2028
+ paginated_stats = apply_pagination(records=stats_list, limit=limit, page=page)
2029
+
2030
+ # Convert ISO strings to datetime objects
2031
+ for stat in paginated_stats:
2032
+ first_trace_at_str = stat["first_trace_at"]
2033
+ last_trace_at_str = stat["last_trace_at"]
2034
+ stat["first_trace_at"] = datetime.fromisoformat(first_trace_at_str.replace("Z", "+00:00"))
2035
+ stat["last_trace_at"] = datetime.fromisoformat(last_trace_at_str.replace("Z", "+00:00"))
2036
+
2037
+ return paginated_stats, total_count
2038
+
2039
+ except Exception as e:
2040
+ log_error(f"Error getting trace stats: {e}")
2041
+ return [], 0
2042
+
2043
+ # --- Spans ---
2044
+ def create_span(self, span: "Span") -> None:
2045
+ """Create a single span in the database.
2046
+
2047
+ Args:
2048
+ span: The Span object to store.
2049
+ """
2050
+ try:
2051
+ self._store_record(
2052
+ "spans",
2053
+ span.span_id,
2054
+ span.to_dict(),
2055
+ index_fields=["trace_id", "parent_span_id"],
2056
+ )
2057
+
2058
+ except Exception as e:
2059
+ log_error(f"Error creating span: {e}")
2060
+
2061
+ def create_spans(self, spans: List) -> None:
2062
+ """Create multiple spans in the database as a batch.
2063
+
2064
+ Args:
2065
+ spans: List of Span objects to store.
2066
+ """
2067
+ if not spans:
2068
+ return
2069
+
2070
+ try:
2071
+ for span in spans:
2072
+ self._store_record(
2073
+ "spans",
2074
+ span.span_id,
2075
+ span.to_dict(),
2076
+ index_fields=["trace_id", "parent_span_id"],
2077
+ )
2078
+
2079
+ except Exception as e:
2080
+ log_error(f"Error creating spans batch: {e}")
2081
+
2082
+ def get_span(self, span_id: str):
2083
+ """Get a single span by its span_id.
2084
+
2085
+ Args:
2086
+ span_id: The unique span identifier.
2087
+
2088
+ Returns:
2089
+ Optional[Span]: The span if found, None otherwise.
2090
+ """
2091
+ try:
2092
+ from agno.tracing.schemas import Span as SpanSchema
2093
+
2094
+ result = self._get_record("spans", span_id)
2095
+ if result:
2096
+ return SpanSchema.from_dict(result)
2097
+ return None
2098
+
2099
+ except Exception as e:
2100
+ log_error(f"Error getting span: {e}")
2101
+ return None
2102
+
2103
+ def get_spans(
2104
+ self,
2105
+ trace_id: Optional[str] = None,
2106
+ parent_span_id: Optional[str] = None,
2107
+ limit: Optional[int] = 1000,
2108
+ ) -> List:
2109
+ """Get spans matching the provided filters.
2110
+
2111
+ Args:
2112
+ trace_id: Filter by trace ID.
2113
+ parent_span_id: Filter by parent span ID.
2114
+ limit: Maximum number of spans to return.
2115
+
2116
+ Returns:
2117
+ List[Span]: List of matching spans.
2118
+ """
2119
+ try:
2120
+ from agno.tracing.schemas import Span as SpanSchema
2121
+
2122
+ all_spans = self._get_all_records("spans")
2123
+
2124
+ # Apply filters
2125
+ filtered_spans = []
2126
+ for span in all_spans:
2127
+ if trace_id and span.get("trace_id") != trace_id:
2128
+ continue
2129
+ if parent_span_id and span.get("parent_span_id") != parent_span_id:
2130
+ continue
2131
+ filtered_spans.append(span)
2132
+
2133
+ # Apply limit
2134
+ if limit:
2135
+ filtered_spans = filtered_spans[:limit]
2136
+
2137
+ return [SpanSchema.from_dict(s) for s in filtered_spans]
2138
+
2139
+ except Exception as e:
2140
+ log_error(f"Error getting spans: {e}")
2141
+ return []