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,1156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Decision Log Store
|
|
3
|
+
==================
|
|
4
|
+
Storage backend for Decision Log learning type.
|
|
5
|
+
|
|
6
|
+
Records decisions made by agents with reasoning, context, and outcomes.
|
|
7
|
+
Useful for auditing, debugging, and learning from past decisions.
|
|
8
|
+
|
|
9
|
+
Key Features:
|
|
10
|
+
- Log decisions with reasoning and context
|
|
11
|
+
- Record outcomes for feedback loops
|
|
12
|
+
- Search past decisions by type, time range, or content
|
|
13
|
+
- Agent tools for explicit decision logging
|
|
14
|
+
|
|
15
|
+
Scope:
|
|
16
|
+
- Decisions are stored per agent/session
|
|
17
|
+
- Can be queried by agent_id, session_id, or time range
|
|
18
|
+
|
|
19
|
+
Supported Modes:
|
|
20
|
+
- ALWAYS: Automatic extraction of decisions from tool calls
|
|
21
|
+
- AGENTIC: Agent explicitly logs decisions via tools
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import uuid
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import datetime, timedelta
|
|
27
|
+
from os import getenv
|
|
28
|
+
from textwrap import dedent
|
|
29
|
+
from typing import Any, Callable, List, Optional, Union
|
|
30
|
+
|
|
31
|
+
from agno.learn.config import DecisionLogConfig, LearningMode
|
|
32
|
+
from agno.learn.schemas import DecisionLog
|
|
33
|
+
from agno.learn.stores.protocol import LearningStore
|
|
34
|
+
from agno.learn.utils import from_dict_safe, to_dict_safe
|
|
35
|
+
from agno.utils.log import (
|
|
36
|
+
log_debug,
|
|
37
|
+
log_warning,
|
|
38
|
+
set_log_level_to_debug,
|
|
39
|
+
set_log_level_to_info,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
44
|
+
from agno.models.message import Message
|
|
45
|
+
except ImportError:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class DecisionLogStore(LearningStore):
|
|
51
|
+
"""Storage backend for Decision Log learning type.
|
|
52
|
+
|
|
53
|
+
Records and retrieves decisions made by agents. Decisions include
|
|
54
|
+
the choice made, reasoning, context, and optionally the outcome.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
config: DecisionLogConfig with all settings including db and model.
|
|
58
|
+
debug_mode: Enable debug logging.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
config: DecisionLogConfig = field(default_factory=DecisionLogConfig)
|
|
62
|
+
debug_mode: bool = False
|
|
63
|
+
|
|
64
|
+
# State tracking (internal)
|
|
65
|
+
decisions_updated: bool = field(default=False, init=False)
|
|
66
|
+
_schema: Any = field(default=None, init=False)
|
|
67
|
+
|
|
68
|
+
def __post_init__(self):
|
|
69
|
+
self._schema = self.config.schema or DecisionLog
|
|
70
|
+
|
|
71
|
+
# =========================================================================
|
|
72
|
+
# LearningStore Protocol Implementation
|
|
73
|
+
# =========================================================================
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def learning_type(self) -> str:
|
|
77
|
+
"""Unique identifier for this learning type."""
|
|
78
|
+
return "decision_log"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def schema(self) -> Any:
|
|
82
|
+
"""Schema class used for decisions."""
|
|
83
|
+
return self._schema
|
|
84
|
+
|
|
85
|
+
def recall(
|
|
86
|
+
self,
|
|
87
|
+
agent_id: Optional[str] = None,
|
|
88
|
+
session_id: Optional[str] = None,
|
|
89
|
+
decision_type: Optional[str] = None,
|
|
90
|
+
limit: int = 10,
|
|
91
|
+
days: Optional[int] = None,
|
|
92
|
+
**kwargs,
|
|
93
|
+
) -> Optional[List[DecisionLog]]:
|
|
94
|
+
"""Retrieve recent decisions.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
agent_id: Filter by agent (optional).
|
|
98
|
+
session_id: Filter by session (optional).
|
|
99
|
+
decision_type: Filter by decision type (optional).
|
|
100
|
+
limit: Maximum number of decisions to return.
|
|
101
|
+
days: Only return decisions from last N days.
|
|
102
|
+
**kwargs: Additional context (ignored).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
List of decisions, or None if none found.
|
|
106
|
+
"""
|
|
107
|
+
return self.search(
|
|
108
|
+
agent_id=agent_id,
|
|
109
|
+
session_id=session_id,
|
|
110
|
+
decision_type=decision_type,
|
|
111
|
+
limit=limit,
|
|
112
|
+
days=days,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def arecall(
|
|
116
|
+
self,
|
|
117
|
+
agent_id: Optional[str] = None,
|
|
118
|
+
session_id: Optional[str] = None,
|
|
119
|
+
decision_type: Optional[str] = None,
|
|
120
|
+
limit: int = 10,
|
|
121
|
+
days: Optional[int] = None,
|
|
122
|
+
**kwargs,
|
|
123
|
+
) -> Optional[List[DecisionLog]]:
|
|
124
|
+
"""Async version of recall."""
|
|
125
|
+
return await self.asearch(
|
|
126
|
+
agent_id=agent_id,
|
|
127
|
+
session_id=session_id,
|
|
128
|
+
decision_type=decision_type,
|
|
129
|
+
limit=limit,
|
|
130
|
+
days=days,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def process(
|
|
134
|
+
self,
|
|
135
|
+
messages: List[Any],
|
|
136
|
+
agent_id: Optional[str] = None,
|
|
137
|
+
session_id: Optional[str] = None,
|
|
138
|
+
user_id: Optional[str] = None,
|
|
139
|
+
team_id: Optional[str] = None,
|
|
140
|
+
**kwargs,
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Extract decisions from messages (tool calls, etc).
|
|
143
|
+
|
|
144
|
+
In ALWAYS mode, this extracts decisions from tool calls and
|
|
145
|
+
significant response choices. In AGENTIC mode, this is a no-op
|
|
146
|
+
as decisions are logged explicitly via tools.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
messages: Conversation messages to analyze.
|
|
150
|
+
agent_id: Agent context.
|
|
151
|
+
session_id: Session context.
|
|
152
|
+
user_id: User context.
|
|
153
|
+
team_id: Team context.
|
|
154
|
+
**kwargs: Additional context (ignored).
|
|
155
|
+
"""
|
|
156
|
+
if self.config.mode != LearningMode.ALWAYS:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
if not messages:
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Extract decisions from tool calls in messages
|
|
163
|
+
self._extract_decisions_from_messages(
|
|
164
|
+
messages=messages,
|
|
165
|
+
agent_id=agent_id,
|
|
166
|
+
session_id=session_id,
|
|
167
|
+
user_id=user_id,
|
|
168
|
+
team_id=team_id,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def aprocess(
|
|
172
|
+
self,
|
|
173
|
+
messages: List[Any],
|
|
174
|
+
agent_id: Optional[str] = None,
|
|
175
|
+
session_id: Optional[str] = None,
|
|
176
|
+
user_id: Optional[str] = None,
|
|
177
|
+
team_id: Optional[str] = None,
|
|
178
|
+
**kwargs,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Async version of process."""
|
|
181
|
+
if self.config.mode != LearningMode.ALWAYS:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if not messages:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
await self._aextract_decisions_from_messages(
|
|
188
|
+
messages=messages,
|
|
189
|
+
agent_id=agent_id,
|
|
190
|
+
session_id=session_id,
|
|
191
|
+
user_id=user_id,
|
|
192
|
+
team_id=team_id,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def build_context(self, data: Any) -> str:
|
|
196
|
+
"""Build context for the agent.
|
|
197
|
+
|
|
198
|
+
Formats recent decisions for injection into the agent's system prompt.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
data: List of decisions from recall().
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Context string to inject into the agent's system prompt.
|
|
205
|
+
"""
|
|
206
|
+
if not data:
|
|
207
|
+
if self._should_expose_tools:
|
|
208
|
+
return dedent("""\
|
|
209
|
+
<decision_log>
|
|
210
|
+
No recent decisions logged.
|
|
211
|
+
|
|
212
|
+
Use `log_decision` to record significant decisions with reasoning.
|
|
213
|
+
Use `search_decisions` to find past decisions.
|
|
214
|
+
</decision_log>""")
|
|
215
|
+
return ""
|
|
216
|
+
|
|
217
|
+
decisions = data if isinstance(data, list) else [data]
|
|
218
|
+
|
|
219
|
+
context = "<decision_log>\n"
|
|
220
|
+
context += "Recent decisions:\n\n"
|
|
221
|
+
|
|
222
|
+
for decision in decisions[:5]: # Limit to 5 most recent
|
|
223
|
+
if isinstance(decision, DecisionLog):
|
|
224
|
+
context += f"- **{decision.decision}**\n"
|
|
225
|
+
if decision.reasoning:
|
|
226
|
+
context += f" Reasoning: {decision.reasoning}\n"
|
|
227
|
+
if decision.outcome:
|
|
228
|
+
context += f" Outcome: {decision.outcome}\n"
|
|
229
|
+
context += "\n"
|
|
230
|
+
elif isinstance(decision, dict):
|
|
231
|
+
context += f"- **{decision.get('decision', 'Unknown')}**\n"
|
|
232
|
+
if decision.get("reasoning"):
|
|
233
|
+
context += f" Reasoning: {decision['reasoning']}\n"
|
|
234
|
+
if decision.get("outcome"):
|
|
235
|
+
context += f" Outcome: {decision['outcome']}\n"
|
|
236
|
+
context += "\n"
|
|
237
|
+
|
|
238
|
+
if self._should_expose_tools:
|
|
239
|
+
context += dedent("""
|
|
240
|
+
Use `log_decision` to record new decisions.
|
|
241
|
+
Use `search_decisions` to find past decisions.
|
|
242
|
+
Use `record_outcome` to update a decision with its outcome.
|
|
243
|
+
""")
|
|
244
|
+
|
|
245
|
+
context += "</decision_log>"
|
|
246
|
+
|
|
247
|
+
return context
|
|
248
|
+
|
|
249
|
+
def get_tools(
|
|
250
|
+
self,
|
|
251
|
+
agent_id: Optional[str] = None,
|
|
252
|
+
session_id: Optional[str] = None,
|
|
253
|
+
user_id: Optional[str] = None,
|
|
254
|
+
team_id: Optional[str] = None,
|
|
255
|
+
**kwargs,
|
|
256
|
+
) -> List[Callable]:
|
|
257
|
+
"""Get tools to expose to agent.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
agent_id: Agent context.
|
|
261
|
+
session_id: Session context.
|
|
262
|
+
user_id: User context.
|
|
263
|
+
team_id: Team context.
|
|
264
|
+
**kwargs: Additional context (ignored).
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
List containing decision logging and search tools if enabled.
|
|
268
|
+
"""
|
|
269
|
+
if not self._should_expose_tools:
|
|
270
|
+
return []
|
|
271
|
+
return self.get_agent_tools(
|
|
272
|
+
agent_id=agent_id,
|
|
273
|
+
session_id=session_id,
|
|
274
|
+
user_id=user_id,
|
|
275
|
+
team_id=team_id,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
async def aget_tools(
|
|
279
|
+
self,
|
|
280
|
+
agent_id: Optional[str] = None,
|
|
281
|
+
session_id: Optional[str] = None,
|
|
282
|
+
user_id: Optional[str] = None,
|
|
283
|
+
team_id: Optional[str] = None,
|
|
284
|
+
**kwargs,
|
|
285
|
+
) -> List[Callable]:
|
|
286
|
+
"""Async version of get_tools."""
|
|
287
|
+
if not self._should_expose_tools:
|
|
288
|
+
return []
|
|
289
|
+
return await self.aget_agent_tools(
|
|
290
|
+
agent_id=agent_id,
|
|
291
|
+
session_id=session_id,
|
|
292
|
+
user_id=user_id,
|
|
293
|
+
team_id=team_id,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def was_updated(self) -> bool:
|
|
298
|
+
"""Check if decisions were updated in last operation."""
|
|
299
|
+
return self.decisions_updated
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def _should_expose_tools(self) -> bool:
|
|
303
|
+
"""Check if tools should be exposed to the agent."""
|
|
304
|
+
return self.config.mode == LearningMode.AGENTIC or self.config.enable_agent_tools
|
|
305
|
+
|
|
306
|
+
# =========================================================================
|
|
307
|
+
# Properties
|
|
308
|
+
# =========================================================================
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def db(self) -> Optional[Union["BaseDb", "AsyncBaseDb"]]:
|
|
312
|
+
"""Database backend."""
|
|
313
|
+
return self.config.db
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def model(self):
|
|
317
|
+
"""Model for extraction."""
|
|
318
|
+
return self.config.model
|
|
319
|
+
|
|
320
|
+
# =========================================================================
|
|
321
|
+
# Debug/Logging
|
|
322
|
+
# =========================================================================
|
|
323
|
+
|
|
324
|
+
def set_log_level(self):
|
|
325
|
+
"""Set log level based on debug_mode or environment variable."""
|
|
326
|
+
if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true":
|
|
327
|
+
self.debug_mode = True
|
|
328
|
+
set_log_level_to_debug()
|
|
329
|
+
else:
|
|
330
|
+
set_log_level_to_info()
|
|
331
|
+
|
|
332
|
+
# =========================================================================
|
|
333
|
+
# Agent Tools
|
|
334
|
+
# =========================================================================
|
|
335
|
+
|
|
336
|
+
def get_agent_tools(
|
|
337
|
+
self,
|
|
338
|
+
agent_id: Optional[str] = None,
|
|
339
|
+
session_id: Optional[str] = None,
|
|
340
|
+
user_id: Optional[str] = None,
|
|
341
|
+
team_id: Optional[str] = None,
|
|
342
|
+
) -> List[Callable]:
|
|
343
|
+
"""Get the tools to expose to the agent."""
|
|
344
|
+
tools = []
|
|
345
|
+
|
|
346
|
+
if self.config.agent_can_save:
|
|
347
|
+
log_decision = self._build_log_decision_tool(
|
|
348
|
+
agent_id=agent_id,
|
|
349
|
+
session_id=session_id,
|
|
350
|
+
user_id=user_id,
|
|
351
|
+
team_id=team_id,
|
|
352
|
+
)
|
|
353
|
+
if log_decision:
|
|
354
|
+
tools.append(log_decision)
|
|
355
|
+
|
|
356
|
+
record_outcome = self._build_record_outcome_tool(
|
|
357
|
+
agent_id=agent_id,
|
|
358
|
+
team_id=team_id,
|
|
359
|
+
)
|
|
360
|
+
if record_outcome:
|
|
361
|
+
tools.append(record_outcome)
|
|
362
|
+
|
|
363
|
+
if self.config.agent_can_search:
|
|
364
|
+
search_decisions = self._build_search_decisions_tool(
|
|
365
|
+
agent_id=agent_id,
|
|
366
|
+
session_id=session_id,
|
|
367
|
+
)
|
|
368
|
+
if search_decisions:
|
|
369
|
+
tools.append(search_decisions)
|
|
370
|
+
|
|
371
|
+
return tools
|
|
372
|
+
|
|
373
|
+
async def aget_agent_tools(
|
|
374
|
+
self,
|
|
375
|
+
agent_id: Optional[str] = None,
|
|
376
|
+
session_id: Optional[str] = None,
|
|
377
|
+
user_id: Optional[str] = None,
|
|
378
|
+
team_id: Optional[str] = None,
|
|
379
|
+
) -> List[Callable]:
|
|
380
|
+
"""Async version of get_agent_tools."""
|
|
381
|
+
tools = []
|
|
382
|
+
|
|
383
|
+
if self.config.agent_can_save:
|
|
384
|
+
log_decision = await self._abuild_log_decision_tool(
|
|
385
|
+
agent_id=agent_id,
|
|
386
|
+
session_id=session_id,
|
|
387
|
+
user_id=user_id,
|
|
388
|
+
team_id=team_id,
|
|
389
|
+
)
|
|
390
|
+
if log_decision:
|
|
391
|
+
tools.append(log_decision)
|
|
392
|
+
|
|
393
|
+
record_outcome = await self._abuild_record_outcome_tool(
|
|
394
|
+
agent_id=agent_id,
|
|
395
|
+
team_id=team_id,
|
|
396
|
+
)
|
|
397
|
+
if record_outcome:
|
|
398
|
+
tools.append(record_outcome)
|
|
399
|
+
|
|
400
|
+
if self.config.agent_can_search:
|
|
401
|
+
search_decisions = await self._abuild_search_decisions_tool(
|
|
402
|
+
agent_id=agent_id,
|
|
403
|
+
session_id=session_id,
|
|
404
|
+
)
|
|
405
|
+
if search_decisions:
|
|
406
|
+
tools.append(search_decisions)
|
|
407
|
+
|
|
408
|
+
return tools
|
|
409
|
+
|
|
410
|
+
def _build_log_decision_tool(
|
|
411
|
+
self,
|
|
412
|
+
agent_id: Optional[str] = None,
|
|
413
|
+
session_id: Optional[str] = None,
|
|
414
|
+
user_id: Optional[str] = None,
|
|
415
|
+
team_id: Optional[str] = None,
|
|
416
|
+
) -> Optional[Callable]:
|
|
417
|
+
"""Build the log_decision tool."""
|
|
418
|
+
store = self
|
|
419
|
+
|
|
420
|
+
def log_decision(
|
|
421
|
+
decision: str,
|
|
422
|
+
reasoning: Optional[str] = None,
|
|
423
|
+
decision_type: Optional[str] = None,
|
|
424
|
+
context: Optional[str] = None,
|
|
425
|
+
alternatives: Optional[str] = None,
|
|
426
|
+
confidence: Optional[float] = None,
|
|
427
|
+
) -> str:
|
|
428
|
+
"""Log a significant decision with reasoning.
|
|
429
|
+
|
|
430
|
+
Use this to record important choices you make, especially:
|
|
431
|
+
- Tool selection decisions
|
|
432
|
+
- Response style choices
|
|
433
|
+
- When you decide to ask for clarification
|
|
434
|
+
- When you choose between different approaches
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
decision: What you decided to do.
|
|
438
|
+
reasoning: Why you made this decision.
|
|
439
|
+
decision_type: Category (tool_selection, response_style, clarification, etc).
|
|
440
|
+
context: The situation that required this decision.
|
|
441
|
+
alternatives: Other options you considered (comma-separated).
|
|
442
|
+
confidence: How confident you are (0.0 to 1.0).
|
|
443
|
+
|
|
444
|
+
Returns:
|
|
445
|
+
Confirmation with decision ID.
|
|
446
|
+
"""
|
|
447
|
+
try:
|
|
448
|
+
decision_id = f"dec_{uuid.uuid4().hex[:8]}"
|
|
449
|
+
alt_list = [a.strip() for a in alternatives.split(",")] if alternatives else None
|
|
450
|
+
|
|
451
|
+
decision_obj = DecisionLog(
|
|
452
|
+
id=decision_id,
|
|
453
|
+
decision=decision,
|
|
454
|
+
reasoning=reasoning,
|
|
455
|
+
decision_type=decision_type,
|
|
456
|
+
context=context,
|
|
457
|
+
alternatives=alt_list,
|
|
458
|
+
confidence=confidence,
|
|
459
|
+
session_id=session_id,
|
|
460
|
+
user_id=user_id,
|
|
461
|
+
agent_id=agent_id,
|
|
462
|
+
team_id=team_id,
|
|
463
|
+
created_at=datetime.utcnow().isoformat(),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
store.save(decision=decision_obj)
|
|
467
|
+
log_debug(f"DecisionLogStore: Logged decision {decision_id}")
|
|
468
|
+
return f"Decision logged: {decision_id}"
|
|
469
|
+
|
|
470
|
+
except Exception as e:
|
|
471
|
+
log_warning(f"Error logging decision: {e}")
|
|
472
|
+
return f"Error: {e}"
|
|
473
|
+
|
|
474
|
+
return log_decision
|
|
475
|
+
|
|
476
|
+
async def _abuild_log_decision_tool(
|
|
477
|
+
self,
|
|
478
|
+
agent_id: Optional[str] = None,
|
|
479
|
+
session_id: Optional[str] = None,
|
|
480
|
+
user_id: Optional[str] = None,
|
|
481
|
+
team_id: Optional[str] = None,
|
|
482
|
+
) -> Optional[Callable]:
|
|
483
|
+
"""Async version of _build_log_decision_tool."""
|
|
484
|
+
store = self
|
|
485
|
+
|
|
486
|
+
async def log_decision(
|
|
487
|
+
decision: str,
|
|
488
|
+
reasoning: Optional[str] = None,
|
|
489
|
+
decision_type: Optional[str] = None,
|
|
490
|
+
context: Optional[str] = None,
|
|
491
|
+
alternatives: Optional[str] = None,
|
|
492
|
+
confidence: Optional[float] = None,
|
|
493
|
+
) -> str:
|
|
494
|
+
"""Log a significant decision with reasoning."""
|
|
495
|
+
try:
|
|
496
|
+
decision_id = f"dec_{uuid.uuid4().hex[:8]}"
|
|
497
|
+
alt_list = [a.strip() for a in alternatives.split(",")] if alternatives else None
|
|
498
|
+
|
|
499
|
+
decision_obj = DecisionLog(
|
|
500
|
+
id=decision_id,
|
|
501
|
+
decision=decision,
|
|
502
|
+
reasoning=reasoning,
|
|
503
|
+
decision_type=decision_type,
|
|
504
|
+
context=context,
|
|
505
|
+
alternatives=alt_list,
|
|
506
|
+
confidence=confidence,
|
|
507
|
+
session_id=session_id,
|
|
508
|
+
user_id=user_id,
|
|
509
|
+
agent_id=agent_id,
|
|
510
|
+
team_id=team_id,
|
|
511
|
+
created_at=datetime.utcnow().isoformat(),
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
await store.asave(decision=decision_obj)
|
|
515
|
+
log_debug(f"DecisionLogStore: Logged decision {decision_id}")
|
|
516
|
+
return f"Decision logged: {decision_id}"
|
|
517
|
+
|
|
518
|
+
except Exception as e:
|
|
519
|
+
log_warning(f"Error logging decision: {e}")
|
|
520
|
+
return f"Error: {e}"
|
|
521
|
+
|
|
522
|
+
return log_decision
|
|
523
|
+
|
|
524
|
+
def _build_record_outcome_tool(
|
|
525
|
+
self,
|
|
526
|
+
agent_id: Optional[str] = None,
|
|
527
|
+
team_id: Optional[str] = None,
|
|
528
|
+
) -> Optional[Callable]:
|
|
529
|
+
"""Build the record_outcome tool."""
|
|
530
|
+
store = self
|
|
531
|
+
|
|
532
|
+
def record_outcome(
|
|
533
|
+
decision_id: str,
|
|
534
|
+
outcome: str,
|
|
535
|
+
outcome_quality: Optional[str] = None,
|
|
536
|
+
) -> str:
|
|
537
|
+
"""Record the outcome of a previous decision.
|
|
538
|
+
|
|
539
|
+
Use this to update a decision with what actually happened.
|
|
540
|
+
This helps build feedback loops for learning.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
decision_id: The ID of the decision to update.
|
|
544
|
+
outcome: What happened as a result of the decision.
|
|
545
|
+
outcome_quality: Was it good, bad, or neutral?
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
Confirmation message.
|
|
549
|
+
"""
|
|
550
|
+
try:
|
|
551
|
+
success = store.update_outcome(
|
|
552
|
+
decision_id=decision_id,
|
|
553
|
+
outcome=outcome,
|
|
554
|
+
outcome_quality=outcome_quality,
|
|
555
|
+
)
|
|
556
|
+
if success:
|
|
557
|
+
return f"Outcome recorded for decision {decision_id}"
|
|
558
|
+
else:
|
|
559
|
+
return f"Decision {decision_id} not found"
|
|
560
|
+
|
|
561
|
+
except Exception as e:
|
|
562
|
+
log_warning(f"Error recording outcome: {e}")
|
|
563
|
+
return f"Error: {e}"
|
|
564
|
+
|
|
565
|
+
return record_outcome
|
|
566
|
+
|
|
567
|
+
async def _abuild_record_outcome_tool(
|
|
568
|
+
self,
|
|
569
|
+
agent_id: Optional[str] = None,
|
|
570
|
+
team_id: Optional[str] = None,
|
|
571
|
+
) -> Optional[Callable]:
|
|
572
|
+
"""Async version of _build_record_outcome_tool."""
|
|
573
|
+
store = self
|
|
574
|
+
|
|
575
|
+
async def record_outcome(
|
|
576
|
+
decision_id: str,
|
|
577
|
+
outcome: str,
|
|
578
|
+
outcome_quality: Optional[str] = None,
|
|
579
|
+
) -> str:
|
|
580
|
+
"""Record the outcome of a previous decision."""
|
|
581
|
+
try:
|
|
582
|
+
success = await store.aupdate_outcome(
|
|
583
|
+
decision_id=decision_id,
|
|
584
|
+
outcome=outcome,
|
|
585
|
+
outcome_quality=outcome_quality,
|
|
586
|
+
)
|
|
587
|
+
if success:
|
|
588
|
+
return f"Outcome recorded for decision {decision_id}"
|
|
589
|
+
else:
|
|
590
|
+
return f"Decision {decision_id} not found"
|
|
591
|
+
|
|
592
|
+
except Exception as e:
|
|
593
|
+
log_warning(f"Error recording outcome: {e}")
|
|
594
|
+
return f"Error: {e}"
|
|
595
|
+
|
|
596
|
+
return record_outcome
|
|
597
|
+
|
|
598
|
+
def _build_search_decisions_tool(
|
|
599
|
+
self,
|
|
600
|
+
agent_id: Optional[str] = None,
|
|
601
|
+
session_id: Optional[str] = None,
|
|
602
|
+
) -> Optional[Callable]:
|
|
603
|
+
"""Build the search_decisions tool."""
|
|
604
|
+
store = self
|
|
605
|
+
|
|
606
|
+
def search_decisions(
|
|
607
|
+
query: Optional[str] = None,
|
|
608
|
+
decision_type: Optional[str] = None,
|
|
609
|
+
days: Optional[int] = None,
|
|
610
|
+
limit: int = 5,
|
|
611
|
+
) -> str:
|
|
612
|
+
"""Search past decisions.
|
|
613
|
+
|
|
614
|
+
Use this to find relevant past decisions for context.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
query: Text to search for in decisions.
|
|
618
|
+
decision_type: Filter by type (tool_selection, response_style, etc).
|
|
619
|
+
days: Only search last N days.
|
|
620
|
+
limit: Maximum results to return.
|
|
621
|
+
|
|
622
|
+
Returns:
|
|
623
|
+
Formatted list of matching decisions.
|
|
624
|
+
"""
|
|
625
|
+
try:
|
|
626
|
+
results = store.search(
|
|
627
|
+
query=query,
|
|
628
|
+
decision_type=decision_type,
|
|
629
|
+
days=days,
|
|
630
|
+
limit=limit,
|
|
631
|
+
agent_id=agent_id,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
if not results:
|
|
635
|
+
return "No matching decisions found."
|
|
636
|
+
|
|
637
|
+
output = []
|
|
638
|
+
for d in results:
|
|
639
|
+
line = f"[{d.id}] {d.decision}"
|
|
640
|
+
if d.reasoning:
|
|
641
|
+
line += f" - {d.reasoning[:50]}..."
|
|
642
|
+
if d.outcome:
|
|
643
|
+
line += f" -> {d.outcome[:30]}..."
|
|
644
|
+
output.append(line)
|
|
645
|
+
|
|
646
|
+
return "\n".join(output)
|
|
647
|
+
|
|
648
|
+
except Exception as e:
|
|
649
|
+
log_warning(f"Error searching decisions: {e}")
|
|
650
|
+
return f"Error: {e}"
|
|
651
|
+
|
|
652
|
+
return search_decisions
|
|
653
|
+
|
|
654
|
+
async def _abuild_search_decisions_tool(
|
|
655
|
+
self,
|
|
656
|
+
agent_id: Optional[str] = None,
|
|
657
|
+
session_id: Optional[str] = None,
|
|
658
|
+
) -> Optional[Callable]:
|
|
659
|
+
"""Async version of _build_search_decisions_tool."""
|
|
660
|
+
store = self
|
|
661
|
+
|
|
662
|
+
async def search_decisions(
|
|
663
|
+
query: Optional[str] = None,
|
|
664
|
+
decision_type: Optional[str] = None,
|
|
665
|
+
days: Optional[int] = None,
|
|
666
|
+
limit: int = 5,
|
|
667
|
+
) -> str:
|
|
668
|
+
"""Search past decisions."""
|
|
669
|
+
try:
|
|
670
|
+
results = await store.asearch(
|
|
671
|
+
query=query,
|
|
672
|
+
decision_type=decision_type,
|
|
673
|
+
days=days,
|
|
674
|
+
limit=limit,
|
|
675
|
+
agent_id=agent_id,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
if not results:
|
|
679
|
+
return "No matching decisions found."
|
|
680
|
+
|
|
681
|
+
output = []
|
|
682
|
+
for d in results:
|
|
683
|
+
line = f"[{d.id}] {d.decision}"
|
|
684
|
+
if d.reasoning:
|
|
685
|
+
line += f" - {d.reasoning[:50]}..."
|
|
686
|
+
if d.outcome:
|
|
687
|
+
line += f" -> {d.outcome[:30]}..."
|
|
688
|
+
output.append(line)
|
|
689
|
+
|
|
690
|
+
return "\n".join(output)
|
|
691
|
+
|
|
692
|
+
except Exception as e:
|
|
693
|
+
log_warning(f"Error searching decisions: {e}")
|
|
694
|
+
return f"Error: {e}"
|
|
695
|
+
|
|
696
|
+
return search_decisions
|
|
697
|
+
|
|
698
|
+
# =========================================================================
|
|
699
|
+
# Read Operations
|
|
700
|
+
# =========================================================================
|
|
701
|
+
|
|
702
|
+
def search(
|
|
703
|
+
self,
|
|
704
|
+
query: Optional[str] = None,
|
|
705
|
+
agent_id: Optional[str] = None,
|
|
706
|
+
session_id: Optional[str] = None,
|
|
707
|
+
decision_type: Optional[str] = None,
|
|
708
|
+
days: Optional[int] = None,
|
|
709
|
+
limit: int = 10,
|
|
710
|
+
) -> List[DecisionLog]:
|
|
711
|
+
"""Search decisions with filters.
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
query: Text to search for.
|
|
715
|
+
agent_id: Filter by agent.
|
|
716
|
+
session_id: Filter by session.
|
|
717
|
+
decision_type: Filter by type.
|
|
718
|
+
days: Only last N days.
|
|
719
|
+
limit: Maximum results.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
List of matching decisions.
|
|
723
|
+
"""
|
|
724
|
+
if not self.db:
|
|
725
|
+
return []
|
|
726
|
+
|
|
727
|
+
# Ensure sync db for sync method
|
|
728
|
+
if not isinstance(self.db, BaseDb):
|
|
729
|
+
return []
|
|
730
|
+
|
|
731
|
+
try:
|
|
732
|
+
# Get all matching records
|
|
733
|
+
results = self.db.get_learnings(
|
|
734
|
+
learning_type=self.learning_type,
|
|
735
|
+
agent_id=agent_id,
|
|
736
|
+
limit=limit * 3, # Over-fetch for filtering
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
if not results:
|
|
740
|
+
return []
|
|
741
|
+
|
|
742
|
+
decisions = []
|
|
743
|
+
cutoff_date = None
|
|
744
|
+
if days:
|
|
745
|
+
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
|
746
|
+
|
|
747
|
+
for record in results:
|
|
748
|
+
content = record.get("content") if isinstance(record, dict) else None
|
|
749
|
+
if not content:
|
|
750
|
+
continue
|
|
751
|
+
|
|
752
|
+
decision = from_dict_safe(DecisionLog, content)
|
|
753
|
+
if not decision:
|
|
754
|
+
continue
|
|
755
|
+
|
|
756
|
+
# Apply filters
|
|
757
|
+
if decision_type and decision.decision_type != decision_type:
|
|
758
|
+
continue
|
|
759
|
+
|
|
760
|
+
if cutoff_date and decision.created_at:
|
|
761
|
+
try:
|
|
762
|
+
created = datetime.fromisoformat(decision.created_at.replace("Z", "+00:00"))
|
|
763
|
+
if created < cutoff_date:
|
|
764
|
+
continue
|
|
765
|
+
except (ValueError, AttributeError):
|
|
766
|
+
pass
|
|
767
|
+
|
|
768
|
+
if query:
|
|
769
|
+
query_lower = query.lower()
|
|
770
|
+
text = decision.to_text().lower()
|
|
771
|
+
if query_lower not in text:
|
|
772
|
+
continue
|
|
773
|
+
|
|
774
|
+
decisions.append(decision)
|
|
775
|
+
|
|
776
|
+
if len(decisions) >= limit:
|
|
777
|
+
break
|
|
778
|
+
|
|
779
|
+
return decisions
|
|
780
|
+
|
|
781
|
+
except Exception as e:
|
|
782
|
+
log_debug(f"DecisionLogStore.search failed: {e}")
|
|
783
|
+
return []
|
|
784
|
+
|
|
785
|
+
async def asearch(
|
|
786
|
+
self,
|
|
787
|
+
query: Optional[str] = None,
|
|
788
|
+
agent_id: Optional[str] = None,
|
|
789
|
+
session_id: Optional[str] = None,
|
|
790
|
+
decision_type: Optional[str] = None,
|
|
791
|
+
days: Optional[int] = None,
|
|
792
|
+
limit: int = 10,
|
|
793
|
+
) -> List[DecisionLog]:
|
|
794
|
+
"""Async version of search."""
|
|
795
|
+
if not self.db:
|
|
796
|
+
return []
|
|
797
|
+
|
|
798
|
+
try:
|
|
799
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
800
|
+
results = await self.db.get_learnings(
|
|
801
|
+
learning_type=self.learning_type,
|
|
802
|
+
agent_id=agent_id,
|
|
803
|
+
limit=limit * 3,
|
|
804
|
+
)
|
|
805
|
+
else:
|
|
806
|
+
results = self.db.get_learnings(
|
|
807
|
+
learning_type=self.learning_type,
|
|
808
|
+
agent_id=agent_id,
|
|
809
|
+
limit=limit * 3,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
if not results:
|
|
813
|
+
return []
|
|
814
|
+
|
|
815
|
+
decisions = []
|
|
816
|
+
cutoff_date = None
|
|
817
|
+
if days:
|
|
818
|
+
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
|
819
|
+
|
|
820
|
+
for record in results:
|
|
821
|
+
content = record.get("content") if isinstance(record, dict) else None
|
|
822
|
+
if not content:
|
|
823
|
+
continue
|
|
824
|
+
|
|
825
|
+
decision = from_dict_safe(DecisionLog, content)
|
|
826
|
+
if not decision:
|
|
827
|
+
continue
|
|
828
|
+
|
|
829
|
+
if decision_type and decision.decision_type != decision_type:
|
|
830
|
+
continue
|
|
831
|
+
|
|
832
|
+
if cutoff_date and decision.created_at:
|
|
833
|
+
try:
|
|
834
|
+
created = datetime.fromisoformat(decision.created_at.replace("Z", "+00:00"))
|
|
835
|
+
if created < cutoff_date:
|
|
836
|
+
continue
|
|
837
|
+
except (ValueError, AttributeError):
|
|
838
|
+
pass
|
|
839
|
+
|
|
840
|
+
if query:
|
|
841
|
+
query_lower = query.lower()
|
|
842
|
+
text = decision.to_text().lower()
|
|
843
|
+
if query_lower not in text:
|
|
844
|
+
continue
|
|
845
|
+
|
|
846
|
+
decisions.append(decision)
|
|
847
|
+
|
|
848
|
+
if len(decisions) >= limit:
|
|
849
|
+
break
|
|
850
|
+
|
|
851
|
+
return decisions
|
|
852
|
+
|
|
853
|
+
except Exception as e:
|
|
854
|
+
log_debug(f"DecisionLogStore.asearch failed: {e}")
|
|
855
|
+
return []
|
|
856
|
+
|
|
857
|
+
def get(self, decision_id: str) -> Optional[DecisionLog]:
|
|
858
|
+
"""Get a specific decision by ID."""
|
|
859
|
+
if not self.db:
|
|
860
|
+
return None
|
|
861
|
+
|
|
862
|
+
# Ensure sync db for sync method
|
|
863
|
+
if not isinstance(self.db, BaseDb):
|
|
864
|
+
return None
|
|
865
|
+
|
|
866
|
+
try:
|
|
867
|
+
# Get learnings and filter by decision_id in content
|
|
868
|
+
results = self.db.get_learnings(
|
|
869
|
+
learning_type=self.learning_type,
|
|
870
|
+
limit=100,
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
if not results:
|
|
874
|
+
return None
|
|
875
|
+
|
|
876
|
+
for record in results:
|
|
877
|
+
content = record.get("content") if isinstance(record, dict) else None
|
|
878
|
+
if content and content.get("id") == decision_id:
|
|
879
|
+
return from_dict_safe(DecisionLog, content)
|
|
880
|
+
|
|
881
|
+
return None
|
|
882
|
+
|
|
883
|
+
except Exception as e:
|
|
884
|
+
log_debug(f"DecisionLogStore.get failed: {e}")
|
|
885
|
+
return None
|
|
886
|
+
|
|
887
|
+
async def aget(self, decision_id: str) -> Optional[DecisionLog]:
|
|
888
|
+
"""Async version of get."""
|
|
889
|
+
if not self.db:
|
|
890
|
+
return None
|
|
891
|
+
|
|
892
|
+
try:
|
|
893
|
+
# Get learnings and filter by decision_id in content
|
|
894
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
895
|
+
results = await self.db.get_learnings(
|
|
896
|
+
learning_type=self.learning_type,
|
|
897
|
+
limit=100,
|
|
898
|
+
)
|
|
899
|
+
else:
|
|
900
|
+
results = self.db.get_learnings(
|
|
901
|
+
learning_type=self.learning_type,
|
|
902
|
+
limit=100,
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
if not results:
|
|
906
|
+
return None
|
|
907
|
+
|
|
908
|
+
for record in results:
|
|
909
|
+
content = record.get("content") if isinstance(record, dict) else None
|
|
910
|
+
if content and content.get("id") == decision_id:
|
|
911
|
+
return from_dict_safe(DecisionLog, content)
|
|
912
|
+
|
|
913
|
+
return None
|
|
914
|
+
|
|
915
|
+
except Exception as e:
|
|
916
|
+
log_debug(f"DecisionLogStore.aget failed: {e}")
|
|
917
|
+
return None
|
|
918
|
+
|
|
919
|
+
# =========================================================================
|
|
920
|
+
# Write Operations
|
|
921
|
+
# =========================================================================
|
|
922
|
+
|
|
923
|
+
def save(self, decision: DecisionLog) -> None:
|
|
924
|
+
"""Save a decision to the database."""
|
|
925
|
+
if not self.db or not decision:
|
|
926
|
+
return
|
|
927
|
+
|
|
928
|
+
try:
|
|
929
|
+
content = to_dict_safe(decision)
|
|
930
|
+
if not content:
|
|
931
|
+
return
|
|
932
|
+
|
|
933
|
+
self.db.upsert_learning(
|
|
934
|
+
id=decision.id,
|
|
935
|
+
learning_type=self.learning_type,
|
|
936
|
+
agent_id=decision.agent_id,
|
|
937
|
+
session_id=decision.session_id,
|
|
938
|
+
user_id=decision.user_id,
|
|
939
|
+
team_id=decision.team_id,
|
|
940
|
+
content=content,
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
self.decisions_updated = True
|
|
944
|
+
log_debug(f"DecisionLogStore.save: saved decision {decision.id}")
|
|
945
|
+
|
|
946
|
+
except Exception as e:
|
|
947
|
+
log_debug(f"DecisionLogStore.save failed: {e}")
|
|
948
|
+
|
|
949
|
+
async def asave(self, decision: DecisionLog) -> None:
|
|
950
|
+
"""Async version of save."""
|
|
951
|
+
if not self.db or not decision:
|
|
952
|
+
return
|
|
953
|
+
|
|
954
|
+
try:
|
|
955
|
+
content = to_dict_safe(decision)
|
|
956
|
+
if not content:
|
|
957
|
+
return
|
|
958
|
+
|
|
959
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
960
|
+
await self.db.upsert_learning(
|
|
961
|
+
id=decision.id,
|
|
962
|
+
learning_type=self.learning_type,
|
|
963
|
+
agent_id=decision.agent_id,
|
|
964
|
+
session_id=decision.session_id,
|
|
965
|
+
user_id=decision.user_id,
|
|
966
|
+
team_id=decision.team_id,
|
|
967
|
+
content=content,
|
|
968
|
+
)
|
|
969
|
+
else:
|
|
970
|
+
self.db.upsert_learning(
|
|
971
|
+
id=decision.id,
|
|
972
|
+
learning_type=self.learning_type,
|
|
973
|
+
agent_id=decision.agent_id,
|
|
974
|
+
session_id=decision.session_id,
|
|
975
|
+
user_id=decision.user_id,
|
|
976
|
+
team_id=decision.team_id,
|
|
977
|
+
content=content,
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
self.decisions_updated = True
|
|
981
|
+
log_debug(f"DecisionLogStore.asave: saved decision {decision.id}")
|
|
982
|
+
|
|
983
|
+
except Exception as e:
|
|
984
|
+
log_debug(f"DecisionLogStore.asave failed: {e}")
|
|
985
|
+
|
|
986
|
+
def update_outcome(
|
|
987
|
+
self,
|
|
988
|
+
decision_id: str,
|
|
989
|
+
outcome: str,
|
|
990
|
+
outcome_quality: Optional[str] = None,
|
|
991
|
+
) -> bool:
|
|
992
|
+
"""Update a decision with its outcome."""
|
|
993
|
+
decision = self.get(decision_id=decision_id)
|
|
994
|
+
if not decision:
|
|
995
|
+
return False
|
|
996
|
+
|
|
997
|
+
decision.outcome = outcome
|
|
998
|
+
decision.outcome_quality = outcome_quality
|
|
999
|
+
decision.updated_at = datetime.utcnow().isoformat()
|
|
1000
|
+
|
|
1001
|
+
self.save(decision=decision)
|
|
1002
|
+
return True
|
|
1003
|
+
|
|
1004
|
+
async def aupdate_outcome(
|
|
1005
|
+
self,
|
|
1006
|
+
decision_id: str,
|
|
1007
|
+
outcome: str,
|
|
1008
|
+
outcome_quality: Optional[str] = None,
|
|
1009
|
+
) -> bool:
|
|
1010
|
+
"""Async version of update_outcome."""
|
|
1011
|
+
decision = await self.aget(decision_id=decision_id)
|
|
1012
|
+
if not decision:
|
|
1013
|
+
return False
|
|
1014
|
+
|
|
1015
|
+
decision.outcome = outcome
|
|
1016
|
+
decision.outcome_quality = outcome_quality
|
|
1017
|
+
decision.updated_at = datetime.utcnow().isoformat()
|
|
1018
|
+
|
|
1019
|
+
await self.asave(decision=decision)
|
|
1020
|
+
return True
|
|
1021
|
+
|
|
1022
|
+
# =========================================================================
|
|
1023
|
+
# Extraction (ALWAYS mode)
|
|
1024
|
+
# =========================================================================
|
|
1025
|
+
|
|
1026
|
+
def _extract_decisions_from_messages(
|
|
1027
|
+
self,
|
|
1028
|
+
messages: List["Message"],
|
|
1029
|
+
agent_id: Optional[str] = None,
|
|
1030
|
+
session_id: Optional[str] = None,
|
|
1031
|
+
user_id: Optional[str] = None,
|
|
1032
|
+
team_id: Optional[str] = None,
|
|
1033
|
+
) -> None:
|
|
1034
|
+
"""Extract decisions from tool calls in messages."""
|
|
1035
|
+
for msg in messages:
|
|
1036
|
+
if not hasattr(msg, "tool_calls") or not msg.tool_calls:
|
|
1037
|
+
continue
|
|
1038
|
+
|
|
1039
|
+
for tool_call in msg.tool_calls:
|
|
1040
|
+
tool_name = getattr(tool_call, "name", None) or getattr(
|
|
1041
|
+
getattr(tool_call, "function", None), "name", None
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
if not tool_name:
|
|
1045
|
+
continue
|
|
1046
|
+
|
|
1047
|
+
decision_id = f"dec_{uuid.uuid4().hex[:8]}"
|
|
1048
|
+
decision = DecisionLog(
|
|
1049
|
+
id=decision_id,
|
|
1050
|
+
decision=f"Called tool: {tool_name}",
|
|
1051
|
+
decision_type="tool_selection",
|
|
1052
|
+
context="During conversation with user",
|
|
1053
|
+
session_id=session_id,
|
|
1054
|
+
user_id=user_id,
|
|
1055
|
+
agent_id=agent_id,
|
|
1056
|
+
team_id=team_id,
|
|
1057
|
+
created_at=datetime.utcnow().isoformat(),
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
self.save(decision=decision)
|
|
1061
|
+
|
|
1062
|
+
async def _aextract_decisions_from_messages(
|
|
1063
|
+
self,
|
|
1064
|
+
messages: List["Message"],
|
|
1065
|
+
agent_id: Optional[str] = None,
|
|
1066
|
+
session_id: Optional[str] = None,
|
|
1067
|
+
user_id: Optional[str] = None,
|
|
1068
|
+
team_id: Optional[str] = None,
|
|
1069
|
+
) -> None:
|
|
1070
|
+
"""Async version of _extract_decisions_from_messages."""
|
|
1071
|
+
for msg in messages:
|
|
1072
|
+
if not hasattr(msg, "tool_calls") or not msg.tool_calls:
|
|
1073
|
+
continue
|
|
1074
|
+
|
|
1075
|
+
for tool_call in msg.tool_calls:
|
|
1076
|
+
tool_name = getattr(tool_call, "name", None) or getattr(
|
|
1077
|
+
getattr(tool_call, "function", None), "name", None
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
if not tool_name:
|
|
1081
|
+
continue
|
|
1082
|
+
|
|
1083
|
+
decision_id = f"dec_{uuid.uuid4().hex[:8]}"
|
|
1084
|
+
decision = DecisionLog(
|
|
1085
|
+
id=decision_id,
|
|
1086
|
+
decision=f"Called tool: {tool_name}",
|
|
1087
|
+
decision_type="tool_selection",
|
|
1088
|
+
context="During conversation with user",
|
|
1089
|
+
session_id=session_id,
|
|
1090
|
+
user_id=user_id,
|
|
1091
|
+
agent_id=agent_id,
|
|
1092
|
+
team_id=team_id,
|
|
1093
|
+
created_at=datetime.utcnow().isoformat(),
|
|
1094
|
+
)
|
|
1095
|
+
|
|
1096
|
+
await self.asave(decision=decision)
|
|
1097
|
+
|
|
1098
|
+
# =========================================================================
|
|
1099
|
+
# Representation
|
|
1100
|
+
# =========================================================================
|
|
1101
|
+
|
|
1102
|
+
def __repr__(self) -> str:
|
|
1103
|
+
"""String representation for debugging."""
|
|
1104
|
+
has_db = self.db is not None
|
|
1105
|
+
has_model = self.model is not None
|
|
1106
|
+
return (
|
|
1107
|
+
f"DecisionLogStore("
|
|
1108
|
+
f"mode={self.config.mode.value}, "
|
|
1109
|
+
f"db={has_db}, "
|
|
1110
|
+
f"model={has_model}, "
|
|
1111
|
+
f"enable_agent_tools={self.config.enable_agent_tools})"
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
def print(
|
|
1115
|
+
self,
|
|
1116
|
+
agent_id: Optional[str] = None,
|
|
1117
|
+
session_id: Optional[str] = None,
|
|
1118
|
+
limit: int = 10,
|
|
1119
|
+
*,
|
|
1120
|
+
raw: bool = False,
|
|
1121
|
+
) -> None:
|
|
1122
|
+
"""Print formatted decision log.
|
|
1123
|
+
|
|
1124
|
+
Args:
|
|
1125
|
+
agent_id: Filter by agent.
|
|
1126
|
+
session_id: Filter by session.
|
|
1127
|
+
limit: Maximum decisions to show.
|
|
1128
|
+
raw: If True, print raw dict using pprint.
|
|
1129
|
+
"""
|
|
1130
|
+
from agno.learn.utils import print_panel
|
|
1131
|
+
|
|
1132
|
+
decisions = self.search(
|
|
1133
|
+
agent_id=agent_id,
|
|
1134
|
+
session_id=session_id,
|
|
1135
|
+
limit=limit,
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
lines = []
|
|
1139
|
+
for d in decisions:
|
|
1140
|
+
lines.append(f"[{d.id}] {d.decision}")
|
|
1141
|
+
if d.reasoning:
|
|
1142
|
+
lines.append(f" Reasoning: {d.reasoning}")
|
|
1143
|
+
if d.outcome:
|
|
1144
|
+
lines.append(f" Outcome: {d.outcome}")
|
|
1145
|
+
lines.append("")
|
|
1146
|
+
|
|
1147
|
+
subtitle = agent_id or session_id or "all"
|
|
1148
|
+
|
|
1149
|
+
print_panel(
|
|
1150
|
+
title="Decision Log",
|
|
1151
|
+
subtitle=subtitle,
|
|
1152
|
+
lines=lines,
|
|
1153
|
+
empty_message="No decisions logged",
|
|
1154
|
+
raw_data=decisions,
|
|
1155
|
+
raw=raw,
|
|
1156
|
+
)
|