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.
- agno/agent/agent.py +5540 -2273
- agno/api/api.py +2 -0
- agno/api/os.py +1 -1
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/__init__.py +3 -0
- agno/culture/manager.py +956 -0
- agno/db/async_postgres/__init__.py +3 -0
- agno/db/base.py +689 -6
- agno/db/dynamo/dynamo.py +933 -37
- agno/db/dynamo/schemas.py +174 -10
- agno/db/dynamo/utils.py +63 -4
- agno/db/firestore/firestore.py +831 -9
- agno/db/firestore/schemas.py +51 -0
- agno/db/firestore/utils.py +102 -4
- agno/db/gcs_json/gcs_json_db.py +660 -12
- agno/db/gcs_json/utils.py +60 -26
- agno/db/in_memory/in_memory_db.py +287 -14
- agno/db/in_memory/utils.py +60 -2
- agno/db/json/json_db.py +590 -14
- agno/db/json/utils.py +60 -26
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/v1_to_v2.py +43 -13
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +938 -0
- agno/db/mongo/__init__.py +15 -1
- agno/db/mongo/async_mongo.py +2760 -0
- agno/db/mongo/mongo.py +879 -11
- agno/db/mongo/schemas.py +42 -0
- agno/db/mongo/utils.py +80 -8
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2912 -0
- agno/db/mysql/mysql.py +946 -68
- agno/db/mysql/schemas.py +72 -10
- agno/db/mysql/utils.py +198 -7
- agno/db/postgres/__init__.py +2 -1
- agno/db/postgres/async_postgres.py +2579 -0
- agno/db/postgres/postgres.py +942 -57
- agno/db/postgres/schemas.py +81 -18
- agno/db/postgres/utils.py +164 -2
- agno/db/redis/redis.py +671 -7
- agno/db/redis/schemas.py +50 -0
- agno/db/redis/utils.py +65 -7
- agno/db/schemas/__init__.py +2 -1
- agno/db/schemas/culture.py +120 -0
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +17 -2
- agno/db/singlestore/schemas.py +63 -0
- agno/db/singlestore/singlestore.py +949 -83
- agno/db/singlestore/utils.py +60 -2
- agno/db/sqlite/__init__.py +2 -1
- agno/db/sqlite/async_sqlite.py +2911 -0
- agno/db/sqlite/schemas.py +62 -0
- agno/db/sqlite/sqlite.py +965 -46
- agno/db/sqlite/utils.py +169 -8
- agno/db/surrealdb/__init__.py +3 -0
- agno/db/surrealdb/metrics.py +292 -0
- agno/db/surrealdb/models.py +334 -0
- agno/db/surrealdb/queries.py +71 -0
- agno/db/surrealdb/surrealdb.py +1908 -0
- agno/db/surrealdb/utils.py +147 -0
- agno/db/utils.py +2 -0
- agno/eval/__init__.py +10 -0
- agno/eval/accuracy.py +75 -55
- agno/eval/agent_as_judge.py +861 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +16 -7
- agno/eval/reliability.py +28 -16
- agno/eval/utils.py +35 -17
- agno/exceptions.py +27 -2
- agno/filters.py +354 -0
- agno/guardrails/prompt_injection.py +1 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +1 -1
- agno/knowledge/chunking/agentic.py +13 -10
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/semantic.py +9 -4
- agno/knowledge/chunking/strategy.py +59 -15
- agno/knowledge/embedder/fastembed.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/ollama.py +8 -0
- agno/knowledge/embedder/openai.py +8 -8
- agno/knowledge/embedder/sentence_transformer.py +6 -2
- agno/knowledge/embedder/vllm.py +262 -0
- agno/knowledge/knowledge.py +1618 -318
- agno/knowledge/reader/base.py +6 -2
- agno/knowledge/reader/csv_reader.py +8 -10
- agno/knowledge/reader/docx_reader.py +5 -6
- agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
- agno/knowledge/reader/json_reader.py +5 -4
- agno/knowledge/reader/markdown_reader.py +8 -8
- agno/knowledge/reader/pdf_reader.py +17 -19
- agno/knowledge/reader/pptx_reader.py +101 -0
- agno/knowledge/reader/reader_factory.py +32 -3
- agno/knowledge/reader/s3_reader.py +3 -3
- agno/knowledge/reader/tavily_reader.py +193 -0
- agno/knowledge/reader/text_reader.py +22 -10
- agno/knowledge/reader/web_search_reader.py +1 -48
- agno/knowledge/reader/website_reader.py +10 -10
- agno/knowledge/reader/wikipedia_reader.py +33 -1
- agno/knowledge/types.py +1 -0
- agno/knowledge/utils.py +72 -7
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +544 -83
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +66 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +17 -0
- agno/models/anthropic/claude.py +515 -40
- agno/models/aws/bedrock.py +102 -21
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +41 -19
- agno/models/azure/openai_chat.py +39 -8
- agno/models/base.py +1249 -525
- agno/models/cerebras/cerebras.py +91 -21
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +40 -6
- agno/models/cometapi/cometapi.py +18 -1
- agno/models/dashscope/dashscope.py +2 -3
- agno/models/deepinfra/deepinfra.py +18 -1
- agno/models/deepseek/deepseek.py +69 -3
- agno/models/fireworks/fireworks.py +18 -1
- agno/models/google/gemini.py +877 -80
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +51 -18
- agno/models/huggingface/huggingface.py +17 -6
- agno/models/ibm/watsonx.py +16 -6
- agno/models/internlm/internlm.py +18 -1
- agno/models/langdb/langdb.py +13 -1
- agno/models/litellm/chat.py +44 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +28 -5
- agno/models/meta/llama.py +47 -14
- agno/models/meta/llama_openai.py +22 -17
- agno/models/mistral/mistral.py +8 -4
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/chat.py +24 -8
- agno/models/openai/chat.py +104 -29
- agno/models/openai/responses.py +101 -81
- agno/models/openrouter/openrouter.py +60 -3
- agno/models/perplexity/perplexity.py +17 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +24 -4
- agno/models/response.py +73 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/utils.py +254 -8
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/__init__.py +0 -0
- agno/models/vertexai/claude.py +190 -0
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +549 -152
- agno/os/auth.py +190 -3
- agno/os/config.py +23 -0
- agno/os/interfaces/a2a/router.py +8 -11
- agno/os/interfaces/a2a/utils.py +1 -1
- agno/os/interfaces/agui/router.py +18 -3
- agno/os/interfaces/agui/utils.py +152 -39
- agno/os/interfaces/slack/router.py +55 -37
- agno/os/interfaces/slack/slack.py +9 -1
- agno/os/interfaces/whatsapp/router.py +0 -1
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/mcp.py +110 -52
- agno/os/middleware/__init__.py +2 -0
- agno/os/middleware/jwt.py +676 -112
- agno/os/router.py +40 -1478
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +599 -0
- agno/os/routers/agents/schema.py +261 -0
- agno/os/routers/evals/evals.py +96 -39
- agno/os/routers/evals/schemas.py +65 -33
- agno/os/routers/evals/utils.py +80 -10
- agno/os/routers/health.py +10 -4
- agno/os/routers/knowledge/knowledge.py +196 -38
- agno/os/routers/knowledge/schemas.py +82 -22
- agno/os/routers/memory/memory.py +279 -52
- agno/os/routers/memory/schemas.py +46 -17
- agno/os/routers/metrics/metrics.py +20 -8
- agno/os/routers/metrics/schemas.py +16 -16
- agno/os/routers/session/session.py +462 -34
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +512 -0
- agno/os/routers/teams/schema.py +257 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +499 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +624 -0
- agno/os/routers/workflows/schema.py +75 -0
- agno/os/schema.py +256 -693
- agno/os/scopes.py +469 -0
- agno/os/utils.py +514 -36
- agno/reasoning/anthropic.py +80 -0
- agno/reasoning/gemini.py +73 -0
- agno/reasoning/openai.py +5 -0
- agno/reasoning/vertexai.py +76 -0
- agno/run/__init__.py +6 -0
- agno/run/agent.py +155 -32
- agno/run/base.py +55 -3
- agno/run/requirement.py +181 -0
- agno/run/team.py +125 -38
- agno/run/workflow.py +72 -18
- agno/session/agent.py +102 -89
- agno/session/summary.py +56 -15
- agno/session/team.py +164 -90
- agno/session/workflow.py +405 -40
- agno/table.py +10 -0
- agno/team/team.py +3974 -1903
- agno/tools/dalle.py +2 -4
- agno/tools/eleven_labs.py +23 -25
- agno/tools/exa.py +21 -16
- agno/tools/file.py +153 -23
- agno/tools/file_generation.py +16 -10
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +193 -38
- agno/tools/gmail.py +238 -14
- agno/tools/google_drive.py +271 -0
- agno/tools/googlecalendar.py +36 -8
- agno/tools/googlesheets.py +20 -5
- agno/tools/jira.py +20 -0
- agno/tools/mcp/__init__.py +10 -0
- agno/tools/mcp/mcp.py +331 -0
- agno/tools/mcp/multi_mcp.py +347 -0
- agno/tools/mcp/params.py +24 -0
- agno/tools/mcp_toolbox.py +3 -3
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/notion.py +204 -0
- agno/tools/parallel.py +314 -0
- agno/tools/postgres.py +76 -36
- agno/tools/redshift.py +406 -0
- agno/tools/scrapegraph.py +1 -1
- agno/tools/shopify.py +1519 -0
- agno/tools/slack.py +18 -3
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +146 -0
- agno/tools/toolkit.py +25 -0
- agno/tools/workflow.py +8 -1
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +157 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +111 -0
- agno/utils/agent.py +938 -0
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +151 -3
- agno/utils/gemini.py +15 -5
- agno/utils/hooks.py +118 -4
- agno/utils/http.py +113 -2
- agno/utils/knowledge.py +12 -5
- agno/utils/log.py +1 -0
- agno/utils/mcp.py +92 -2
- agno/utils/media.py +187 -1
- agno/utils/merge_dict.py +3 -3
- agno/utils/message.py +60 -0
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +49 -14
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +109 -16
- agno/utils/print_response/team.py +223 -30
- agno/utils/print_response/workflow.py +251 -34
- agno/utils/streamlit.py +1 -1
- agno/utils/team.py +98 -9
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +39 -7
- agno/vectordb/cassandra/cassandra.py +21 -5
- agno/vectordb/chroma/chromadb.py +43 -12
- agno/vectordb/clickhouse/clickhousedb.py +21 -5
- agno/vectordb/couchbase/couchbase.py +29 -5
- agno/vectordb/lancedb/lance_db.py +92 -181
- agno/vectordb/langchaindb/langchaindb.py +24 -4
- agno/vectordb/lightrag/lightrag.py +17 -3
- agno/vectordb/llamaindex/llamaindexdb.py +25 -5
- agno/vectordb/milvus/milvus.py +50 -37
- agno/vectordb/mongodb/__init__.py +7 -1
- agno/vectordb/mongodb/mongodb.py +36 -30
- agno/vectordb/pgvector/pgvector.py +201 -77
- agno/vectordb/pineconedb/pineconedb.py +41 -23
- agno/vectordb/qdrant/qdrant.py +67 -54
- agno/vectordb/redis/__init__.py +9 -0
- agno/vectordb/redis/redisdb.py +682 -0
- agno/vectordb/singlestore/singlestore.py +50 -29
- agno/vectordb/surrealdb/surrealdb.py +31 -41
- agno/vectordb/upstashdb/upstashdb.py +34 -6
- agno/vectordb/weaviate/weaviate.py +53 -14
- agno/workflow/__init__.py +2 -0
- agno/workflow/agent.py +299 -0
- agno/workflow/condition.py +120 -18
- agno/workflow/loop.py +77 -10
- agno/workflow/parallel.py +231 -143
- agno/workflow/router.py +118 -17
- agno/workflow/step.py +609 -170
- agno/workflow/steps.py +73 -6
- agno/workflow/types.py +96 -21
- agno/workflow/workflow.py +2039 -262
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
- agno-2.3.13.dist-info/RECORD +613 -0
- agno/tools/googlesearch.py +0 -98
- agno/tools/mcp.py +0 -679
- agno/tools/memori.py +0 -339
- agno-2.1.2.dist-info/RECORD +0 -543
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
- {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/db/json/utils.py
CHANGED
|
@@ -5,33 +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.
|
|
9
|
-
from agno.run.agent import RunOutput
|
|
10
|
-
from agno.run.team import TeamRunOutput
|
|
11
|
-
from agno.session.summary import SessionSummary
|
|
8
|
+
from agno.db.schemas.culture import CulturalKnowledge
|
|
12
9
|
from agno.utils.log import log_debug
|
|
13
10
|
|
|
14
11
|
|
|
15
|
-
def hydrate_session(session: dict) -> dict:
|
|
16
|
-
"""Convert nested dictionaries to their corresponding object types.
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
session (dict): The session dictionary to hydrate.
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
dict: The hydrated session dictionary.
|
|
23
|
-
"""
|
|
24
|
-
if session.get("summary") is not None:
|
|
25
|
-
session["summary"] = SessionSummary.from_dict(session["summary"])
|
|
26
|
-
if session.get("runs") is not None:
|
|
27
|
-
if session["session_type"] == SessionType.AGENT:
|
|
28
|
-
session["runs"] = [RunOutput.from_dict(run) for run in session["runs"]]
|
|
29
|
-
elif session["session_type"] == SessionType.TEAM:
|
|
30
|
-
session["runs"] = [TeamRunOutput.from_dict(run) for run in session["runs"]]
|
|
31
|
-
|
|
32
|
-
return session
|
|
33
|
-
|
|
34
|
-
|
|
35
12
|
def apply_sorting(
|
|
36
13
|
data: List[Dict[str, Any]], sort_by: Optional[str] = None, sort_order: Optional[str] = None
|
|
37
14
|
) -> List[Dict[str, Any]]:
|
|
@@ -101,7 +78,7 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
|
|
|
101
78
|
all_user_ids = set()
|
|
102
79
|
|
|
103
80
|
for session_type, sessions_count_key, runs_count_key in session_types:
|
|
104
|
-
sessions = sessions_data.get(session_type, [])
|
|
81
|
+
sessions = sessions_data.get(session_type, []) or []
|
|
105
82
|
metrics[sessions_count_key] = len(sessions)
|
|
106
83
|
|
|
107
84
|
for session in sessions:
|
|
@@ -122,7 +99,7 @@ def calculate_date_metrics(date_to_process: date, sessions_data: dict) -> dict:
|
|
|
122
99
|
|
|
123
100
|
model_metrics = []
|
|
124
101
|
for model, count in model_counts.items():
|
|
125
|
-
model_id, model_provider = model.
|
|
102
|
+
model_id, model_provider = model.rsplit(":", 1)
|
|
126
103
|
model_metrics.append({"model_id": model_id, "model_provider": model_provider, "count": count})
|
|
127
104
|
|
|
128
105
|
metrics["users_count"] = len(all_user_ids)
|
|
@@ -194,3 +171,60 @@ def get_dates_to_calculate_metrics_for(starting_date: date) -> list[date]:
|
|
|
194
171
|
if days_diff <= 0:
|
|
195
172
|
return []
|
|
196
173
|
return [starting_date + timedelta(days=x) for x in range(days_diff)]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# -- Cultural Knowledge util methods --
|
|
177
|
+
def serialize_cultural_knowledge_for_db(cultural_knowledge: CulturalKnowledge) -> Dict[str, Any]:
|
|
178
|
+
"""Serialize a CulturalKnowledge object for database storage.
|
|
179
|
+
|
|
180
|
+
Converts the model's separate content, categories, and notes fields
|
|
181
|
+
into a single dict for the database content column.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
cultural_knowledge (CulturalKnowledge): The cultural knowledge object to serialize.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dict[str, Any]: A dictionary with the content field as a dict containing content, categories, and notes.
|
|
188
|
+
"""
|
|
189
|
+
content_dict: Dict[str, Any] = {}
|
|
190
|
+
if cultural_knowledge.content is not None:
|
|
191
|
+
content_dict["content"] = cultural_knowledge.content
|
|
192
|
+
if cultural_knowledge.categories is not None:
|
|
193
|
+
content_dict["categories"] = cultural_knowledge.categories
|
|
194
|
+
if cultural_knowledge.notes is not None:
|
|
195
|
+
content_dict["notes"] = cultural_knowledge.notes
|
|
196
|
+
|
|
197
|
+
return content_dict if content_dict else {}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def deserialize_cultural_knowledge_from_db(db_row: Dict[str, Any]) -> CulturalKnowledge:
|
|
201
|
+
"""Deserialize a database row to a CulturalKnowledge object.
|
|
202
|
+
|
|
203
|
+
The database stores content as a dict containing content, categories, and notes.
|
|
204
|
+
This method extracts those fields and converts them back to the model format.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
db_row (Dict[str, Any]): The database row as a dictionary.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
CulturalKnowledge: The cultural knowledge object.
|
|
211
|
+
"""
|
|
212
|
+
# Extract content, categories, and notes from the content field
|
|
213
|
+
content_json = db_row.get("content", {}) or {}
|
|
214
|
+
|
|
215
|
+
return CulturalKnowledge.from_dict(
|
|
216
|
+
{
|
|
217
|
+
"id": db_row.get("id"),
|
|
218
|
+
"name": db_row.get("name"),
|
|
219
|
+
"summary": db_row.get("summary"),
|
|
220
|
+
"content": content_json.get("content"),
|
|
221
|
+
"categories": content_json.get("categories"),
|
|
222
|
+
"notes": content_json.get("notes"),
|
|
223
|
+
"metadata": db_row.get("metadata"),
|
|
224
|
+
"input": db_row.get("input"),
|
|
225
|
+
"created_at": db_row.get("created_at"),
|
|
226
|
+
"updated_at": db_row.get("updated_at"),
|
|
227
|
+
"agent_id": db_row.get("agent_id"),
|
|
228
|
+
"team_id": db_row.get("team_id"),
|
|
229
|
+
}
|
|
230
|
+
)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from packaging import version as packaging_version
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
|
|
7
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
8
|
+
from agno.utils.log import log_error, log_info, log_warning
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MigrationManager:
|
|
12
|
+
"""Manager class to handle database migrations"""
|
|
13
|
+
|
|
14
|
+
available_versions: list[tuple[str, Version]] = [
|
|
15
|
+
("v2_0_0", packaging_version.parse("2.0.0")),
|
|
16
|
+
("v2_3_0", packaging_version.parse("2.3.0")),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
def __init__(self, db: Union[AsyncBaseDb, BaseDb]):
|
|
20
|
+
self.db = db
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def latest_schema_version(self) -> Version:
|
|
24
|
+
return self.available_versions[-1][1]
|
|
25
|
+
|
|
26
|
+
async def up(self, target_version: Optional[str] = None, table_type: Optional[str] = None, force: bool = False):
|
|
27
|
+
"""Handle executing an up migration.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
target_version: The version to migrate to, e.g. "v3.0.0". If not provided, the latest available version will be used.
|
|
31
|
+
table_type: The type of table to migrate. If not provided, all table types will be considered.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# If not target version is provided, use the latest available version
|
|
35
|
+
if not target_version:
|
|
36
|
+
_target_version = self.latest_schema_version
|
|
37
|
+
log_info(
|
|
38
|
+
f"No target version provided. Will migrate to the latest available version: {str(_target_version)}"
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
_target_version = packaging_version.parse(target_version)
|
|
42
|
+
|
|
43
|
+
# Select tables to migrate
|
|
44
|
+
if table_type:
|
|
45
|
+
if table_type not in ["memory", "session", "metrics", "eval", "knowledge", "culture"]:
|
|
46
|
+
log_warning(
|
|
47
|
+
f"Invalid table type: {table_type}. Use one of: memory, session, metrics, eval, knowledge, culture"
|
|
48
|
+
)
|
|
49
|
+
return
|
|
50
|
+
tables = [(table_type, getattr(self.db, f"{table_type}_table_name"))]
|
|
51
|
+
else:
|
|
52
|
+
tables = [
|
|
53
|
+
("memories", self.db.memory_table_name),
|
|
54
|
+
("sessions", self.db.session_table_name),
|
|
55
|
+
("metrics", self.db.metrics_table_name),
|
|
56
|
+
("evals", self.db.eval_table_name),
|
|
57
|
+
("knowledge", self.db.knowledge_table_name),
|
|
58
|
+
("culture", self.db.culture_table_name),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
# Handle migrations for each table separately (extend in future if needed):
|
|
62
|
+
for table_type, table_name in tables:
|
|
63
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
64
|
+
current_version = packaging_version.parse(await self.db.get_latest_schema_version(table_name))
|
|
65
|
+
else:
|
|
66
|
+
current_version = packaging_version.parse(self.db.get_latest_schema_version(table_name))
|
|
67
|
+
|
|
68
|
+
if current_version is None:
|
|
69
|
+
log_warning(f"Skipping up migration: No version found for table {table_name}.")
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
# If the target version is less or equal to the current version, no migrations needed
|
|
73
|
+
if _target_version <= current_version and not force:
|
|
74
|
+
log_warning(
|
|
75
|
+
f"Skipping up migration: the version of table '{table_name}' ({current_version}) is less or equal to the target version ({_target_version})."
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
log_info(
|
|
80
|
+
f"Starting database migration for table {table_name}. Current version: {current_version}. Target version: {_target_version}."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Find files after the current version
|
|
84
|
+
latest_version = None
|
|
85
|
+
migration_executed = False
|
|
86
|
+
for version, normalised_version in self.available_versions:
|
|
87
|
+
if normalised_version > current_version:
|
|
88
|
+
if target_version and normalised_version > _target_version:
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
log_info(f"Applying migration {normalised_version} on {table_name}")
|
|
92
|
+
migration_executed = await self._up_migration(version, table_type, table_name)
|
|
93
|
+
if migration_executed:
|
|
94
|
+
log_info(f"Successfully applied migration {normalised_version} on table {table_name}")
|
|
95
|
+
else:
|
|
96
|
+
log_info(f"Skipping application of migration {normalised_version} on table {table_name}")
|
|
97
|
+
|
|
98
|
+
latest_version = normalised_version.public
|
|
99
|
+
|
|
100
|
+
if migration_executed and latest_version:
|
|
101
|
+
log_info(f"Storing version {latest_version} in database for table {table_name}")
|
|
102
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
103
|
+
await self.db.upsert_schema_version(table_name, latest_version)
|
|
104
|
+
else:
|
|
105
|
+
self.db.upsert_schema_version(table_name, latest_version)
|
|
106
|
+
log_info(f"Successfully stored version {latest_version} in database for table {table_name}")
|
|
107
|
+
log_info("----------------------------------------------------------")
|
|
108
|
+
|
|
109
|
+
async def _up_migration(self, version: str, table_type: str, table_name: str) -> bool:
|
|
110
|
+
"""Run the database-specific logic to handle an up migration.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
version: The version to migrate to, e.g. "v3.0.0"
|
|
114
|
+
"""
|
|
115
|
+
migration_module = importlib.import_module(f"agno.db.migrations.versions.{version}")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
119
|
+
return await migration_module.async_up(self.db, table_type, table_name)
|
|
120
|
+
else:
|
|
121
|
+
return migration_module.up(self.db, table_type, table_name)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
log_error(f"Error running migration to version {version}: {e}")
|
|
124
|
+
raise
|
|
125
|
+
|
|
126
|
+
async def down(self, target_version: str, table_type: Optional[str] = None, force: bool = False):
|
|
127
|
+
"""Handle executing a down migration.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
target_version: The version to migrate to. e.g. "v2.3.0"
|
|
131
|
+
table_type: The type of table to migrate. If not provided, all table types will be considered.
|
|
132
|
+
"""
|
|
133
|
+
_target_version = packaging_version.parse(target_version)
|
|
134
|
+
|
|
135
|
+
# Select tables to migrate
|
|
136
|
+
if table_type:
|
|
137
|
+
if table_type not in ["memory", "session", "metrics", "eval", "knowledge", "culture"]:
|
|
138
|
+
log_warning(
|
|
139
|
+
f"Invalid table type: {table_type}. Use one of: memory, session, metrics, eval, knowledge, culture"
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
tables = [(table_type, getattr(self.db, f"{table_type}_table_name"))]
|
|
143
|
+
else:
|
|
144
|
+
tables = [
|
|
145
|
+
("memories", self.db.memory_table_name),
|
|
146
|
+
("sessions", self.db.session_table_name),
|
|
147
|
+
("metrics", self.db.metrics_table_name),
|
|
148
|
+
("evals", self.db.eval_table_name),
|
|
149
|
+
("knowledge", self.db.knowledge_table_name),
|
|
150
|
+
("culture", self.db.culture_table_name),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for table_type, table_name in tables:
|
|
154
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
155
|
+
current_version = packaging_version.parse(await self.db.get_latest_schema_version(table_name))
|
|
156
|
+
else:
|
|
157
|
+
current_version = packaging_version.parse(self.db.get_latest_schema_version(table_name))
|
|
158
|
+
|
|
159
|
+
if _target_version >= current_version and not force:
|
|
160
|
+
log_warning(
|
|
161
|
+
f"Skipping down migration: the version of table '{table_name}' ({current_version}) is less or equal to the target version ({_target_version})."
|
|
162
|
+
)
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
migration_executed = False
|
|
166
|
+
# Run down migration for all versions between target and current (include down of current version)
|
|
167
|
+
# Apply down migrations in reverse order to ensure dependencies are met
|
|
168
|
+
for version, normalised_version in reversed(self.available_versions):
|
|
169
|
+
if normalised_version > _target_version:
|
|
170
|
+
log_info(f"Reverting migration {normalised_version} on table {table_name}")
|
|
171
|
+
migration_executed = await self._down_migration(version, table_type, table_name)
|
|
172
|
+
if migration_executed:
|
|
173
|
+
log_info(f"Successfully reverted migration {normalised_version} on table {table_name}")
|
|
174
|
+
else:
|
|
175
|
+
log_info(f"Skipping revert of migration {normalised_version} on table {table_name}")
|
|
176
|
+
|
|
177
|
+
if migration_executed:
|
|
178
|
+
log_info(f"Storing version {_target_version} in database for table {table_name}")
|
|
179
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
180
|
+
await self.db.upsert_schema_version(table_name, _target_version.public)
|
|
181
|
+
else:
|
|
182
|
+
self.db.upsert_schema_version(table_name, _target_version.public)
|
|
183
|
+
log_info(f"Successfully stored version {_target_version} in database for table {table_name}")
|
|
184
|
+
|
|
185
|
+
async def _down_migration(self, version: str, table_type: str, table_name: str) -> bool:
|
|
186
|
+
"""Run the database-specific logic to handle a down migration.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
version: The version to migrate to, e.g. "v3.0.0"
|
|
190
|
+
"""
|
|
191
|
+
migration_module = importlib.import_module(f"agno.db.migrations.versions.{version}")
|
|
192
|
+
try:
|
|
193
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
194
|
+
return await migration_module.async_down(self.db, table_type, table_name)
|
|
195
|
+
else:
|
|
196
|
+
return migration_module.down(self.db, table_type, table_name)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
log_error(f"Error running migration to version {version}: {e}")
|
|
199
|
+
raise
|
agno/db/migrations/v1_to_v2.py
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
"""Migration utility to migrate your Agno tables from v1 to v2"""
|
|
2
2
|
|
|
3
|
+
import gc
|
|
3
4
|
import json
|
|
4
|
-
from typing import Any, Dict, List, Optional, Union
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union, cast
|
|
5
6
|
|
|
6
7
|
from sqlalchemy import text
|
|
7
8
|
|
|
8
|
-
from agno.db.
|
|
9
|
-
from agno.db.mysql.mysql import MySQLDb
|
|
10
|
-
from agno.db.postgres.postgres import PostgresDb
|
|
9
|
+
from agno.db.base import BaseDb
|
|
11
10
|
from agno.db.schemas.memory import UserMemory
|
|
12
|
-
from agno.db.sqlite.sqlite import SqliteDb
|
|
13
11
|
from agno.session import AgentSession, TeamSession, WorkflowSession
|
|
14
12
|
from agno.utils.log import log_error, log_info, log_warning
|
|
15
13
|
|
|
@@ -315,7 +313,7 @@ def convert_v1_fields_to_v2(data: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
315
313
|
|
|
316
314
|
|
|
317
315
|
def migrate(
|
|
318
|
-
db:
|
|
316
|
+
db: BaseDb,
|
|
319
317
|
v1_db_schema: str,
|
|
320
318
|
agent_sessions_table_name: Optional[str] = None,
|
|
321
319
|
team_sessions_table_name: Optional[str] = None,
|
|
@@ -372,7 +370,7 @@ def migrate(
|
|
|
372
370
|
|
|
373
371
|
|
|
374
372
|
def migrate_table_in_batches(
|
|
375
|
-
db:
|
|
373
|
+
db: BaseDb,
|
|
376
374
|
v1_db_schema: str,
|
|
377
375
|
v1_table_name: str,
|
|
378
376
|
v1_table_type: str,
|
|
@@ -410,7 +408,7 @@ def migrate_table_in_batches(
|
|
|
410
408
|
if hasattr(db, "Session"):
|
|
411
409
|
db.Session.remove() # type: ignore
|
|
412
410
|
|
|
413
|
-
db.upsert_sessions(sessions) # type: ignore
|
|
411
|
+
db.upsert_sessions(sessions, preserve_updated_at=True) # type: ignore
|
|
414
412
|
total_migrated += len(sessions)
|
|
415
413
|
log_info(f"Bulk upserted {len(sessions)} sessions in batch {batch_count}")
|
|
416
414
|
|
|
@@ -420,21 +418,35 @@ def migrate_table_in_batches(
|
|
|
420
418
|
if hasattr(db, "Session"):
|
|
421
419
|
db.Session.remove() # type: ignore
|
|
422
420
|
|
|
423
|
-
db.upsert_memories(memories)
|
|
421
|
+
db.upsert_memories(memories, preserve_updated_at=True)
|
|
424
422
|
total_migrated += len(memories)
|
|
425
423
|
log_info(f"Bulk upserted {len(memories)} memories in batch {batch_count}")
|
|
426
424
|
|
|
427
425
|
log_info(f"Completed batch {batch_count}: migrated {batch_size_actual} records")
|
|
428
426
|
|
|
427
|
+
# Explicit cleanup to free memory before next batch
|
|
428
|
+
del batch_content
|
|
429
|
+
if v1_table_type in ["agent_sessions", "team_sessions", "workflow_sessions"]:
|
|
430
|
+
del sessions
|
|
431
|
+
elif v1_table_type == "memories":
|
|
432
|
+
del memories
|
|
433
|
+
|
|
434
|
+
# Force garbage collection to return memory to OS
|
|
435
|
+
# This is necessary because Python's memory allocator retains memory after large operations
|
|
436
|
+
# See: https://github.com/sqlalchemy/sqlalchemy/issues/4616
|
|
437
|
+
gc.collect()
|
|
438
|
+
|
|
429
439
|
log_info(f"✅ Migration completed for table {v1_table_name}: {total_migrated} total records migrated")
|
|
430
440
|
|
|
431
441
|
|
|
432
|
-
def get_table_content_in_batches(
|
|
433
|
-
db: Union[PostgresDb, MySQLDb, SqliteDb, MongoDb], db_schema: str, table_name: str, batch_size: int = 5000
|
|
434
|
-
):
|
|
442
|
+
def get_table_content_in_batches(db: BaseDb, db_schema: str, table_name: str, batch_size: int = 5000):
|
|
435
443
|
"""Get table content in batches to avoid memory issues with large tables"""
|
|
436
444
|
try:
|
|
437
|
-
if
|
|
445
|
+
if type(db).__name__ == "MongoDb":
|
|
446
|
+
from agno.db.mongo.mongo import MongoDb
|
|
447
|
+
|
|
448
|
+
db = cast(MongoDb, db)
|
|
449
|
+
|
|
438
450
|
# MongoDB implementation with cursor and batching
|
|
439
451
|
collection = db.database[table_name]
|
|
440
452
|
cursor = collection.find({}).batch_size(batch_size)
|
|
@@ -455,6 +467,24 @@ def get_table_content_in_batches(
|
|
|
455
467
|
yield batch
|
|
456
468
|
else:
|
|
457
469
|
# SQL database implementations (PostgresDb, MySQLDb, SqliteDb)
|
|
470
|
+
if type(db).__name__ == "PostgresDb":
|
|
471
|
+
from agno.db.postgres.postgres import PostgresDb
|
|
472
|
+
|
|
473
|
+
db = cast(PostgresDb, db)
|
|
474
|
+
|
|
475
|
+
elif type(db).__name__ == "MySQLDb":
|
|
476
|
+
from agno.db.mysql.mysql import MySQLDb
|
|
477
|
+
|
|
478
|
+
db = cast(MySQLDb, db)
|
|
479
|
+
|
|
480
|
+
elif type(db).__name__ == "SqliteDb":
|
|
481
|
+
from agno.db.sqlite.sqlite import SqliteDb
|
|
482
|
+
|
|
483
|
+
db = cast(SqliteDb, db)
|
|
484
|
+
|
|
485
|
+
else:
|
|
486
|
+
raise ValueError(f"Invalid database type: {type(db).__name__}")
|
|
487
|
+
|
|
458
488
|
offset = 0
|
|
459
489
|
while True:
|
|
460
490
|
# Create a new session for each batch to avoid transaction conflicts
|
|
File without changes
|