agno 2.3.24__py3-none-any.whl → 2.3.26__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. agno/agent/agent.py +357 -28
  2. agno/db/base.py +214 -0
  3. agno/db/dynamo/dynamo.py +47 -0
  4. agno/db/firestore/firestore.py +47 -0
  5. agno/db/gcs_json/gcs_json_db.py +47 -0
  6. agno/db/in_memory/in_memory_db.py +47 -0
  7. agno/db/json/json_db.py +47 -0
  8. agno/db/mongo/async_mongo.py +229 -0
  9. agno/db/mongo/mongo.py +47 -0
  10. agno/db/mongo/schemas.py +16 -0
  11. agno/db/mysql/async_mysql.py +47 -0
  12. agno/db/mysql/mysql.py +47 -0
  13. agno/db/postgres/async_postgres.py +231 -0
  14. agno/db/postgres/postgres.py +239 -0
  15. agno/db/postgres/schemas.py +19 -0
  16. agno/db/redis/redis.py +47 -0
  17. agno/db/singlestore/singlestore.py +47 -0
  18. agno/db/sqlite/async_sqlite.py +242 -0
  19. agno/db/sqlite/schemas.py +18 -0
  20. agno/db/sqlite/sqlite.py +239 -0
  21. agno/db/surrealdb/surrealdb.py +47 -0
  22. agno/knowledge/chunking/code.py +90 -0
  23. agno/knowledge/chunking/document.py +62 -2
  24. agno/knowledge/chunking/strategy.py +14 -0
  25. agno/knowledge/knowledge.py +7 -1
  26. agno/knowledge/reader/arxiv_reader.py +1 -0
  27. agno/knowledge/reader/csv_reader.py +1 -0
  28. agno/knowledge/reader/docx_reader.py +1 -0
  29. agno/knowledge/reader/firecrawl_reader.py +1 -0
  30. agno/knowledge/reader/json_reader.py +1 -0
  31. agno/knowledge/reader/markdown_reader.py +1 -0
  32. agno/knowledge/reader/pdf_reader.py +1 -0
  33. agno/knowledge/reader/pptx_reader.py +1 -0
  34. agno/knowledge/reader/s3_reader.py +1 -0
  35. agno/knowledge/reader/tavily_reader.py +1 -0
  36. agno/knowledge/reader/text_reader.py +1 -0
  37. agno/knowledge/reader/web_search_reader.py +1 -0
  38. agno/knowledge/reader/website_reader.py +1 -0
  39. agno/knowledge/reader/wikipedia_reader.py +1 -0
  40. agno/knowledge/reader/youtube_reader.py +1 -0
  41. agno/knowledge/utils.py +1 -0
  42. agno/learn/__init__.py +65 -0
  43. agno/learn/config.py +463 -0
  44. agno/learn/curate.py +185 -0
  45. agno/learn/machine.py +690 -0
  46. agno/learn/schemas.py +1043 -0
  47. agno/learn/stores/__init__.py +35 -0
  48. agno/learn/stores/entity_memory.py +3275 -0
  49. agno/learn/stores/learned_knowledge.py +1583 -0
  50. agno/learn/stores/protocol.py +117 -0
  51. agno/learn/stores/session_context.py +1217 -0
  52. agno/learn/stores/user_memory.py +1495 -0
  53. agno/learn/stores/user_profile.py +1220 -0
  54. agno/learn/utils.py +209 -0
  55. agno/models/base.py +59 -0
  56. agno/os/routers/agents/router.py +4 -4
  57. agno/os/routers/knowledge/knowledge.py +7 -0
  58. agno/os/routers/teams/router.py +3 -3
  59. agno/os/routers/workflows/router.py +5 -5
  60. agno/os/utils.py +55 -3
  61. agno/team/team.py +131 -0
  62. agno/tools/browserbase.py +78 -6
  63. agno/tools/google_bigquery.py +11 -2
  64. agno/utils/agent.py +30 -1
  65. agno/workflow/workflow.py +198 -0
  66. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/METADATA +24 -2
  67. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/RECORD +70 -56
  68. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/WHEEL +0 -0
  69. {agno-2.3.24.dist-info → agno-2.3.26.dist-info}/licenses/LICENSE +0 -0
  70. {agno-2.3.24.dist-info → agno-2.3.26.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
+ )