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,1217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session Context Store
|
|
3
|
+
=====================
|
|
4
|
+
Storage backend for Session Context learning type.
|
|
5
|
+
|
|
6
|
+
Stores the current state of a session: what's happened, what's the goal, what's the plan.
|
|
7
|
+
|
|
8
|
+
Key Features:
|
|
9
|
+
- Summary extraction from conversations
|
|
10
|
+
- Optional planning mode (goal, plan, progress tracking)
|
|
11
|
+
- Session-scoped storage (each session_id has one context)
|
|
12
|
+
- Builds on previous context (doesn't start from scratch each time)
|
|
13
|
+
- No agent tool (system-managed only)
|
|
14
|
+
|
|
15
|
+
Scope:
|
|
16
|
+
- Context is retrieved by session_id only
|
|
17
|
+
- agent_id/team_id stored in DB columns for audit trail
|
|
18
|
+
|
|
19
|
+
Key Behavior:
|
|
20
|
+
- Extraction receives the previous context and updates it
|
|
21
|
+
- This ensures continuity even when message history is truncated
|
|
22
|
+
- Previous context + new messages → Updated context
|
|
23
|
+
|
|
24
|
+
Supported Modes:
|
|
25
|
+
- ALWAYS only. SessionContextStore does not support AGENTIC, PROPOSE, or HITL modes.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from copy import deepcopy
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from os import getenv
|
|
31
|
+
from textwrap import dedent
|
|
32
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
33
|
+
|
|
34
|
+
from agno.learn.config import LearningMode, SessionContextConfig
|
|
35
|
+
from agno.learn.schemas import SessionContext
|
|
36
|
+
from agno.learn.stores.protocol import LearningStore
|
|
37
|
+
from agno.learn.utils import from_dict_safe, to_dict_safe
|
|
38
|
+
from agno.utils.log import (
|
|
39
|
+
log_debug,
|
|
40
|
+
log_warning,
|
|
41
|
+
set_log_level_to_debug,
|
|
42
|
+
set_log_level_to_info,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from agno.db.base import AsyncBaseDb, BaseDb
|
|
47
|
+
from agno.models.message import Message
|
|
48
|
+
from agno.tools.function import Function
|
|
49
|
+
except ImportError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class SessionContextStore(LearningStore):
|
|
55
|
+
"""Storage backend for Session Context learning type.
|
|
56
|
+
|
|
57
|
+
Context is retrieved by session_id only — all agents sharing the same DB
|
|
58
|
+
will see the same context for a given session. agent_id and team_id are
|
|
59
|
+
stored in DB columns for audit purposes.
|
|
60
|
+
|
|
61
|
+
Key difference from UserProfileStore:
|
|
62
|
+
- UserProfile: accumulates memories over time
|
|
63
|
+
- SessionContext: snapshot of current session state (updated on each extraction)
|
|
64
|
+
|
|
65
|
+
Key behavior:
|
|
66
|
+
- Extraction builds on previous context rather than starting fresh
|
|
67
|
+
- This ensures continuity even when message history is truncated
|
|
68
|
+
- Previous summary, goal, plan, progress are preserved and updated
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
config: SessionContextConfig with all settings including db and model.
|
|
72
|
+
debug_mode: Enable debug logging.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
config: SessionContextConfig = field(default_factory=SessionContextConfig)
|
|
76
|
+
debug_mode: bool = False
|
|
77
|
+
|
|
78
|
+
# State tracking (internal)
|
|
79
|
+
context_updated: bool = field(default=False, init=False)
|
|
80
|
+
_schema: Any = field(default=None, init=False)
|
|
81
|
+
|
|
82
|
+
def __post_init__(self):
|
|
83
|
+
self._schema = self.config.schema or SessionContext
|
|
84
|
+
|
|
85
|
+
if self.config.mode != LearningMode.ALWAYS:
|
|
86
|
+
log_warning(
|
|
87
|
+
f"SessionContextStore only supports ALWAYS mode, got {self.config.mode}. Ignoring mode setting."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# =========================================================================
|
|
91
|
+
# LearningStore Protocol Implementation
|
|
92
|
+
# =========================================================================
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def learning_type(self) -> str:
|
|
96
|
+
"""Unique identifier for this learning type."""
|
|
97
|
+
return "session_context"
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def schema(self) -> Any:
|
|
101
|
+
"""Schema class used for context."""
|
|
102
|
+
return self._schema
|
|
103
|
+
|
|
104
|
+
def recall(self, session_id: str, **kwargs) -> Optional[Any]:
|
|
105
|
+
"""Retrieve session context from storage.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
session_id: The session to retrieve context for (required).
|
|
109
|
+
**kwargs: Additional context (ignored).
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Session context, or None if not found.
|
|
113
|
+
"""
|
|
114
|
+
if not session_id:
|
|
115
|
+
return None
|
|
116
|
+
return self.get(session_id=session_id)
|
|
117
|
+
|
|
118
|
+
async def arecall(self, session_id: str, **kwargs) -> Optional[Any]:
|
|
119
|
+
"""Async version of recall."""
|
|
120
|
+
if not session_id:
|
|
121
|
+
return None
|
|
122
|
+
return await self.aget(session_id=session_id)
|
|
123
|
+
|
|
124
|
+
def process(
|
|
125
|
+
self,
|
|
126
|
+
messages: List[Any],
|
|
127
|
+
session_id: str,
|
|
128
|
+
user_id: Optional[str] = None,
|
|
129
|
+
agent_id: Optional[str] = None,
|
|
130
|
+
team_id: Optional[str] = None,
|
|
131
|
+
**kwargs,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Extract session context from messages.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
messages: Conversation messages to analyze.
|
|
137
|
+
session_id: The session to update context for (required).
|
|
138
|
+
user_id: User context (stored for audit).
|
|
139
|
+
agent_id: Agent context (stored for audit).
|
|
140
|
+
team_id: Team context (stored for audit).
|
|
141
|
+
**kwargs: Additional context (ignored).
|
|
142
|
+
"""
|
|
143
|
+
# process only supported in ALWAYS mode
|
|
144
|
+
# for programmatic extraction, use extract_and_save directly
|
|
145
|
+
if self.config.mode != LearningMode.ALWAYS:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
if not session_id or not messages:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
self.extract_and_save(
|
|
152
|
+
messages=messages,
|
|
153
|
+
session_id=session_id,
|
|
154
|
+
user_id=user_id,
|
|
155
|
+
agent_id=agent_id,
|
|
156
|
+
team_id=team_id,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def aprocess(
|
|
160
|
+
self,
|
|
161
|
+
messages: List[Any],
|
|
162
|
+
session_id: str,
|
|
163
|
+
user_id: Optional[str] = None,
|
|
164
|
+
agent_id: Optional[str] = None,
|
|
165
|
+
team_id: Optional[str] = None,
|
|
166
|
+
**kwargs,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Async version of process."""
|
|
169
|
+
if self.config.mode != LearningMode.ALWAYS:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if not session_id or not messages:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
await self.aextract_and_save(
|
|
176
|
+
messages=messages,
|
|
177
|
+
session_id=session_id,
|
|
178
|
+
user_id=user_id,
|
|
179
|
+
agent_id=agent_id,
|
|
180
|
+
team_id=team_id,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def build_context(self, data: Any) -> str:
|
|
184
|
+
"""Build context for the agent.
|
|
185
|
+
|
|
186
|
+
Formats session context for injection into the agent's system prompt.
|
|
187
|
+
Session context provides continuity within a single conversation,
|
|
188
|
+
especially useful when message history gets truncated.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
data: Session context data from recall().
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Context string to inject into the agent's system prompt.
|
|
195
|
+
"""
|
|
196
|
+
if not data:
|
|
197
|
+
return ""
|
|
198
|
+
|
|
199
|
+
context_text = None
|
|
200
|
+
if hasattr(data, "get_context_text"):
|
|
201
|
+
context_text = data.get_context_text()
|
|
202
|
+
elif hasattr(data, "summary") and data.summary:
|
|
203
|
+
context_text = self._format_context(context=data)
|
|
204
|
+
|
|
205
|
+
if not context_text:
|
|
206
|
+
return ""
|
|
207
|
+
|
|
208
|
+
return dedent(f"""\
|
|
209
|
+
<session_context>
|
|
210
|
+
This is a continuation of an ongoing session. Here's where things stand:
|
|
211
|
+
|
|
212
|
+
{context_text}
|
|
213
|
+
|
|
214
|
+
<session_context_guidelines>
|
|
215
|
+
Use this context to maintain continuity:
|
|
216
|
+
- Reference earlier decisions and conclusions naturally
|
|
217
|
+
- Don't re-ask questions that have already been answered
|
|
218
|
+
- Build on established understanding rather than starting fresh
|
|
219
|
+
- If the user references something from "earlier," this context has the details
|
|
220
|
+
|
|
221
|
+
Current messages take precedence if there's any conflict with this summary.
|
|
222
|
+
</session_context_guidelines>
|
|
223
|
+
</session_context>\
|
|
224
|
+
""")
|
|
225
|
+
|
|
226
|
+
def get_tools(self, **kwargs) -> List[Callable]:
|
|
227
|
+
"""Session context has no agent tools (system-managed only)."""
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
async def aget_tools(self, **kwargs) -> List[Callable]:
|
|
231
|
+
"""Async version of get_tools."""
|
|
232
|
+
return []
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def was_updated(self) -> bool:
|
|
236
|
+
"""Check if context was updated in last operation."""
|
|
237
|
+
return self.context_updated
|
|
238
|
+
|
|
239
|
+
# =========================================================================
|
|
240
|
+
# Properties
|
|
241
|
+
# =========================================================================
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def db(self) -> Optional[Union["BaseDb", "AsyncBaseDb"]]:
|
|
245
|
+
"""Database backend."""
|
|
246
|
+
return self.config.db
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def model(self):
|
|
250
|
+
"""Model for extraction."""
|
|
251
|
+
return self.config.model
|
|
252
|
+
|
|
253
|
+
# =========================================================================
|
|
254
|
+
# Debug/Logging
|
|
255
|
+
# =========================================================================
|
|
256
|
+
|
|
257
|
+
def set_log_level(self):
|
|
258
|
+
"""Set log level based on debug_mode or environment variable."""
|
|
259
|
+
if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true":
|
|
260
|
+
self.debug_mode = True
|
|
261
|
+
set_log_level_to_debug()
|
|
262
|
+
else:
|
|
263
|
+
set_log_level_to_info()
|
|
264
|
+
|
|
265
|
+
# =========================================================================
|
|
266
|
+
# Read Operations
|
|
267
|
+
# =========================================================================
|
|
268
|
+
|
|
269
|
+
def get(self, session_id: str) -> Optional[Any]:
|
|
270
|
+
"""Retrieve session context by session_id.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
session_id: The unique session identifier.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Session context as schema instance, or None if not found.
|
|
277
|
+
"""
|
|
278
|
+
if not self.db:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
result = self.db.get_learning(
|
|
283
|
+
learning_type=self.learning_type,
|
|
284
|
+
session_id=session_id,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if result and result.get("content"): # type: ignore[union-attr]
|
|
288
|
+
return from_dict_safe(self.schema, result["content"]) # type: ignore[index]
|
|
289
|
+
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
log_debug(f"SessionContextStore.get failed for session_id={session_id}: {e}")
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
async def aget(self, session_id: str) -> Optional[Any]:
|
|
297
|
+
"""Async version of get."""
|
|
298
|
+
if not self.db:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
303
|
+
result = await self.db.get_learning(
|
|
304
|
+
learning_type=self.learning_type,
|
|
305
|
+
session_id=session_id,
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
result = self.db.get_learning(
|
|
309
|
+
learning_type=self.learning_type,
|
|
310
|
+
session_id=session_id,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if result and result.get("content"):
|
|
314
|
+
return from_dict_safe(self.schema, result["content"])
|
|
315
|
+
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
except Exception as e:
|
|
319
|
+
log_debug(f"SessionContextStore.aget failed for session_id={session_id}: {e}")
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
# =========================================================================
|
|
323
|
+
# Write Operations
|
|
324
|
+
# =========================================================================
|
|
325
|
+
|
|
326
|
+
def save(
|
|
327
|
+
self,
|
|
328
|
+
session_id: str,
|
|
329
|
+
context: Any,
|
|
330
|
+
user_id: Optional[str] = None,
|
|
331
|
+
agent_id: Optional[str] = None,
|
|
332
|
+
team_id: Optional[str] = None,
|
|
333
|
+
) -> None:
|
|
334
|
+
"""Save or replace session context.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
session_id: The unique session identifier.
|
|
338
|
+
context: The context data to save.
|
|
339
|
+
user_id: User context (stored in DB column for audit).
|
|
340
|
+
agent_id: Agent context (stored in DB column for audit).
|
|
341
|
+
team_id: Team context (stored in DB column for audit).
|
|
342
|
+
"""
|
|
343
|
+
if not self.db or not context:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
content = to_dict_safe(context)
|
|
348
|
+
if not content:
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
self.db.upsert_learning(
|
|
352
|
+
id=self._build_context_id(session_id=session_id),
|
|
353
|
+
learning_type=self.learning_type,
|
|
354
|
+
session_id=session_id,
|
|
355
|
+
user_id=user_id,
|
|
356
|
+
agent_id=agent_id,
|
|
357
|
+
team_id=team_id,
|
|
358
|
+
content=content,
|
|
359
|
+
)
|
|
360
|
+
log_debug(f"SessionContextStore.save: saved context for session_id={session_id}")
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
log_debug(f"SessionContextStore.save failed for session_id={session_id}: {e}")
|
|
364
|
+
|
|
365
|
+
async def asave(
|
|
366
|
+
self,
|
|
367
|
+
session_id: str,
|
|
368
|
+
context: Any,
|
|
369
|
+
user_id: Optional[str] = None,
|
|
370
|
+
agent_id: Optional[str] = None,
|
|
371
|
+
team_id: Optional[str] = None,
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Async version of save."""
|
|
374
|
+
if not self.db or not context:
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
content = to_dict_safe(context)
|
|
379
|
+
if not content:
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
383
|
+
await self.db.upsert_learning(
|
|
384
|
+
id=self._build_context_id(session_id=session_id),
|
|
385
|
+
learning_type=self.learning_type,
|
|
386
|
+
session_id=session_id,
|
|
387
|
+
user_id=user_id,
|
|
388
|
+
agent_id=agent_id,
|
|
389
|
+
team_id=team_id,
|
|
390
|
+
content=content,
|
|
391
|
+
)
|
|
392
|
+
else:
|
|
393
|
+
self.db.upsert_learning(
|
|
394
|
+
id=self._build_context_id(session_id=session_id),
|
|
395
|
+
learning_type=self.learning_type,
|
|
396
|
+
session_id=session_id,
|
|
397
|
+
user_id=user_id,
|
|
398
|
+
agent_id=agent_id,
|
|
399
|
+
team_id=team_id,
|
|
400
|
+
content=content,
|
|
401
|
+
)
|
|
402
|
+
log_debug(f"SessionContextStore.asave: saved context for session_id={session_id}")
|
|
403
|
+
|
|
404
|
+
except Exception as e:
|
|
405
|
+
log_debug(f"SessionContextStore.asave failed for session_id={session_id}: {e}")
|
|
406
|
+
|
|
407
|
+
# =========================================================================
|
|
408
|
+
# Delete Operations
|
|
409
|
+
# =========================================================================
|
|
410
|
+
|
|
411
|
+
def delete(self, session_id: str) -> bool:
|
|
412
|
+
"""Delete session context.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
session_id: The unique session identifier.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
True if deleted, False otherwise.
|
|
419
|
+
"""
|
|
420
|
+
if not self.db:
|
|
421
|
+
return False
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
context_id = self._build_context_id(session_id=session_id)
|
|
425
|
+
return self.db.delete_learning(id=context_id) # type: ignore[return-value]
|
|
426
|
+
except Exception as e:
|
|
427
|
+
log_debug(f"SessionContextStore.delete failed for session_id={session_id}: {e}")
|
|
428
|
+
return False
|
|
429
|
+
|
|
430
|
+
async def adelete(self, session_id: str) -> bool:
|
|
431
|
+
"""Async version of delete."""
|
|
432
|
+
if not self.db:
|
|
433
|
+
return False
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
context_id = self._build_context_id(session_id=session_id)
|
|
437
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
438
|
+
return await self.db.delete_learning(id=context_id)
|
|
439
|
+
else:
|
|
440
|
+
return self.db.delete_learning(id=context_id)
|
|
441
|
+
except Exception as e:
|
|
442
|
+
log_debug(f"SessionContextStore.adelete failed for session_id={session_id}: {e}")
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
def clear(
|
|
446
|
+
self,
|
|
447
|
+
session_id: str,
|
|
448
|
+
agent_id: Optional[str] = None,
|
|
449
|
+
team_id: Optional[str] = None,
|
|
450
|
+
) -> None:
|
|
451
|
+
"""Clear session context (reset to empty).
|
|
452
|
+
|
|
453
|
+
Args:
|
|
454
|
+
session_id: The unique session identifier.
|
|
455
|
+
agent_id: Agent context (stored for audit).
|
|
456
|
+
team_id: Team context (stored for audit).
|
|
457
|
+
"""
|
|
458
|
+
if not self.db:
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
empty_context = self.schema(session_id=session_id)
|
|
463
|
+
self.save(session_id=session_id, context=empty_context, agent_id=agent_id, team_id=team_id)
|
|
464
|
+
log_debug(f"SessionContextStore.clear: cleared context for session_id={session_id}")
|
|
465
|
+
except Exception as e:
|
|
466
|
+
log_debug(f"SessionContextStore.clear failed for session_id={session_id}: {e}")
|
|
467
|
+
|
|
468
|
+
async def aclear(
|
|
469
|
+
self,
|
|
470
|
+
session_id: str,
|
|
471
|
+
agent_id: Optional[str] = None,
|
|
472
|
+
team_id: Optional[str] = None,
|
|
473
|
+
) -> None:
|
|
474
|
+
"""Async version of clear."""
|
|
475
|
+
if not self.db:
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
try:
|
|
479
|
+
empty_context = self.schema(session_id=session_id)
|
|
480
|
+
await self.asave(session_id=session_id, context=empty_context, agent_id=agent_id, team_id=team_id)
|
|
481
|
+
log_debug(f"SessionContextStore.aclear: cleared context for session_id={session_id}")
|
|
482
|
+
except Exception as e:
|
|
483
|
+
log_debug(f"SessionContextStore.aclear failed for session_id={session_id}: {e}")
|
|
484
|
+
|
|
485
|
+
# =========================================================================
|
|
486
|
+
# Extraction Operations
|
|
487
|
+
# =========================================================================
|
|
488
|
+
|
|
489
|
+
def extract_and_save(
|
|
490
|
+
self,
|
|
491
|
+
messages: List["Message"],
|
|
492
|
+
session_id: str,
|
|
493
|
+
user_id: Optional[str] = None,
|
|
494
|
+
agent_id: Optional[str] = None,
|
|
495
|
+
team_id: Optional[str] = None,
|
|
496
|
+
) -> str:
|
|
497
|
+
"""Extract session context from messages and save.
|
|
498
|
+
|
|
499
|
+
Builds on previous context rather than starting from scratch.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
messages: Conversation messages to analyze.
|
|
503
|
+
session_id: The unique session identifier.
|
|
504
|
+
user_id: User context (stored for audit).
|
|
505
|
+
agent_id: Agent context (stored for audit).
|
|
506
|
+
team_id: Team context (stored for audit).
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
Response from model.
|
|
510
|
+
"""
|
|
511
|
+
if self.model is None:
|
|
512
|
+
log_warning("SessionContextStore.extract_and_save: no model provided")
|
|
513
|
+
return "No model provided for session context extraction"
|
|
514
|
+
|
|
515
|
+
if not self.db:
|
|
516
|
+
log_warning("SessionContextStore.extract_and_save: no database provided")
|
|
517
|
+
return "No DB provided for session context store"
|
|
518
|
+
|
|
519
|
+
log_debug("SessionContextStore: Extracting session context", center=True)
|
|
520
|
+
|
|
521
|
+
self.context_updated = False
|
|
522
|
+
|
|
523
|
+
# Get existing context to build upon
|
|
524
|
+
existing_context = self.get(session_id=session_id)
|
|
525
|
+
|
|
526
|
+
conversation_text = self._messages_to_text(messages=messages)
|
|
527
|
+
|
|
528
|
+
tools = self._get_extraction_tools(
|
|
529
|
+
session_id=session_id,
|
|
530
|
+
user_id=user_id,
|
|
531
|
+
agent_id=agent_id,
|
|
532
|
+
team_id=team_id,
|
|
533
|
+
existing_context=existing_context,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
functions = self._build_functions_for_model(tools=tools)
|
|
537
|
+
|
|
538
|
+
system_message = self._get_system_message(
|
|
539
|
+
conversation_text=conversation_text,
|
|
540
|
+
existing_context=existing_context,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
messages_for_model = [system_message]
|
|
544
|
+
|
|
545
|
+
model_copy = deepcopy(self.model)
|
|
546
|
+
response = model_copy.response(
|
|
547
|
+
messages=messages_for_model,
|
|
548
|
+
tools=functions,
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
if response.tool_executions:
|
|
552
|
+
self.context_updated = True
|
|
553
|
+
|
|
554
|
+
log_debug("SessionContextStore: Extraction complete", center=True)
|
|
555
|
+
|
|
556
|
+
return response.content or ("Context updated" if self.context_updated else "No updates needed")
|
|
557
|
+
|
|
558
|
+
async def aextract_and_save(
|
|
559
|
+
self,
|
|
560
|
+
messages: List["Message"],
|
|
561
|
+
session_id: str,
|
|
562
|
+
user_id: Optional[str] = None,
|
|
563
|
+
agent_id: Optional[str] = None,
|
|
564
|
+
team_id: Optional[str] = None,
|
|
565
|
+
) -> str:
|
|
566
|
+
"""Async version of extract_and_save."""
|
|
567
|
+
if self.model is None:
|
|
568
|
+
log_warning("SessionContextStore.aextract_and_save: no model provided")
|
|
569
|
+
return "No model provided for session context extraction"
|
|
570
|
+
|
|
571
|
+
if not self.db:
|
|
572
|
+
log_warning("SessionContextStore.aextract_and_save: no database provided")
|
|
573
|
+
return "No DB provided for session context store"
|
|
574
|
+
|
|
575
|
+
log_debug("SessionContextStore: Extracting session context (async)", center=True)
|
|
576
|
+
|
|
577
|
+
self.context_updated = False
|
|
578
|
+
|
|
579
|
+
# Get existing context to build upon
|
|
580
|
+
existing_context = await self.aget(session_id=session_id)
|
|
581
|
+
|
|
582
|
+
conversation_text = self._messages_to_text(messages=messages)
|
|
583
|
+
|
|
584
|
+
tools = await self._aget_extraction_tools(
|
|
585
|
+
session_id=session_id,
|
|
586
|
+
user_id=user_id,
|
|
587
|
+
agent_id=agent_id,
|
|
588
|
+
team_id=team_id,
|
|
589
|
+
existing_context=existing_context,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
functions = self._build_functions_for_model(tools=tools)
|
|
593
|
+
|
|
594
|
+
system_message = self._get_system_message(
|
|
595
|
+
conversation_text=conversation_text,
|
|
596
|
+
existing_context=existing_context,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
messages_for_model = [system_message]
|
|
600
|
+
|
|
601
|
+
model_copy = deepcopy(self.model)
|
|
602
|
+
response = await model_copy.aresponse(
|
|
603
|
+
messages=messages_for_model,
|
|
604
|
+
tools=functions,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if response.tool_executions:
|
|
608
|
+
self.context_updated = True
|
|
609
|
+
|
|
610
|
+
log_debug("SessionContextStore: Extraction complete", center=True)
|
|
611
|
+
|
|
612
|
+
return response.content or ("Context updated" if self.context_updated else "No updates needed")
|
|
613
|
+
|
|
614
|
+
# =========================================================================
|
|
615
|
+
# Private Helpers
|
|
616
|
+
# =========================================================================
|
|
617
|
+
|
|
618
|
+
def _build_context_id(self, session_id: str) -> str:
|
|
619
|
+
"""Build a unique context ID."""
|
|
620
|
+
return f"session_context_{session_id}"
|
|
621
|
+
|
|
622
|
+
def _format_context(self, context: Any) -> str:
|
|
623
|
+
"""Format context data for display in agent prompt."""
|
|
624
|
+
parts = []
|
|
625
|
+
|
|
626
|
+
if hasattr(context, "summary") and context.summary:
|
|
627
|
+
parts.append(f"**Summary:** {context.summary}")
|
|
628
|
+
|
|
629
|
+
if hasattr(context, "goal") and context.goal:
|
|
630
|
+
parts.append(f"**Current Goal:** {context.goal}")
|
|
631
|
+
|
|
632
|
+
if hasattr(context, "plan") and context.plan:
|
|
633
|
+
plan_items = "\n - ".join(context.plan)
|
|
634
|
+
parts.append(f"**Plan:**\n - {plan_items}")
|
|
635
|
+
|
|
636
|
+
if hasattr(context, "progress") and context.progress:
|
|
637
|
+
progress_items = "\n - ".join(f"✓ {item}" for item in context.progress)
|
|
638
|
+
parts.append(f"**Completed:**\n - {progress_items}")
|
|
639
|
+
|
|
640
|
+
return "\n\n".join(parts)
|
|
641
|
+
|
|
642
|
+
def _messages_to_text(self, messages: List["Message"]) -> str:
|
|
643
|
+
"""Convert messages to text for extraction."""
|
|
644
|
+
parts = []
|
|
645
|
+
for msg in messages:
|
|
646
|
+
if msg.role == "user":
|
|
647
|
+
content = msg.get_content_string() if hasattr(msg, "get_content_string") else str(msg.content)
|
|
648
|
+
if content and content.strip():
|
|
649
|
+
parts.append(f"User: {content}")
|
|
650
|
+
elif msg.role in ["assistant", "model"]:
|
|
651
|
+
content = msg.get_content_string() if hasattr(msg, "get_content_string") else str(msg.content)
|
|
652
|
+
if content and content.strip():
|
|
653
|
+
parts.append(f"Assistant: {content}")
|
|
654
|
+
return "\n".join(parts)
|
|
655
|
+
|
|
656
|
+
def _get_system_message(
|
|
657
|
+
self,
|
|
658
|
+
conversation_text: str,
|
|
659
|
+
existing_context: Optional[Any] = None,
|
|
660
|
+
) -> "Message":
|
|
661
|
+
"""Build system message for extraction.
|
|
662
|
+
|
|
663
|
+
Creates a prompt that guides the model to extract and update session context,
|
|
664
|
+
building on previous context rather than starting fresh each time.
|
|
665
|
+
"""
|
|
666
|
+
from agno.models.message import Message
|
|
667
|
+
|
|
668
|
+
if self.config.system_message is not None:
|
|
669
|
+
return Message(role="system", content=self.config.system_message)
|
|
670
|
+
|
|
671
|
+
enable_planning = self.config.enable_planning
|
|
672
|
+
custom_instructions = self.config.instructions or ""
|
|
673
|
+
|
|
674
|
+
# Build previous context section
|
|
675
|
+
previous_context_section = ""
|
|
676
|
+
if existing_context:
|
|
677
|
+
previous_context_section = dedent("""\
|
|
678
|
+
## Previous Context
|
|
679
|
+
|
|
680
|
+
This session already has context from earlier exchanges. Your job is to UPDATE it,
|
|
681
|
+
not replace it. Integrate new information while preserving what's still relevant.
|
|
682
|
+
|
|
683
|
+
""")
|
|
684
|
+
if hasattr(existing_context, "summary") and existing_context.summary:
|
|
685
|
+
previous_context_section += f"**Previous summary:**\n{existing_context.summary}\n\n"
|
|
686
|
+
if enable_planning:
|
|
687
|
+
if hasattr(existing_context, "goal") and existing_context.goal:
|
|
688
|
+
previous_context_section += f"**Established goal:** {existing_context.goal}\n"
|
|
689
|
+
if hasattr(existing_context, "plan") and existing_context.plan:
|
|
690
|
+
previous_context_section += f"**Current plan:** {', '.join(existing_context.plan)}\n"
|
|
691
|
+
if hasattr(existing_context, "progress") and existing_context.progress:
|
|
692
|
+
previous_context_section += f"**Completed so far:** {', '.join(existing_context.progress)}\n"
|
|
693
|
+
previous_context_section += "\n"
|
|
694
|
+
|
|
695
|
+
if enable_planning:
|
|
696
|
+
system_prompt = (
|
|
697
|
+
dedent("""\
|
|
698
|
+
You are a Session Context Manager. Your job is to maintain a living summary of this
|
|
699
|
+
conversation that enables continuity - especially important when message history
|
|
700
|
+
gets truncated.
|
|
701
|
+
|
|
702
|
+
## Philosophy
|
|
703
|
+
|
|
704
|
+
Think of session context like notes a colleague would take during a working session:
|
|
705
|
+
- Not a transcript, but the current STATE of the work
|
|
706
|
+
- What's been decided, what's still open
|
|
707
|
+
- Where things stand, not every step of how we got here
|
|
708
|
+
- What someone would need to pick up exactly where we left off
|
|
709
|
+
|
|
710
|
+
## What to Capture
|
|
711
|
+
|
|
712
|
+
1. **Summary**: The essential narrative of this session
|
|
713
|
+
- Key topics and how they were resolved
|
|
714
|
+
- Important decisions and their rationale
|
|
715
|
+
- Current state of any work in progress
|
|
716
|
+
- Open questions or unresolved items
|
|
717
|
+
|
|
718
|
+
2. **Goal**: What the user is ultimately trying to accomplish
|
|
719
|
+
- May evolve as the conversation progresses
|
|
720
|
+
- Keep updating if the user clarifies or pivots
|
|
721
|
+
|
|
722
|
+
3. **Plan**: The approach being taken (if one has emerged)
|
|
723
|
+
- Steps that have been outlined
|
|
724
|
+
- Update if the plan changes
|
|
725
|
+
|
|
726
|
+
4. **Progress**: What's been completed
|
|
727
|
+
- Helps track where we are in multi-step work
|
|
728
|
+
- Mark items done as they're completed
|
|
729
|
+
|
|
730
|
+
""")
|
|
731
|
+
+ previous_context_section
|
|
732
|
+
+ dedent("""\
|
|
733
|
+
## New Conversation to Integrate
|
|
734
|
+
|
|
735
|
+
<conversation>
|
|
736
|
+
""")
|
|
737
|
+
+ conversation_text
|
|
738
|
+
+ dedent("""
|
|
739
|
+
</conversation>
|
|
740
|
+
|
|
741
|
+
## Guidelines
|
|
742
|
+
|
|
743
|
+
**Integration, not replacement:**
|
|
744
|
+
- BUILD ON previous context - don't lose earlier information
|
|
745
|
+
- If previous summary mentioned topic X and it's still relevant, keep it
|
|
746
|
+
- If something was resolved or superseded, update accordingly
|
|
747
|
+
|
|
748
|
+
**Quality of summary:**
|
|
749
|
+
- Should stand alone - reader should understand the full session
|
|
750
|
+
- Capture conclusions and current state, not conversation flow
|
|
751
|
+
- Be concise but complete - aim for density of useful information
|
|
752
|
+
- Include enough detail that work could continue seamlessly
|
|
753
|
+
|
|
754
|
+
**Good summary characteristics:**
|
|
755
|
+
- "User is building a REST API for inventory management. Decided on FastAPI over Flask
|
|
756
|
+
for async support. Schema design complete with Products, Categories, and Suppliers tables.
|
|
757
|
+
Currently implementing the Products endpoint with pagination."
|
|
758
|
+
|
|
759
|
+
**Poor summary characteristics:**
|
|
760
|
+
- "User asked about APIs. We discussed some options. Made some decisions."
|
|
761
|
+
(Too vague - doesn't capture what was actually decided)\
|
|
762
|
+
""")
|
|
763
|
+
+ custom_instructions
|
|
764
|
+
+ dedent("""
|
|
765
|
+
|
|
766
|
+
Save your updated context using the save_session_context tool.\
|
|
767
|
+
""")
|
|
768
|
+
)
|
|
769
|
+
else:
|
|
770
|
+
system_prompt = (
|
|
771
|
+
dedent("""\
|
|
772
|
+
You are a Session Context Manager. Your job is to maintain a living summary of this
|
|
773
|
+
conversation that enables continuity - especially important when message history
|
|
774
|
+
gets truncated.
|
|
775
|
+
|
|
776
|
+
## Philosophy
|
|
777
|
+
|
|
778
|
+
Think of session context like meeting notes:
|
|
779
|
+
- Not a transcript, but what matters for continuity
|
|
780
|
+
- What was discussed, decided, and concluded
|
|
781
|
+
- Current state of any ongoing work
|
|
782
|
+
- What someone would need to pick up where we left off
|
|
783
|
+
|
|
784
|
+
## What to Capture
|
|
785
|
+
|
|
786
|
+
Create a summary that includes:
|
|
787
|
+
- **Topics covered** and how they were addressed
|
|
788
|
+
- **Decisions made** and key conclusions
|
|
789
|
+
- **Current state** of any work in progress
|
|
790
|
+
- **Open items** - questions pending, next steps discussed
|
|
791
|
+
- **Important details** that would be awkward to re-establish
|
|
792
|
+
|
|
793
|
+
""")
|
|
794
|
+
+ previous_context_section
|
|
795
|
+
+ dedent("""\
|
|
796
|
+
## New Conversation to Integrate
|
|
797
|
+
|
|
798
|
+
<conversation>
|
|
799
|
+
""")
|
|
800
|
+
+ conversation_text
|
|
801
|
+
+ dedent("""
|
|
802
|
+
</conversation>
|
|
803
|
+
|
|
804
|
+
## Guidelines
|
|
805
|
+
|
|
806
|
+
**Integration, not replacement:**
|
|
807
|
+
- BUILD ON previous summary - don't lose earlier context
|
|
808
|
+
- Weave new information into existing narrative
|
|
809
|
+
- If something is superseded, update it; if still relevant, preserve it
|
|
810
|
+
|
|
811
|
+
**Quality standards:**
|
|
812
|
+
|
|
813
|
+
*Good summary:*
|
|
814
|
+
"Helping user debug a memory leak in their Node.js application. Identified that the
|
|
815
|
+
issue occurs in the WebSocket handler - connections aren't being cleaned up on
|
|
816
|
+
disconnect. Reviewed the connection management code and found missing event listener
|
|
817
|
+
removal. User is implementing the fix with a connection registry pattern. Next step:
|
|
818
|
+
test under load to verify the leak is resolved."
|
|
819
|
+
|
|
820
|
+
*Poor summary:*
|
|
821
|
+
"User had a bug. We looked at code. Found some issues. Working on fixing it."
|
|
822
|
+
(Missing: what bug, what code, what issues, what fix)
|
|
823
|
+
|
|
824
|
+
**Aim for:**
|
|
825
|
+
- Density of useful information
|
|
826
|
+
- Standalone comprehensibility
|
|
827
|
+
- Enough detail to continue seamlessly
|
|
828
|
+
- Focus on state over story
|
|
829
|
+
""")
|
|
830
|
+
+ custom_instructions
|
|
831
|
+
+ dedent("""
|
|
832
|
+
Save your updated summary using the save_session_context tool.\
|
|
833
|
+
""")
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
if self.config.additional_instructions:
|
|
837
|
+
system_prompt += f"\n\n{self.config.additional_instructions}"
|
|
838
|
+
|
|
839
|
+
return Message(role="system", content=system_prompt)
|
|
840
|
+
|
|
841
|
+
def _build_functions_for_model(self, tools: List[Callable]) -> List["Function"]:
|
|
842
|
+
"""Convert callables to Functions for model."""
|
|
843
|
+
from agno.tools.function import Function
|
|
844
|
+
|
|
845
|
+
functions = []
|
|
846
|
+
seen_names = set()
|
|
847
|
+
|
|
848
|
+
for tool in tools:
|
|
849
|
+
try:
|
|
850
|
+
name = tool.__name__
|
|
851
|
+
if name in seen_names:
|
|
852
|
+
continue
|
|
853
|
+
seen_names.add(name)
|
|
854
|
+
|
|
855
|
+
func = Function.from_callable(tool, strict=True)
|
|
856
|
+
func.strict = True
|
|
857
|
+
functions.append(func)
|
|
858
|
+
log_debug(f"Added function {func.name}")
|
|
859
|
+
except Exception as e:
|
|
860
|
+
log_warning(f"Could not add function {tool}: {e}")
|
|
861
|
+
|
|
862
|
+
return functions
|
|
863
|
+
|
|
864
|
+
def _get_extraction_tools(
|
|
865
|
+
self,
|
|
866
|
+
session_id: str,
|
|
867
|
+
user_id: Optional[str] = None,
|
|
868
|
+
agent_id: Optional[str] = None,
|
|
869
|
+
team_id: Optional[str] = None,
|
|
870
|
+
existing_context: Optional[Any] = None,
|
|
871
|
+
) -> List[Callable]:
|
|
872
|
+
"""Get sync extraction tools for the model."""
|
|
873
|
+
enable_planning = self.config.enable_planning
|
|
874
|
+
|
|
875
|
+
if enable_planning:
|
|
876
|
+
# Full planning mode: include goal, plan, progress parameters
|
|
877
|
+
def save_session_context(
|
|
878
|
+
summary: str,
|
|
879
|
+
goal: Optional[str] = None,
|
|
880
|
+
plan: Optional[List[str]] = None,
|
|
881
|
+
progress: Optional[List[str]] = None,
|
|
882
|
+
) -> str:
|
|
883
|
+
"""Save the updated session context.
|
|
884
|
+
|
|
885
|
+
The summary should capture the current state of the conversation in a way that
|
|
886
|
+
enables seamless continuation. Think: "What would someone need to know to pick
|
|
887
|
+
up exactly where we left off?"
|
|
888
|
+
|
|
889
|
+
Args:
|
|
890
|
+
summary: A comprehensive summary that integrates previous context with new
|
|
891
|
+
developments. Should be standalone - readable without seeing the
|
|
892
|
+
actual messages. Capture:
|
|
893
|
+
- What's being worked on and why
|
|
894
|
+
- Key decisions made and their rationale
|
|
895
|
+
- Current state of any work in progress
|
|
896
|
+
- Open questions or pending items
|
|
897
|
+
|
|
898
|
+
Good: "Debugging a React performance issue in the user's dashboard.
|
|
899
|
+
Identified unnecessary re-renders in the DataTable component caused by
|
|
900
|
+
inline object creation in props. Implemented useMemo for the column
|
|
901
|
+
definitions. Testing shows 60% render reduction. Next: profile the
|
|
902
|
+
filtering logic which may have similar issues."
|
|
903
|
+
|
|
904
|
+
Bad: "Looked at React code. Found some performance issues. Made changes."
|
|
905
|
+
|
|
906
|
+
goal: The user's primary objective for this session (if one is apparent).
|
|
907
|
+
Update if the goal has evolved or been clarified.
|
|
908
|
+
|
|
909
|
+
plan: Current plan of action as a list of steps (if a structured approach
|
|
910
|
+
has emerged). Update as the plan evolves.
|
|
911
|
+
|
|
912
|
+
progress: Steps from the plan that have been completed. Add items as work
|
|
913
|
+
is finished to track advancement through the plan.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
Confirmation message.
|
|
917
|
+
"""
|
|
918
|
+
try:
|
|
919
|
+
context_data: Dict[str, Any] = {
|
|
920
|
+
"session_id": session_id,
|
|
921
|
+
"summary": summary,
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
# Preserve previous values if not updated
|
|
925
|
+
if goal is not None:
|
|
926
|
+
context_data["goal"] = goal
|
|
927
|
+
elif existing_context and hasattr(existing_context, "goal"):
|
|
928
|
+
context_data["goal"] = existing_context.goal
|
|
929
|
+
|
|
930
|
+
if plan is not None:
|
|
931
|
+
context_data["plan"] = plan
|
|
932
|
+
elif existing_context and hasattr(existing_context, "plan"):
|
|
933
|
+
context_data["plan"] = existing_context.plan or []
|
|
934
|
+
|
|
935
|
+
if progress is not None:
|
|
936
|
+
context_data["progress"] = progress
|
|
937
|
+
elif existing_context and hasattr(existing_context, "progress"):
|
|
938
|
+
context_data["progress"] = existing_context.progress or []
|
|
939
|
+
|
|
940
|
+
context = from_dict_safe(self.schema, context_data)
|
|
941
|
+
self.save(
|
|
942
|
+
session_id=session_id,
|
|
943
|
+
context=context,
|
|
944
|
+
user_id=user_id,
|
|
945
|
+
agent_id=agent_id,
|
|
946
|
+
team_id=team_id,
|
|
947
|
+
)
|
|
948
|
+
log_debug(f"Session context saved: {summary[:50]}...")
|
|
949
|
+
return "Session context saved"
|
|
950
|
+
except Exception as e:
|
|
951
|
+
log_warning(f"Error saving session context: {e}")
|
|
952
|
+
return f"Error: {e}"
|
|
953
|
+
|
|
954
|
+
else:
|
|
955
|
+
# Summary-only mode: only summary parameter
|
|
956
|
+
def save_session_context(summary: str) -> str: # type: ignore[misc]
|
|
957
|
+
"""Save the updated session summary.
|
|
958
|
+
|
|
959
|
+
The summary should capture the current state of the conversation in a way that
|
|
960
|
+
enables seamless continuation. Think: "What would someone need to know to pick
|
|
961
|
+
up exactly where we left off?"
|
|
962
|
+
|
|
963
|
+
Args:
|
|
964
|
+
summary: A comprehensive summary that integrates previous context with new
|
|
965
|
+
developments. Should be standalone - readable without seeing the
|
|
966
|
+
actual messages. Capture:
|
|
967
|
+
- What's being worked on and why
|
|
968
|
+
- Key decisions made and their rationale
|
|
969
|
+
- Current state of any work in progress
|
|
970
|
+
- Open questions or pending items
|
|
971
|
+
|
|
972
|
+
Good: "Helping user debug a memory leak in their Node.js application.
|
|
973
|
+
Identified that the issue occurs in the WebSocket handler - connections
|
|
974
|
+
aren't being cleaned up on disconnect. Reviewed the connection management
|
|
975
|
+
code and found missing event listener removal. User is implementing the
|
|
976
|
+
fix with a connection registry pattern. Next step: test under load."
|
|
977
|
+
|
|
978
|
+
Bad: "User had a bug. We looked at code. Found some issues. Working on fixing it."
|
|
979
|
+
|
|
980
|
+
Returns:
|
|
981
|
+
Confirmation message.
|
|
982
|
+
"""
|
|
983
|
+
try:
|
|
984
|
+
context_data = {
|
|
985
|
+
"session_id": session_id,
|
|
986
|
+
"summary": summary,
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
context = from_dict_safe(self.schema, context_data)
|
|
990
|
+
self.save(
|
|
991
|
+
session_id=session_id,
|
|
992
|
+
context=context,
|
|
993
|
+
user_id=user_id,
|
|
994
|
+
agent_id=agent_id,
|
|
995
|
+
team_id=team_id,
|
|
996
|
+
)
|
|
997
|
+
log_debug(f"Session context saved: {summary[:50]}...")
|
|
998
|
+
return "Session context saved"
|
|
999
|
+
except Exception as e:
|
|
1000
|
+
log_warning(f"Error saving session context: {e}")
|
|
1001
|
+
return f"Error: {e}"
|
|
1002
|
+
|
|
1003
|
+
return [save_session_context]
|
|
1004
|
+
|
|
1005
|
+
async def _aget_extraction_tools(
|
|
1006
|
+
self,
|
|
1007
|
+
session_id: str,
|
|
1008
|
+
user_id: Optional[str] = None,
|
|
1009
|
+
agent_id: Optional[str] = None,
|
|
1010
|
+
team_id: Optional[str] = None,
|
|
1011
|
+
existing_context: Optional[Any] = None,
|
|
1012
|
+
) -> List[Callable]:
|
|
1013
|
+
"""Get async extraction tools for the model."""
|
|
1014
|
+
enable_planning = self.config.enable_planning
|
|
1015
|
+
|
|
1016
|
+
if enable_planning:
|
|
1017
|
+
# Full planning mode: include goal, plan, progress parameters
|
|
1018
|
+
async def save_session_context(
|
|
1019
|
+
summary: str,
|
|
1020
|
+
goal: Optional[str] = None,
|
|
1021
|
+
plan: Optional[List[str]] = None,
|
|
1022
|
+
progress: Optional[List[str]] = None,
|
|
1023
|
+
) -> str:
|
|
1024
|
+
"""Save the updated session context.
|
|
1025
|
+
|
|
1026
|
+
The summary should capture the current state of the conversation in a way that
|
|
1027
|
+
enables seamless continuation. Think: "What would someone need to know to pick
|
|
1028
|
+
up exactly where we left off?"
|
|
1029
|
+
|
|
1030
|
+
Args:
|
|
1031
|
+
summary: A comprehensive summary that integrates previous context with new
|
|
1032
|
+
developments. Should be standalone - readable without seeing the
|
|
1033
|
+
actual messages. Capture:
|
|
1034
|
+
- What's being worked on and why
|
|
1035
|
+
- Key decisions made and their rationale
|
|
1036
|
+
- Current state of any work in progress
|
|
1037
|
+
- Open questions or pending items
|
|
1038
|
+
|
|
1039
|
+
Good: "Debugging a React performance issue in the user's dashboard.
|
|
1040
|
+
Identified unnecessary re-renders in the DataTable component caused by
|
|
1041
|
+
inline object creation in props. Implemented useMemo for the column
|
|
1042
|
+
definitions. Testing shows 60% render reduction. Next: profile the
|
|
1043
|
+
filtering logic which may have similar issues."
|
|
1044
|
+
|
|
1045
|
+
Bad: "Looked at React code. Found some performance issues. Made changes."
|
|
1046
|
+
|
|
1047
|
+
goal: The user's primary objective for this session (if one is apparent).
|
|
1048
|
+
Update if the goal has evolved or been clarified.
|
|
1049
|
+
|
|
1050
|
+
plan: Current plan of action as a list of steps (if a structured approach
|
|
1051
|
+
has emerged). Update as the plan evolves.
|
|
1052
|
+
|
|
1053
|
+
progress: Steps from the plan that have been completed. Add items as work
|
|
1054
|
+
is finished to track advancement through the plan.
|
|
1055
|
+
|
|
1056
|
+
Returns:
|
|
1057
|
+
Confirmation message.
|
|
1058
|
+
"""
|
|
1059
|
+
try:
|
|
1060
|
+
context_data: Dict[str, Any] = {
|
|
1061
|
+
"session_id": session_id,
|
|
1062
|
+
"summary": summary,
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
# Preserve previous values if not updated
|
|
1066
|
+
if goal is not None:
|
|
1067
|
+
context_data["goal"] = goal
|
|
1068
|
+
elif existing_context and hasattr(existing_context, "goal"):
|
|
1069
|
+
context_data["goal"] = existing_context.goal
|
|
1070
|
+
|
|
1071
|
+
if plan is not None:
|
|
1072
|
+
context_data["plan"] = plan
|
|
1073
|
+
elif existing_context and hasattr(existing_context, "plan"):
|
|
1074
|
+
context_data["plan"] = existing_context.plan or []
|
|
1075
|
+
|
|
1076
|
+
if progress is not None:
|
|
1077
|
+
context_data["progress"] = progress
|
|
1078
|
+
elif existing_context and hasattr(existing_context, "progress"):
|
|
1079
|
+
context_data["progress"] = existing_context.progress or []
|
|
1080
|
+
|
|
1081
|
+
context = from_dict_safe(self.schema, context_data)
|
|
1082
|
+
await self.asave(
|
|
1083
|
+
session_id=session_id,
|
|
1084
|
+
context=context,
|
|
1085
|
+
user_id=user_id,
|
|
1086
|
+
agent_id=agent_id,
|
|
1087
|
+
team_id=team_id,
|
|
1088
|
+
)
|
|
1089
|
+
log_debug(f"Session context saved: {summary[:50]}...")
|
|
1090
|
+
return "Session context saved"
|
|
1091
|
+
except Exception as e:
|
|
1092
|
+
log_warning(f"Error saving session context: {e}")
|
|
1093
|
+
return f"Error: {e}"
|
|
1094
|
+
|
|
1095
|
+
else:
|
|
1096
|
+
# Summary-only mode: only summary parameter
|
|
1097
|
+
async def save_session_context(summary: str) -> str: # type: ignore[misc]
|
|
1098
|
+
"""Save the updated session summary.
|
|
1099
|
+
|
|
1100
|
+
The summary should capture the current state of the conversation in a way that
|
|
1101
|
+
enables seamless continuation. Think: "What would someone need to know to pick
|
|
1102
|
+
up exactly where we left off?"
|
|
1103
|
+
|
|
1104
|
+
Args:
|
|
1105
|
+
summary: A comprehensive summary that integrates previous context with new
|
|
1106
|
+
developments. Should be standalone - readable without seeing the
|
|
1107
|
+
actual messages. Capture:
|
|
1108
|
+
- What's being worked on and why
|
|
1109
|
+
- Key decisions made and their rationale
|
|
1110
|
+
- Current state of any work in progress
|
|
1111
|
+
- Open questions or pending items
|
|
1112
|
+
|
|
1113
|
+
Good: "Helping user debug a memory leak in their Node.js application.
|
|
1114
|
+
Identified that the issue occurs in the WebSocket handler - connections
|
|
1115
|
+
aren't being cleaned up on disconnect. Reviewed the connection management
|
|
1116
|
+
code and found missing event listener removal. User is implementing the
|
|
1117
|
+
fix with a connection registry pattern. Next step: test under load."
|
|
1118
|
+
|
|
1119
|
+
Bad: "User had a bug. We looked at code. Found some issues. Working on fixing it."
|
|
1120
|
+
|
|
1121
|
+
Returns:
|
|
1122
|
+
Confirmation message.
|
|
1123
|
+
"""
|
|
1124
|
+
try:
|
|
1125
|
+
context_data = {
|
|
1126
|
+
"session_id": session_id,
|
|
1127
|
+
"summary": summary,
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
context = from_dict_safe(self.schema, context_data)
|
|
1131
|
+
await self.asave(
|
|
1132
|
+
session_id=session_id,
|
|
1133
|
+
context=context,
|
|
1134
|
+
user_id=user_id,
|
|
1135
|
+
agent_id=agent_id,
|
|
1136
|
+
team_id=team_id,
|
|
1137
|
+
)
|
|
1138
|
+
log_debug(f"Session context saved: {summary[:50]}...")
|
|
1139
|
+
return "Session context saved"
|
|
1140
|
+
except Exception as e:
|
|
1141
|
+
log_warning(f"Error saving session context: {e}")
|
|
1142
|
+
return f"Error: {e}"
|
|
1143
|
+
|
|
1144
|
+
return [save_session_context]
|
|
1145
|
+
|
|
1146
|
+
# =========================================================================
|
|
1147
|
+
# Representation
|
|
1148
|
+
# =========================================================================
|
|
1149
|
+
|
|
1150
|
+
def __repr__(self) -> str:
|
|
1151
|
+
"""String representation for debugging."""
|
|
1152
|
+
has_db = self.db is not None
|
|
1153
|
+
has_model = self.model is not None
|
|
1154
|
+
return (
|
|
1155
|
+
f"SessionContextStore("
|
|
1156
|
+
f"mode={self.config.mode.value}, "
|
|
1157
|
+
f"db={has_db}, "
|
|
1158
|
+
f"model={has_model}, "
|
|
1159
|
+
f"enable_planning={self.config.enable_planning})"
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
def print(self, session_id: str, *, raw: bool = False) -> None:
|
|
1163
|
+
"""Print formatted session context.
|
|
1164
|
+
|
|
1165
|
+
Args:
|
|
1166
|
+
session_id: The session to print context for.
|
|
1167
|
+
raw: If True, print raw dict using pprint instead of formatted panel.
|
|
1168
|
+
|
|
1169
|
+
Example:
|
|
1170
|
+
>>> store.print(session_id="sess_123")
|
|
1171
|
+
╭─────────────── Session Context ───────────────╮
|
|
1172
|
+
│ Summary: Debugging React performance issue... │
|
|
1173
|
+
│ Goal: Fix DataTable re-renders │
|
|
1174
|
+
│ Plan: │
|
|
1175
|
+
│ 1. Profile component renders │
|
|
1176
|
+
│ 2. Identify unnecessary re-renders │
|
|
1177
|
+
│ Progress: │
|
|
1178
|
+
│ ✓ Profile component renders │
|
|
1179
|
+
╰──────────────── sess_123 ─────────────────────╯
|
|
1180
|
+
"""
|
|
1181
|
+
from agno.learn.utils import print_panel
|
|
1182
|
+
|
|
1183
|
+
context = self.get(session_id=session_id)
|
|
1184
|
+
|
|
1185
|
+
lines = []
|
|
1186
|
+
|
|
1187
|
+
if context:
|
|
1188
|
+
if hasattr(context, "summary") and context.summary:
|
|
1189
|
+
lines.append(f"Summary: {context.summary}")
|
|
1190
|
+
|
|
1191
|
+
if hasattr(context, "goal") and context.goal:
|
|
1192
|
+
if lines:
|
|
1193
|
+
lines.append("")
|
|
1194
|
+
lines.append(f"Goal: {context.goal}")
|
|
1195
|
+
|
|
1196
|
+
if hasattr(context, "plan") and context.plan:
|
|
1197
|
+
if lines:
|
|
1198
|
+
lines.append("")
|
|
1199
|
+
lines.append("Plan:")
|
|
1200
|
+
for i, step in enumerate(context.plan, 1):
|
|
1201
|
+
lines.append(f" {i}. {step}")
|
|
1202
|
+
|
|
1203
|
+
if hasattr(context, "progress") and context.progress:
|
|
1204
|
+
if lines:
|
|
1205
|
+
lines.append("")
|
|
1206
|
+
lines.append("Progress:")
|
|
1207
|
+
for step in context.progress:
|
|
1208
|
+
lines.append(f" [green]✓[/green] {step}")
|
|
1209
|
+
|
|
1210
|
+
print_panel(
|
|
1211
|
+
title="Session Context",
|
|
1212
|
+
subtitle=session_id,
|
|
1213
|
+
lines=lines,
|
|
1214
|
+
empty_message="No session context",
|
|
1215
|
+
raw_data=context,
|
|
1216
|
+
raw=raw,
|
|
1217
|
+
)
|