agno 2.2.13__py3-none-any.whl → 2.4.3__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/__init__.py +6 -0
- agno/agent/agent.py +5252 -3145
- agno/agent/remote.py +525 -0
- agno/api/api.py +2 -0
- agno/client/__init__.py +3 -0
- agno/client/a2a/__init__.py +10 -0
- agno/client/a2a/client.py +554 -0
- agno/client/a2a/schemas.py +112 -0
- agno/client/a2a/utils.py +369 -0
- agno/client/os.py +2669 -0
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +247 -0
- agno/culture/manager.py +2 -2
- agno/db/base.py +927 -6
- agno/db/dynamo/dynamo.py +788 -2
- agno/db/dynamo/schemas.py +128 -0
- agno/db/dynamo/utils.py +26 -3
- agno/db/firestore/firestore.py +674 -50
- agno/db/firestore/schemas.py +41 -0
- agno/db/firestore/utils.py +25 -10
- agno/db/gcs_json/gcs_json_db.py +506 -3
- agno/db/gcs_json/utils.py +14 -2
- agno/db/in_memory/in_memory_db.py +203 -4
- agno/db/in_memory/utils.py +14 -2
- agno/db/json/json_db.py +498 -2
- agno/db/json/utils.py +14 -2
- agno/db/migrations/manager.py +199 -0
- agno/db/migrations/utils.py +19 -0
- agno/db/migrations/v1_to_v2.py +54 -16
- agno/db/migrations/versions/__init__.py +0 -0
- agno/db/migrations/versions/v2_3_0.py +977 -0
- agno/db/mongo/async_mongo.py +1013 -39
- agno/db/mongo/mongo.py +684 -4
- agno/db/mongo/schemas.py +48 -0
- agno/db/mongo/utils.py +17 -0
- agno/db/mysql/__init__.py +2 -1
- agno/db/mysql/async_mysql.py +2958 -0
- agno/db/mysql/mysql.py +722 -53
- agno/db/mysql/schemas.py +77 -11
- agno/db/mysql/utils.py +151 -8
- agno/db/postgres/async_postgres.py +1254 -137
- agno/db/postgres/postgres.py +2316 -93
- agno/db/postgres/schemas.py +153 -21
- agno/db/postgres/utils.py +22 -7
- agno/db/redis/redis.py +531 -3
- agno/db/redis/schemas.py +36 -0
- agno/db/redis/utils.py +31 -15
- agno/db/schemas/evals.py +1 -0
- agno/db/schemas/memory.py +20 -9
- agno/db/singlestore/schemas.py +70 -1
- agno/db/singlestore/singlestore.py +737 -74
- agno/db/singlestore/utils.py +13 -3
- agno/db/sqlite/async_sqlite.py +1069 -89
- agno/db/sqlite/schemas.py +133 -1
- agno/db/sqlite/sqlite.py +2203 -165
- agno/db/sqlite/utils.py +21 -11
- agno/db/surrealdb/models.py +25 -0
- agno/db/surrealdb/surrealdb.py +603 -1
- agno/db/utils.py +60 -0
- agno/eval/__init__.py +26 -3
- agno/eval/accuracy.py +25 -12
- agno/eval/agent_as_judge.py +871 -0
- agno/eval/base.py +29 -0
- agno/eval/performance.py +10 -4
- agno/eval/reliability.py +22 -13
- agno/eval/utils.py +2 -1
- agno/exceptions.py +42 -0
- agno/hooks/__init__.py +3 -0
- agno/hooks/decorator.py +164 -0
- agno/integrations/discord/client.py +13 -2
- agno/knowledge/__init__.py +4 -0
- agno/knowledge/chunking/code.py +90 -0
- agno/knowledge/chunking/document.py +65 -4
- agno/knowledge/chunking/fixed.py +4 -1
- agno/knowledge/chunking/markdown.py +102 -11
- agno/knowledge/chunking/recursive.py +2 -2
- agno/knowledge/chunking/semantic.py +130 -48
- agno/knowledge/chunking/strategy.py +18 -0
- agno/knowledge/embedder/azure_openai.py +0 -1
- agno/knowledge/embedder/google.py +1 -1
- agno/knowledge/embedder/mistral.py +1 -1
- agno/knowledge/embedder/nebius.py +1 -1
- agno/knowledge/embedder/openai.py +16 -12
- agno/knowledge/filesystem.py +412 -0
- agno/knowledge/knowledge.py +4261 -1199
- agno/knowledge/protocol.py +134 -0
- agno/knowledge/reader/arxiv_reader.py +3 -2
- agno/knowledge/reader/base.py +9 -7
- agno/knowledge/reader/csv_reader.py +91 -42
- agno/knowledge/reader/docx_reader.py +9 -10
- agno/knowledge/reader/excel_reader.py +225 -0
- agno/knowledge/reader/field_labeled_csv_reader.py +38 -48
- agno/knowledge/reader/firecrawl_reader.py +3 -2
- agno/knowledge/reader/json_reader.py +16 -22
- agno/knowledge/reader/markdown_reader.py +15 -14
- agno/knowledge/reader/pdf_reader.py +33 -28
- agno/knowledge/reader/pptx_reader.py +9 -10
- agno/knowledge/reader/reader_factory.py +135 -1
- agno/knowledge/reader/s3_reader.py +8 -16
- agno/knowledge/reader/tavily_reader.py +3 -3
- agno/knowledge/reader/text_reader.py +15 -14
- agno/knowledge/reader/utils/__init__.py +17 -0
- agno/knowledge/reader/utils/spreadsheet.py +114 -0
- agno/knowledge/reader/web_search_reader.py +8 -65
- agno/knowledge/reader/website_reader.py +16 -13
- agno/knowledge/reader/wikipedia_reader.py +36 -3
- agno/knowledge/reader/youtube_reader.py +3 -2
- agno/knowledge/remote_content/__init__.py +33 -0
- agno/knowledge/remote_content/config.py +266 -0
- agno/knowledge/remote_content/remote_content.py +105 -17
- agno/knowledge/utils.py +76 -22
- agno/learn/__init__.py +71 -0
- agno/learn/config.py +463 -0
- agno/learn/curate.py +185 -0
- agno/learn/machine.py +725 -0
- agno/learn/schemas.py +1114 -0
- agno/learn/stores/__init__.py +38 -0
- agno/learn/stores/decision_log.py +1156 -0
- agno/learn/stores/entity_memory.py +3275 -0
- agno/learn/stores/learned_knowledge.py +1583 -0
- agno/learn/stores/protocol.py +117 -0
- agno/learn/stores/session_context.py +1217 -0
- agno/learn/stores/user_memory.py +1495 -0
- agno/learn/stores/user_profile.py +1220 -0
- agno/learn/utils.py +209 -0
- agno/media.py +22 -6
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +223 -8
- 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 +434 -59
- agno/models/aws/bedrock.py +121 -20
- agno/models/aws/claude.py +131 -274
- agno/models/azure/ai_foundry.py +10 -6
- agno/models/azure/openai_chat.py +33 -10
- agno/models/base.py +1162 -561
- agno/models/cerebras/cerebras.py +120 -24
- agno/models/cerebras/cerebras_openai.py +21 -2
- agno/models/cohere/chat.py +65 -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 +959 -89
- agno/models/google/utils.py +22 -0
- agno/models/groq/groq.py +48 -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 +88 -9
- agno/models/litellm/litellm_openai.py +18 -1
- agno/models/message.py +24 -5
- agno/models/meta/llama.py +40 -13
- agno/models/meta/llama_openai.py +22 -21
- agno/models/metrics.py +12 -0
- agno/models/mistral/mistral.py +8 -4
- agno/models/n1n/__init__.py +3 -0
- agno/models/n1n/n1n.py +57 -0
- agno/models/nebius/nebius.py +6 -7
- agno/models/nvidia/nvidia.py +20 -3
- agno/models/ollama/__init__.py +2 -0
- agno/models/ollama/chat.py +17 -6
- agno/models/ollama/responses.py +100 -0
- agno/models/openai/__init__.py +2 -0
- agno/models/openai/chat.py +117 -26
- agno/models/openai/open_responses.py +46 -0
- agno/models/openai/responses.py +110 -32
- agno/models/openrouter/__init__.py +2 -0
- agno/models/openrouter/openrouter.py +67 -2
- agno/models/openrouter/responses.py +146 -0
- agno/models/perplexity/perplexity.py +19 -1
- agno/models/portkey/portkey.py +7 -6
- agno/models/requesty/requesty.py +19 -2
- agno/models/response.py +20 -2
- agno/models/sambanova/sambanova.py +20 -3
- agno/models/siliconflow/siliconflow.py +19 -2
- agno/models/together/together.py +20 -3
- agno/models/vercel/v0.py +20 -3
- agno/models/vertexai/claude.py +124 -4
- agno/models/vllm/vllm.py +19 -14
- agno/models/xai/xai.py +19 -2
- agno/os/app.py +467 -137
- agno/os/auth.py +253 -5
- agno/os/config.py +22 -0
- agno/os/interfaces/a2a/a2a.py +7 -6
- agno/os/interfaces/a2a/router.py +635 -26
- agno/os/interfaces/a2a/utils.py +32 -33
- agno/os/interfaces/agui/agui.py +5 -3
- agno/os/interfaces/agui/router.py +26 -16
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/interfaces/base.py +7 -7
- agno/os/interfaces/slack/router.py +16 -7
- agno/os/interfaces/slack/slack.py +7 -7
- agno/os/interfaces/whatsapp/router.py +35 -7
- agno/os/interfaces/whatsapp/security.py +3 -1
- agno/os/interfaces/whatsapp/whatsapp.py +11 -8
- agno/os/managers.py +326 -0
- agno/os/mcp.py +652 -79
- agno/os/middleware/__init__.py +4 -0
- agno/os/middleware/jwt.py +718 -115
- agno/os/middleware/trailing_slash.py +27 -0
- agno/os/router.py +105 -1558
- agno/os/routers/agents/__init__.py +3 -0
- agno/os/routers/agents/router.py +655 -0
- agno/os/routers/agents/schema.py +288 -0
- agno/os/routers/components/__init__.py +3 -0
- agno/os/routers/components/components.py +475 -0
- agno/os/routers/database.py +155 -0
- agno/os/routers/evals/evals.py +111 -18
- agno/os/routers/evals/schemas.py +38 -5
- agno/os/routers/evals/utils.py +80 -11
- agno/os/routers/health.py +3 -3
- agno/os/routers/knowledge/knowledge.py +284 -35
- agno/os/routers/knowledge/schemas.py +14 -2
- agno/os/routers/memory/memory.py +274 -11
- agno/os/routers/memory/schemas.py +44 -3
- agno/os/routers/metrics/metrics.py +30 -15
- agno/os/routers/metrics/schemas.py +10 -6
- agno/os/routers/registry/__init__.py +3 -0
- agno/os/routers/registry/registry.py +337 -0
- agno/os/routers/session/session.py +143 -14
- agno/os/routers/teams/__init__.py +3 -0
- agno/os/routers/teams/router.py +550 -0
- agno/os/routers/teams/schema.py +280 -0
- agno/os/routers/traces/__init__.py +3 -0
- agno/os/routers/traces/schemas.py +414 -0
- agno/os/routers/traces/traces.py +549 -0
- agno/os/routers/workflows/__init__.py +3 -0
- agno/os/routers/workflows/router.py +757 -0
- agno/os/routers/workflows/schema.py +139 -0
- agno/os/schema.py +157 -584
- agno/os/scopes.py +469 -0
- agno/os/settings.py +3 -0
- agno/os/utils.py +574 -185
- agno/reasoning/anthropic.py +85 -1
- agno/reasoning/azure_ai_foundry.py +93 -1
- agno/reasoning/deepseek.py +102 -2
- agno/reasoning/default.py +6 -7
- agno/reasoning/gemini.py +87 -3
- agno/reasoning/groq.py +109 -2
- agno/reasoning/helpers.py +6 -7
- agno/reasoning/manager.py +1238 -0
- agno/reasoning/ollama.py +93 -1
- agno/reasoning/openai.py +115 -1
- agno/reasoning/vertexai.py +85 -1
- agno/registry/__init__.py +3 -0
- agno/registry/registry.py +68 -0
- agno/remote/__init__.py +3 -0
- agno/remote/base.py +581 -0
- agno/run/__init__.py +2 -4
- agno/run/agent.py +134 -19
- agno/run/base.py +49 -1
- agno/run/cancel.py +65 -52
- agno/run/cancellation_management/__init__.py +9 -0
- agno/run/cancellation_management/base.py +78 -0
- agno/run/cancellation_management/in_memory_cancellation_manager.py +100 -0
- agno/run/cancellation_management/redis_cancellation_manager.py +236 -0
- agno/run/requirement.py +181 -0
- agno/run/team.py +111 -19
- agno/run/workflow.py +2 -1
- agno/session/agent.py +57 -92
- agno/session/summary.py +1 -1
- agno/session/team.py +62 -115
- agno/session/workflow.py +353 -57
- agno/skills/__init__.py +17 -0
- agno/skills/agent_skills.py +377 -0
- agno/skills/errors.py +32 -0
- agno/skills/loaders/__init__.py +4 -0
- agno/skills/loaders/base.py +27 -0
- agno/skills/loaders/local.py +216 -0
- agno/skills/skill.py +65 -0
- agno/skills/utils.py +107 -0
- agno/skills/validator.py +277 -0
- agno/table.py +10 -0
- agno/team/__init__.py +5 -1
- agno/team/remote.py +447 -0
- agno/team/team.py +3769 -2202
- agno/tools/brandfetch.py +27 -18
- agno/tools/browserbase.py +225 -16
- agno/tools/crawl4ai.py +3 -0
- agno/tools/duckduckgo.py +25 -71
- agno/tools/exa.py +0 -21
- agno/tools/file.py +14 -13
- agno/tools/file_generation.py +12 -6
- agno/tools/firecrawl.py +15 -7
- agno/tools/function.py +94 -113
- agno/tools/google_bigquery.py +11 -2
- agno/tools/google_drive.py +4 -3
- agno/tools/knowledge.py +9 -4
- agno/tools/mcp/mcp.py +301 -18
- agno/tools/mcp/multi_mcp.py +269 -14
- agno/tools/mem0.py +11 -10
- agno/tools/memory.py +47 -46
- agno/tools/mlx_transcribe.py +10 -7
- agno/tools/models/nebius.py +5 -5
- agno/tools/models_labs.py +20 -10
- agno/tools/nano_banana.py +151 -0
- agno/tools/parallel.py +0 -7
- agno/tools/postgres.py +76 -36
- agno/tools/python.py +14 -6
- agno/tools/reasoning.py +30 -23
- agno/tools/redshift.py +406 -0
- agno/tools/shopify.py +1519 -0
- agno/tools/spotify.py +919 -0
- agno/tools/tavily.py +4 -1
- agno/tools/toolkit.py +253 -18
- agno/tools/websearch.py +93 -0
- agno/tools/website.py +1 -1
- agno/tools/wikipedia.py +1 -1
- agno/tools/workflow.py +56 -48
- agno/tools/yfinance.py +12 -11
- agno/tracing/__init__.py +12 -0
- agno/tracing/exporter.py +161 -0
- agno/tracing/schemas.py +276 -0
- agno/tracing/setup.py +112 -0
- agno/utils/agent.py +251 -10
- agno/utils/cryptography.py +22 -0
- agno/utils/dttm.py +33 -0
- agno/utils/events.py +264 -7
- agno/utils/hooks.py +111 -3
- agno/utils/http.py +161 -2
- agno/utils/mcp.py +49 -8
- agno/utils/media.py +22 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +20 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/os.py +0 -0
- agno/utils/print_response/agent.py +99 -16
- agno/utils/print_response/team.py +223 -24
- agno/utils/print_response/workflow.py +0 -2
- agno/utils/prompts.py +8 -6
- agno/utils/remote.py +23 -0
- agno/utils/response.py +1 -13
- agno/utils/string.py +91 -2
- agno/utils/team.py +62 -12
- agno/utils/tokens.py +657 -0
- agno/vectordb/base.py +15 -2
- agno/vectordb/cassandra/cassandra.py +1 -1
- agno/vectordb/chroma/__init__.py +2 -1
- agno/vectordb/chroma/chromadb.py +468 -23
- agno/vectordb/clickhouse/clickhousedb.py +1 -1
- agno/vectordb/couchbase/couchbase.py +6 -2
- agno/vectordb/lancedb/lance_db.py +7 -38
- agno/vectordb/lightrag/lightrag.py +7 -6
- agno/vectordb/milvus/milvus.py +118 -84
- agno/vectordb/mongodb/__init__.py +2 -1
- agno/vectordb/mongodb/mongodb.py +14 -31
- agno/vectordb/pgvector/pgvector.py +120 -66
- agno/vectordb/pineconedb/pineconedb.py +2 -19
- agno/vectordb/qdrant/__init__.py +2 -1
- agno/vectordb/qdrant/qdrant.py +33 -56
- agno/vectordb/redis/__init__.py +2 -1
- agno/vectordb/redis/redisdb.py +19 -31
- agno/vectordb/singlestore/singlestore.py +17 -9
- agno/vectordb/surrealdb/surrealdb.py +2 -38
- agno/vectordb/weaviate/__init__.py +2 -1
- agno/vectordb/weaviate/weaviate.py +7 -3
- agno/workflow/__init__.py +5 -1
- agno/workflow/agent.py +2 -2
- agno/workflow/condition.py +12 -10
- agno/workflow/loop.py +28 -9
- agno/workflow/parallel.py +21 -13
- agno/workflow/remote.py +362 -0
- agno/workflow/router.py +12 -9
- agno/workflow/step.py +261 -36
- agno/workflow/steps.py +12 -8
- agno/workflow/types.py +40 -77
- agno/workflow/workflow.py +939 -213
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/METADATA +134 -181
- agno-2.4.3.dist-info/RECORD +677 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/WHEEL +1 -1
- agno/tools/googlesearch.py +0 -98
- agno/tools/memori.py +0 -339
- agno-2.2.13.dist-info/RECORD +0 -575
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/licenses/LICENSE +0 -0
- {agno-2.2.13.dist-info → agno-2.4.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Run cancellation management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import threading
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
from agno.exceptions import RunCancelledException
|
|
8
|
+
from agno.run.cancellation_management.base import BaseRunCancellationManager
|
|
9
|
+
from agno.utils.log import logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InMemoryRunCancellationManager(BaseRunCancellationManager):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self._cancelled_runs: Dict[str, bool] = {}
|
|
15
|
+
self._lock = threading.Lock()
|
|
16
|
+
self._async_lock = asyncio.Lock()
|
|
17
|
+
|
|
18
|
+
def register_run(self, run_id: str) -> None:
|
|
19
|
+
"""Register a new run as not cancelled."""
|
|
20
|
+
with self._lock:
|
|
21
|
+
self._cancelled_runs[run_id] = False
|
|
22
|
+
|
|
23
|
+
async def aregister_run(self, run_id: str) -> None:
|
|
24
|
+
"""Register a new run as not cancelled (async version)."""
|
|
25
|
+
async with self._async_lock:
|
|
26
|
+
self._cancelled_runs[run_id] = False
|
|
27
|
+
|
|
28
|
+
def cancel_run(self, run_id: str) -> bool:
|
|
29
|
+
"""Cancel a run by marking it as cancelled.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bool: True if run was found and cancelled, False if run not found.
|
|
33
|
+
"""
|
|
34
|
+
with self._lock:
|
|
35
|
+
if run_id in self._cancelled_runs:
|
|
36
|
+
self._cancelled_runs[run_id] = True
|
|
37
|
+
logger.info(f"Run {run_id} marked for cancellation")
|
|
38
|
+
return True
|
|
39
|
+
else:
|
|
40
|
+
logger.warning(f"Attempted to cancel unknown run {run_id}")
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
async def acancel_run(self, run_id: str) -> bool:
|
|
44
|
+
"""Cancel a run by marking it as cancelled (async version).
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
bool: True if run was found and cancelled, False if run not found.
|
|
48
|
+
"""
|
|
49
|
+
async with self._async_lock:
|
|
50
|
+
if run_id in self._cancelled_runs:
|
|
51
|
+
self._cancelled_runs[run_id] = True
|
|
52
|
+
logger.info(f"Run {run_id} marked for cancellation")
|
|
53
|
+
return True
|
|
54
|
+
else:
|
|
55
|
+
logger.warning(f"Attempted to cancel unknown run {run_id}")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def is_cancelled(self, run_id: str) -> bool:
|
|
59
|
+
"""Check if a run is cancelled."""
|
|
60
|
+
with self._lock:
|
|
61
|
+
return self._cancelled_runs.get(run_id, False)
|
|
62
|
+
|
|
63
|
+
async def ais_cancelled(self, run_id: str) -> bool:
|
|
64
|
+
"""Check if a run is cancelled (async version)."""
|
|
65
|
+
async with self._async_lock:
|
|
66
|
+
return self._cancelled_runs.get(run_id, False)
|
|
67
|
+
|
|
68
|
+
def cleanup_run(self, run_id: str) -> None:
|
|
69
|
+
"""Remove a run from tracking (called when run completes)."""
|
|
70
|
+
with self._lock:
|
|
71
|
+
if run_id in self._cancelled_runs:
|
|
72
|
+
del self._cancelled_runs[run_id]
|
|
73
|
+
|
|
74
|
+
async def acleanup_run(self, run_id: str) -> None:
|
|
75
|
+
"""Remove a run from tracking (called when run completes) (async version)."""
|
|
76
|
+
async with self._async_lock:
|
|
77
|
+
if run_id in self._cancelled_runs:
|
|
78
|
+
del self._cancelled_runs[run_id]
|
|
79
|
+
|
|
80
|
+
def raise_if_cancelled(self, run_id: str) -> None:
|
|
81
|
+
"""Check if a run should be cancelled and raise exception if so."""
|
|
82
|
+
if self.is_cancelled(run_id):
|
|
83
|
+
logger.info(f"Cancelling run {run_id}")
|
|
84
|
+
raise RunCancelledException(f"Run {run_id} was cancelled")
|
|
85
|
+
|
|
86
|
+
async def araise_if_cancelled(self, run_id: str) -> None:
|
|
87
|
+
"""Check if a run should be cancelled and raise exception if so (async version)."""
|
|
88
|
+
if await self.ais_cancelled(run_id):
|
|
89
|
+
logger.info(f"Cancelling run {run_id}")
|
|
90
|
+
raise RunCancelledException(f"Run {run_id} was cancelled")
|
|
91
|
+
|
|
92
|
+
def get_active_runs(self) -> Dict[str, bool]:
|
|
93
|
+
"""Get all currently tracked runs and their cancellation status."""
|
|
94
|
+
with self._lock:
|
|
95
|
+
return self._cancelled_runs.copy()
|
|
96
|
+
|
|
97
|
+
async def aget_active_runs(self) -> Dict[str, bool]:
|
|
98
|
+
"""Get all currently tracked runs and their cancellation status (async version)."""
|
|
99
|
+
async with self._async_lock:
|
|
100
|
+
return self._cancelled_runs.copy()
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Redis-based run cancellation management."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
|
4
|
+
|
|
5
|
+
from agno.exceptions import RunCancelledException
|
|
6
|
+
from agno.run.cancellation_management.base import BaseRunCancellationManager
|
|
7
|
+
from agno.utils.log import logger
|
|
8
|
+
|
|
9
|
+
# Defer import error until class instantiation
|
|
10
|
+
_redis_available = True
|
|
11
|
+
_redis_import_error: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from redis import Redis, RedisCluster
|
|
15
|
+
from redis.asyncio import Redis as AsyncRedis
|
|
16
|
+
from redis.asyncio import RedisCluster as AsyncRedisCluster
|
|
17
|
+
except ImportError:
|
|
18
|
+
_redis_available = False
|
|
19
|
+
_redis_import_error = "`redis` not installed. Please install it using `pip install redis`"
|
|
20
|
+
# Type hints for when redis is not installed
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from redis import Redis, RedisCluster
|
|
23
|
+
from redis.asyncio import Redis as AsyncRedis
|
|
24
|
+
from redis.asyncio import RedisCluster as AsyncRedisCluster
|
|
25
|
+
else:
|
|
26
|
+
Redis = Any
|
|
27
|
+
RedisCluster = Any
|
|
28
|
+
AsyncRedis = Any
|
|
29
|
+
AsyncRedisCluster = Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RedisRunCancellationManager(BaseRunCancellationManager):
|
|
33
|
+
"""Redis-based cancellation manager for distributed run cancellation.
|
|
34
|
+
This manager stores run cancellation state in Redis, enabling cancellation
|
|
35
|
+
across multiple processes or services.
|
|
36
|
+
|
|
37
|
+
To use: call the set_cancellation_manager function to set the cancellation manager.
|
|
38
|
+
Args:
|
|
39
|
+
redis_client: Sync Redis client for sync methods. Can be Redis or RedisCluster.
|
|
40
|
+
async_redis_client: Async Redis client for async methods. Can be AsyncRedis or AsyncRedisCluster.
|
|
41
|
+
key_prefix: Prefix for Redis keys. Defaults to "agno:run:cancellation:".
|
|
42
|
+
ttl_seconds: TTL for keys in seconds. Defaults to 86400 (1 day).
|
|
43
|
+
Keys auto-expire to prevent orphaned keys if runs aren't cleaned up.
|
|
44
|
+
Set to None to disable expiration.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
DEFAULT_TTL_SECONDS = 60 * 60 * 24 # 1 day
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
redis_client: Optional[Union[Redis, RedisCluster]] = None,
|
|
52
|
+
async_redis_client: Optional[Union[AsyncRedis, AsyncRedisCluster]] = None,
|
|
53
|
+
key_prefix: str = "agno:run:cancellation:",
|
|
54
|
+
ttl_seconds: Optional[int] = DEFAULT_TTL_SECONDS,
|
|
55
|
+
):
|
|
56
|
+
if not _redis_available:
|
|
57
|
+
raise ImportError(_redis_import_error)
|
|
58
|
+
|
|
59
|
+
super().__init__()
|
|
60
|
+
self.redis_client = redis_client
|
|
61
|
+
self.async_redis_client = async_redis_client
|
|
62
|
+
self.key_prefix = key_prefix
|
|
63
|
+
self.ttl_seconds = ttl_seconds
|
|
64
|
+
|
|
65
|
+
if redis_client is None and async_redis_client is None:
|
|
66
|
+
raise ValueError("At least one of redis_client or async_redis_client must be provided")
|
|
67
|
+
|
|
68
|
+
def _get_key(self, run_id: str) -> str:
|
|
69
|
+
"""Get the Redis key for a run ID."""
|
|
70
|
+
return f"{self.key_prefix}{run_id}"
|
|
71
|
+
|
|
72
|
+
def _ensure_sync_client(self) -> Union[Redis, RedisCluster]:
|
|
73
|
+
"""Ensure sync client is available."""
|
|
74
|
+
if self.redis_client is None:
|
|
75
|
+
raise RuntimeError("Sync Redis client not provided. Use async methods or provide a sync client.")
|
|
76
|
+
return self.redis_client
|
|
77
|
+
|
|
78
|
+
def _ensure_async_client(self) -> Union[AsyncRedis, AsyncRedisCluster]:
|
|
79
|
+
"""Ensure async client is available."""
|
|
80
|
+
if self.async_redis_client is None:
|
|
81
|
+
raise RuntimeError("Async Redis client not provided. Use sync methods or provide an async client.")
|
|
82
|
+
return self.async_redis_client
|
|
83
|
+
|
|
84
|
+
def register_run(self, run_id: str) -> None:
|
|
85
|
+
"""Register a new run as not cancelled."""
|
|
86
|
+
client = self._ensure_sync_client()
|
|
87
|
+
key = self._get_key(run_id)
|
|
88
|
+
client.set(key, "0", ex=self.ttl_seconds)
|
|
89
|
+
|
|
90
|
+
async def aregister_run(self, run_id: str) -> None:
|
|
91
|
+
"""Register a new run as not cancelled (async version)."""
|
|
92
|
+
client = self._ensure_async_client()
|
|
93
|
+
key = self._get_key(run_id)
|
|
94
|
+
await client.set(key, "0", ex=self.ttl_seconds)
|
|
95
|
+
|
|
96
|
+
def cancel_run(self, run_id: str) -> bool:
|
|
97
|
+
"""Cancel a run by marking it as cancelled.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
bool: True if run was found and cancelled, False if run not found.
|
|
101
|
+
"""
|
|
102
|
+
client = self._ensure_sync_client()
|
|
103
|
+
key = self._get_key(run_id)
|
|
104
|
+
|
|
105
|
+
# Atomically set to "1" only if key exists (XX flag)
|
|
106
|
+
result = client.set(key, "1", ex=self.ttl_seconds, xx=True)
|
|
107
|
+
|
|
108
|
+
if result:
|
|
109
|
+
logger.info(f"Run {run_id} marked for cancellation")
|
|
110
|
+
return True
|
|
111
|
+
else:
|
|
112
|
+
logger.warning(f"Attempted to cancel unknown run {run_id}")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
async def acancel_run(self, run_id: str) -> bool:
|
|
116
|
+
"""Cancel a run by marking it as cancelled (async version).
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
bool: True if run was found and cancelled, False if run not found.
|
|
120
|
+
"""
|
|
121
|
+
client = self._ensure_async_client()
|
|
122
|
+
key = self._get_key(run_id)
|
|
123
|
+
|
|
124
|
+
# Atomically set to "1" only if key exists (XX flag)
|
|
125
|
+
result = await client.set(key, "1", ex=self.ttl_seconds, xx=True)
|
|
126
|
+
|
|
127
|
+
if result:
|
|
128
|
+
logger.info(f"Run {run_id} marked for cancellation")
|
|
129
|
+
return True
|
|
130
|
+
else:
|
|
131
|
+
logger.warning(f"Attempted to cancel unknown run {run_id}")
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def is_cancelled(self, run_id: str) -> bool:
|
|
135
|
+
"""Check if a run is cancelled."""
|
|
136
|
+
client = self._ensure_sync_client()
|
|
137
|
+
key = self._get_key(run_id)
|
|
138
|
+
value = client.get(key)
|
|
139
|
+
if value is None:
|
|
140
|
+
return False
|
|
141
|
+
# Redis returns bytes, handle both bytes and str
|
|
142
|
+
if isinstance(value, bytes):
|
|
143
|
+
return value == b"1"
|
|
144
|
+
return value == "1"
|
|
145
|
+
|
|
146
|
+
async def ais_cancelled(self, run_id: str) -> bool:
|
|
147
|
+
"""Check if a run is cancelled (async version)."""
|
|
148
|
+
client = self._ensure_async_client()
|
|
149
|
+
key = self._get_key(run_id)
|
|
150
|
+
value = await client.get(key)
|
|
151
|
+
if value is None:
|
|
152
|
+
return False
|
|
153
|
+
# Redis returns bytes, handle both bytes and str
|
|
154
|
+
if isinstance(value, bytes):
|
|
155
|
+
return value == b"1"
|
|
156
|
+
return value == "1"
|
|
157
|
+
|
|
158
|
+
def cleanup_run(self, run_id: str) -> None:
|
|
159
|
+
"""Remove a run from tracking (called when run completes)."""
|
|
160
|
+
client = self._ensure_sync_client()
|
|
161
|
+
key = self._get_key(run_id)
|
|
162
|
+
client.delete(key)
|
|
163
|
+
|
|
164
|
+
async def acleanup_run(self, run_id: str) -> None:
|
|
165
|
+
"""Remove a run from tracking (called when run completes) (async version)."""
|
|
166
|
+
client = self._ensure_async_client()
|
|
167
|
+
key = self._get_key(run_id)
|
|
168
|
+
await client.delete(key)
|
|
169
|
+
|
|
170
|
+
def raise_if_cancelled(self, run_id: str) -> None:
|
|
171
|
+
"""Check if a run should be cancelled and raise exception if so."""
|
|
172
|
+
if self.is_cancelled(run_id):
|
|
173
|
+
logger.info(f"Cancelling run {run_id}")
|
|
174
|
+
raise RunCancelledException(f"Run {run_id} was cancelled")
|
|
175
|
+
|
|
176
|
+
async def araise_if_cancelled(self, run_id: str) -> None:
|
|
177
|
+
"""Check if a run should be cancelled and raise exception if so (async version)."""
|
|
178
|
+
if await self.ais_cancelled(run_id):
|
|
179
|
+
logger.info(f"Cancelling run {run_id}")
|
|
180
|
+
raise RunCancelledException(f"Run {run_id} was cancelled")
|
|
181
|
+
|
|
182
|
+
def get_active_runs(self) -> Dict[str, bool]:
|
|
183
|
+
"""Get all currently tracked runs and their cancellation status.
|
|
184
|
+
|
|
185
|
+
Note: Uses scan_iter which works correctly with both standalone Redis
|
|
186
|
+
and Redis Cluster (scans all nodes in cluster mode).
|
|
187
|
+
"""
|
|
188
|
+
client = self._ensure_sync_client()
|
|
189
|
+
result: Dict[str, bool] = {}
|
|
190
|
+
|
|
191
|
+
# scan_iter handles cluster mode correctly (scans all nodes)
|
|
192
|
+
pattern = f"{self.key_prefix}*"
|
|
193
|
+
for key in client.scan_iter(match=pattern, count=100):
|
|
194
|
+
# Extract run_id from key
|
|
195
|
+
if isinstance(key, bytes):
|
|
196
|
+
key = key.decode("utf-8")
|
|
197
|
+
run_id = key[len(self.key_prefix) :]
|
|
198
|
+
|
|
199
|
+
# Get value
|
|
200
|
+
value = client.get(key)
|
|
201
|
+
if value is not None:
|
|
202
|
+
if isinstance(value, bytes):
|
|
203
|
+
is_cancelled = value == b"1"
|
|
204
|
+
else:
|
|
205
|
+
is_cancelled = value == "1"
|
|
206
|
+
result[run_id] = is_cancelled
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
async def aget_active_runs(self) -> Dict[str, bool]:
|
|
211
|
+
"""Get all currently tracked runs and their cancellation status (async version).
|
|
212
|
+
|
|
213
|
+
Note: Uses scan_iter which works correctly with both standalone Redis
|
|
214
|
+
and Redis Cluster (scans all nodes in cluster mode).
|
|
215
|
+
"""
|
|
216
|
+
client = self._ensure_async_client()
|
|
217
|
+
result: Dict[str, bool] = {}
|
|
218
|
+
|
|
219
|
+
# scan_iter handles cluster mode correctly (scans all nodes)
|
|
220
|
+
pattern = f"{self.key_prefix}*"
|
|
221
|
+
async for key in client.scan_iter(match=pattern, count=100):
|
|
222
|
+
# Extract run_id from key
|
|
223
|
+
if isinstance(key, bytes):
|
|
224
|
+
key = key.decode("utf-8")
|
|
225
|
+
run_id = key[len(self.key_prefix) :]
|
|
226
|
+
|
|
227
|
+
# Get value
|
|
228
|
+
value = await client.get(key)
|
|
229
|
+
if value is not None:
|
|
230
|
+
if isinstance(value, bytes):
|
|
231
|
+
is_cancelled = value == b"1"
|
|
232
|
+
else:
|
|
233
|
+
is_cancelled = value == "1"
|
|
234
|
+
result[run_id] = is_cancelled
|
|
235
|
+
|
|
236
|
+
return result
|
agno/run/requirement.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from agno.models.response import ToolExecution, UserInputField
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class RunRequirement:
|
|
14
|
+
"""Requirement to complete a paused run (used in HITL flows)"""
|
|
15
|
+
|
|
16
|
+
tool_execution: Optional[ToolExecution] = None
|
|
17
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
18
|
+
|
|
19
|
+
# User confirmation
|
|
20
|
+
confirmation: Optional[bool] = None
|
|
21
|
+
confirmation_note: Optional[str] = None
|
|
22
|
+
|
|
23
|
+
# User input
|
|
24
|
+
user_input_schema: Optional[List[UserInputField]] = None
|
|
25
|
+
|
|
26
|
+
# External execution
|
|
27
|
+
external_execution_result: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
tool_execution: ToolExecution,
|
|
32
|
+
id: Optional[str] = None,
|
|
33
|
+
created_at: Optional[datetime] = None,
|
|
34
|
+
):
|
|
35
|
+
self.id = id or str(uuid4())
|
|
36
|
+
self.tool_execution = tool_execution
|
|
37
|
+
self.user_input_schema = tool_execution.user_input_schema if tool_execution else None
|
|
38
|
+
self.created_at = created_at or datetime.now(timezone.utc)
|
|
39
|
+
self.confirmation = None
|
|
40
|
+
self.confirmation_note = None
|
|
41
|
+
self.external_execution_result = None
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def needs_confirmation(self) -> bool:
|
|
45
|
+
if self.confirmation is not None:
|
|
46
|
+
return False
|
|
47
|
+
if not self.tool_execution:
|
|
48
|
+
return False
|
|
49
|
+
if self.tool_execution.confirmed is True:
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
return self.tool_execution.requires_confirmation or False
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def needs_user_input(self) -> bool:
|
|
56
|
+
if not self.tool_execution:
|
|
57
|
+
return False
|
|
58
|
+
if self.tool_execution.answered is True:
|
|
59
|
+
return False
|
|
60
|
+
if self.user_input_schema and not all(field.value is not None for field in self.user_input_schema):
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
return self.tool_execution.requires_user_input or False
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def needs_external_execution(self) -> bool:
|
|
67
|
+
if not self.tool_execution:
|
|
68
|
+
return False
|
|
69
|
+
if self.external_execution_result is not None:
|
|
70
|
+
return True
|
|
71
|
+
|
|
72
|
+
return self.tool_execution.external_execution_required or False
|
|
73
|
+
|
|
74
|
+
def confirm(self):
|
|
75
|
+
if not self.needs_confirmation:
|
|
76
|
+
raise ValueError("This requirement does not require confirmation")
|
|
77
|
+
self.confirmation = True
|
|
78
|
+
if self.tool_execution:
|
|
79
|
+
self.tool_execution.confirmed = True
|
|
80
|
+
|
|
81
|
+
def reject(self):
|
|
82
|
+
if not self.needs_confirmation:
|
|
83
|
+
raise ValueError("This requirement does not require confirmation")
|
|
84
|
+
self.confirmation = False
|
|
85
|
+
if self.tool_execution:
|
|
86
|
+
self.tool_execution.confirmed = False
|
|
87
|
+
|
|
88
|
+
def set_external_execution_result(self, result: str):
|
|
89
|
+
if not self.needs_external_execution:
|
|
90
|
+
raise ValueError("This requirement does not require external execution")
|
|
91
|
+
self.external_execution_result = result
|
|
92
|
+
if self.tool_execution:
|
|
93
|
+
self.tool_execution.result = result
|
|
94
|
+
|
|
95
|
+
def update_tool(self):
|
|
96
|
+
if not self.tool_execution:
|
|
97
|
+
return
|
|
98
|
+
if self.confirmation is True:
|
|
99
|
+
self.tool_execution.confirmed = True
|
|
100
|
+
elif self.confirmation is False:
|
|
101
|
+
self.tool_execution.confirmed = False
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError("This requirement does not require confirmation or user input")
|
|
104
|
+
|
|
105
|
+
def is_resolved(self) -> bool:
|
|
106
|
+
"""Return True if the requirement has been resolved"""
|
|
107
|
+
return not self.needs_confirmation and not self.needs_user_input and not self.needs_external_execution
|
|
108
|
+
|
|
109
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
110
|
+
"""Convert to JSON-serializable dictionary for storage."""
|
|
111
|
+
_dict: Dict[str, Any] = {
|
|
112
|
+
"id": self.id,
|
|
113
|
+
"created_at": self.created_at.isoformat() if isinstance(self.created_at, datetime) else self.created_at,
|
|
114
|
+
"confirmation": self.confirmation,
|
|
115
|
+
"confirmation_note": self.confirmation_note,
|
|
116
|
+
"external_execution_result": self.external_execution_result,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if self.tool_execution is not None:
|
|
120
|
+
_dict["tool_execution"] = (
|
|
121
|
+
self.tool_execution.to_dict() if isinstance(self.tool_execution, ToolExecution) else self.tool_execution
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if self.user_input_schema is not None:
|
|
125
|
+
_dict["user_input_schema"] = [f.to_dict() if hasattr(f, "to_dict") else f for f in self.user_input_schema]
|
|
126
|
+
|
|
127
|
+
return {k: v for k, v in _dict.items() if v is not None}
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_dict(cls, data: Dict[str, Any]) -> "RunRequirement":
|
|
131
|
+
"""Reconstruct from stored dictionary."""
|
|
132
|
+
if data is None:
|
|
133
|
+
raise ValueError("RunRequirement.from_dict() requires a non-None dict")
|
|
134
|
+
|
|
135
|
+
# Handle tool_execution
|
|
136
|
+
tool_data = data.get("tool_execution")
|
|
137
|
+
tool_execution: Optional[ToolExecution] = None
|
|
138
|
+
if isinstance(tool_data, ToolExecution):
|
|
139
|
+
tool_execution = tool_data
|
|
140
|
+
elif isinstance(tool_data, dict):
|
|
141
|
+
tool_execution = ToolExecution.from_dict(tool_data)
|
|
142
|
+
|
|
143
|
+
# Handle created_at (ISO string or datetime)
|
|
144
|
+
created_at_raw = data.get("created_at")
|
|
145
|
+
created_at: Optional[datetime] = None
|
|
146
|
+
if isinstance(created_at_raw, datetime):
|
|
147
|
+
created_at = created_at_raw
|
|
148
|
+
elif isinstance(created_at_raw, str):
|
|
149
|
+
try:
|
|
150
|
+
created_at = datetime.fromisoformat(created_at_raw)
|
|
151
|
+
except ValueError:
|
|
152
|
+
created_at = None
|
|
153
|
+
|
|
154
|
+
# Build requirement - tool_execution is required by __init__
|
|
155
|
+
# For legacy data without tool_execution, create a minimal placeholder
|
|
156
|
+
if tool_execution is None:
|
|
157
|
+
tool_execution = ToolExecution(tool_name="unknown", tool_args={})
|
|
158
|
+
|
|
159
|
+
requirement = cls(
|
|
160
|
+
tool_execution=tool_execution,
|
|
161
|
+
id=data.get("id"),
|
|
162
|
+
created_at=created_at,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Set optional fields
|
|
166
|
+
requirement.confirmation = data.get("confirmation")
|
|
167
|
+
requirement.confirmation_note = data.get("confirmation_note")
|
|
168
|
+
requirement.external_execution_result = data.get("external_execution_result")
|
|
169
|
+
|
|
170
|
+
# Handle user_input_schema
|
|
171
|
+
schema_raw = data.get("user_input_schema")
|
|
172
|
+
if schema_raw is not None:
|
|
173
|
+
rebuilt_schema: List[UserInputField] = []
|
|
174
|
+
for item in schema_raw:
|
|
175
|
+
if isinstance(item, UserInputField):
|
|
176
|
+
rebuilt_schema.append(item)
|
|
177
|
+
elif isinstance(item, dict):
|
|
178
|
+
rebuilt_schema.append(UserInputField.from_dict(item))
|
|
179
|
+
requirement.user_input_schema = rebuilt_schema if rebuilt_schema else None
|
|
180
|
+
|
|
181
|
+
return requirement
|