agno 2.3.2__py3-none-any.whl → 2.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agno/agent/agent.py +513 -185
- agno/compression/__init__.py +3 -0
- agno/compression/manager.py +176 -0
- agno/db/dynamo/dynamo.py +11 -0
- agno/db/firestore/firestore.py +5 -1
- agno/db/gcs_json/gcs_json_db.py +5 -2
- agno/db/in_memory/in_memory_db.py +5 -2
- agno/db/json/json_db.py +5 -1
- agno/db/migrations/manager.py +4 -4
- agno/db/mongo/async_mongo.py +158 -34
- agno/db/mongo/mongo.py +6 -2
- agno/db/mysql/mysql.py +48 -54
- agno/db/postgres/async_postgres.py +66 -52
- agno/db/postgres/postgres.py +42 -50
- agno/db/redis/redis.py +5 -0
- agno/db/redis/utils.py +5 -5
- agno/db/singlestore/singlestore.py +99 -108
- agno/db/sqlite/async_sqlite.py +29 -27
- agno/db/sqlite/sqlite.py +30 -26
- agno/knowledge/reader/pdf_reader.py +2 -2
- agno/knowledge/reader/tavily_reader.py +0 -1
- agno/memory/__init__.py +14 -1
- agno/memory/manager.py +217 -4
- agno/memory/strategies/__init__.py +15 -0
- agno/memory/strategies/base.py +67 -0
- agno/memory/strategies/summarize.py +196 -0
- agno/memory/strategies/types.py +37 -0
- agno/models/aimlapi/aimlapi.py +18 -0
- agno/models/anthropic/claude.py +87 -81
- agno/models/aws/bedrock.py +38 -16
- agno/models/aws/claude.py +97 -277
- agno/models/azure/ai_foundry.py +8 -4
- agno/models/base.py +101 -14
- agno/models/cerebras/cerebras.py +25 -9
- agno/models/cerebras/cerebras_openai.py +22 -2
- agno/models/cohere/chat.py +18 -6
- agno/models/cometapi/cometapi.py +19 -1
- agno/models/deepinfra/deepinfra.py +19 -1
- agno/models/fireworks/fireworks.py +19 -1
- agno/models/google/gemini.py +583 -21
- agno/models/groq/groq.py +23 -6
- agno/models/huggingface/huggingface.py +22 -7
- agno/models/ibm/watsonx.py +21 -7
- agno/models/internlm/internlm.py +19 -1
- agno/models/langdb/langdb.py +10 -0
- agno/models/litellm/chat.py +17 -7
- agno/models/litellm/litellm_openai.py +19 -1
- agno/models/message.py +19 -5
- agno/models/meta/llama.py +25 -5
- agno/models/meta/llama_openai.py +18 -0
- agno/models/mistral/mistral.py +13 -5
- agno/models/nvidia/nvidia.py +19 -1
- agno/models/ollama/chat.py +17 -6
- agno/models/openai/chat.py +22 -7
- agno/models/openai/responses.py +28 -10
- agno/models/openrouter/openrouter.py +20 -0
- agno/models/perplexity/perplexity.py +17 -0
- agno/models/requesty/requesty.py +18 -0
- agno/models/sambanova/sambanova.py +19 -1
- agno/models/siliconflow/siliconflow.py +19 -1
- agno/models/together/together.py +19 -1
- agno/models/vercel/v0.py +19 -1
- agno/models/vertexai/claude.py +99 -5
- agno/models/xai/xai.py +18 -0
- agno/os/interfaces/agui/router.py +1 -0
- agno/os/interfaces/agui/utils.py +97 -57
- agno/os/router.py +16 -0
- agno/os/routers/memory/memory.py +143 -0
- agno/os/routers/memory/schemas.py +26 -0
- agno/os/schema.py +33 -6
- agno/os/utils.py +134 -10
- agno/run/base.py +2 -1
- agno/run/workflow.py +1 -1
- agno/team/team.py +566 -219
- agno/tools/mcp/mcp.py +1 -1
- agno/utils/agent.py +119 -1
- agno/utils/models/ai_foundry.py +9 -2
- agno/utils/models/claude.py +12 -5
- agno/utils/models/cohere.py +9 -2
- agno/utils/models/llama.py +9 -2
- agno/utils/models/mistral.py +4 -2
- agno/utils/print_response/agent.py +37 -2
- agno/utils/print_response/team.py +52 -0
- agno/utils/tokens.py +41 -0
- agno/workflow/types.py +2 -2
- {agno-2.3.2.dist-info → agno-2.3.4.dist-info}/METADATA +45 -40
- {agno-2.3.2.dist-info → agno-2.3.4.dist-info}/RECORD +90 -83
- {agno-2.3.2.dist-info → agno-2.3.4.dist-info}/WHEEL +0 -0
- {agno-2.3.2.dist-info → agno-2.3.4.dist-info}/licenses/LICENSE +0 -0
- {agno-2.3.2.dist-info → agno-2.3.4.dist-info}/top_level.txt +0 -0
|
@@ -406,7 +406,7 @@ class PDFImageReader(BasePDFReader):
|
|
|
406
406
|
return []
|
|
407
407
|
|
|
408
408
|
# Read and chunk.
|
|
409
|
-
return self._pdf_reader_to_documents(pdf_reader, doc_name, read_images=True, use_uuid_for_id=
|
|
409
|
+
return self._pdf_reader_to_documents(pdf_reader, doc_name, read_images=True, use_uuid_for_id=True)
|
|
410
410
|
|
|
411
411
|
async def async_read(
|
|
412
412
|
self, pdf: Union[str, Path, IO[Any]], name: Optional[str] = None, password: Optional[str] = None
|
|
@@ -428,4 +428,4 @@ class PDFImageReader(BasePDFReader):
|
|
|
428
428
|
return []
|
|
429
429
|
|
|
430
430
|
# Read and chunk.
|
|
431
|
-
return await self._async_pdf_reader_to_documents(pdf_reader, doc_name, read_images=True, use_uuid_for_id=
|
|
431
|
+
return await self._async_pdf_reader_to_documents(pdf_reader, doc_name, read_images=True, use_uuid_for_id=True)
|
|
@@ -140,7 +140,6 @@ class TavilyReader(Reader):
|
|
|
140
140
|
documents.extend(self.chunk_document(Document(name=name or url, id=url, content=content)))
|
|
141
141
|
else:
|
|
142
142
|
documents.append(Document(name=name or url, id=url, content=content))
|
|
143
|
-
|
|
144
143
|
return documents
|
|
145
144
|
|
|
146
145
|
except Exception as e:
|
agno/memory/__init__.py
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
1
|
from agno.memory.manager import MemoryManager, UserMemory
|
|
2
|
+
from agno.memory.strategies import (
|
|
3
|
+
MemoryOptimizationStrategy,
|
|
4
|
+
MemoryOptimizationStrategyFactory,
|
|
5
|
+
MemoryOptimizationStrategyType,
|
|
6
|
+
SummarizeStrategy,
|
|
7
|
+
)
|
|
2
8
|
|
|
3
|
-
__all__ = [
|
|
9
|
+
__all__ = [
|
|
10
|
+
"MemoryManager",
|
|
11
|
+
"UserMemory",
|
|
12
|
+
"MemoryOptimizationStrategy",
|
|
13
|
+
"MemoryOptimizationStrategyType",
|
|
14
|
+
"MemoryOptimizationStrategyFactory",
|
|
15
|
+
"SummarizeStrategy",
|
|
16
|
+
]
|
agno/memory/manager.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from copy import deepcopy
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from datetime import datetime
|
|
4
3
|
from os import getenv
|
|
5
4
|
from textwrap import dedent
|
|
6
5
|
from typing import Any, Callable, Dict, List, Literal, Optional, Type, Union
|
|
@@ -9,6 +8,11 @@ from pydantic import BaseModel, Field
|
|
|
9
8
|
|
|
10
9
|
from agno.db.base import AsyncBaseDb, BaseDb
|
|
11
10
|
from agno.db.schemas import UserMemory
|
|
11
|
+
from agno.memory.strategies import MemoryOptimizationStrategy
|
|
12
|
+
from agno.memory.strategies.types import (
|
|
13
|
+
MemoryOptimizationStrategyFactory,
|
|
14
|
+
MemoryOptimizationStrategyType,
|
|
15
|
+
)
|
|
12
16
|
from agno.models.base import Model
|
|
13
17
|
from agno.models.message import Message
|
|
14
18
|
from agno.models.utils import get_model
|
|
@@ -90,9 +94,6 @@ class MemoryManager:
|
|
|
90
94
|
self.clear_memories = clear_memories
|
|
91
95
|
self.debug_mode = debug_mode
|
|
92
96
|
|
|
93
|
-
self._get_models()
|
|
94
|
-
|
|
95
|
-
def _get_models(self) -> None:
|
|
96
97
|
if self.model is not None:
|
|
97
98
|
self.model = get_model(self.model)
|
|
98
99
|
|
|
@@ -292,6 +293,74 @@ class MemoryManager:
|
|
|
292
293
|
log_warning("Memory DB not provided.")
|
|
293
294
|
return None
|
|
294
295
|
|
|
296
|
+
def clear_user_memories(self, user_id: Optional[str] = None) -> None:
|
|
297
|
+
"""Clear all memories for a specific user.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
user_id (Optional[str]): The user id to clear memories for. If not provided, clears memories for the "default" user.
|
|
301
|
+
"""
|
|
302
|
+
if user_id is None:
|
|
303
|
+
log_warning("Using default user id.")
|
|
304
|
+
user_id = "default"
|
|
305
|
+
|
|
306
|
+
if not self.db:
|
|
307
|
+
log_warning("Memory DB not provided.")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
311
|
+
raise ValueError(
|
|
312
|
+
"clear_user_memories() is not supported with an async DB. Please use aclear_user_memories() instead."
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# TODO: This is inefficient - we fetch all memories just to get their IDs.
|
|
316
|
+
# Extend delete_user_memories() to accept just user_id and delete all memories
|
|
317
|
+
# for that user directly without requiring a list of memory_ids.
|
|
318
|
+
memories = self.get_user_memories(user_id=user_id)
|
|
319
|
+
if not memories:
|
|
320
|
+
log_debug(f"No memories found for user {user_id}")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
# Extract memory IDs
|
|
324
|
+
memory_ids = [mem.memory_id for mem in memories if mem.memory_id]
|
|
325
|
+
|
|
326
|
+
if memory_ids:
|
|
327
|
+
# Delete all memories in a single batch operation
|
|
328
|
+
self.db.delete_user_memories(memory_ids=memory_ids, user_id=user_id)
|
|
329
|
+
log_debug(f"Cleared {len(memory_ids)} memories for user {user_id}")
|
|
330
|
+
|
|
331
|
+
async def aclear_user_memories(self, user_id: Optional[str] = None) -> None:
|
|
332
|
+
"""Clear all memories for a specific user (async).
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
user_id (Optional[str]): The user id to clear memories for. If not provided, clears memories for the "default" user.
|
|
336
|
+
"""
|
|
337
|
+
if user_id is None:
|
|
338
|
+
user_id = "default"
|
|
339
|
+
|
|
340
|
+
if not self.db:
|
|
341
|
+
log_warning("Memory DB not provided.")
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
345
|
+
memories = await self.aget_user_memories(user_id=user_id)
|
|
346
|
+
else:
|
|
347
|
+
memories = self.get_user_memories(user_id=user_id)
|
|
348
|
+
|
|
349
|
+
if not memories:
|
|
350
|
+
log_debug(f"No memories found for user {user_id}")
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
# Extract memory IDs
|
|
354
|
+
memory_ids = [mem.memory_id for mem in memories if mem.memory_id]
|
|
355
|
+
|
|
356
|
+
if memory_ids:
|
|
357
|
+
# Delete all memories in a single batch operation
|
|
358
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
359
|
+
await self.db.delete_user_memories(memory_ids=memory_ids, user_id=user_id)
|
|
360
|
+
else:
|
|
361
|
+
self.db.delete_user_memories(memory_ids=memory_ids, user_id=user_id)
|
|
362
|
+
log_debug(f"Cleared {len(memory_ids)} memories for user {user_id}")
|
|
363
|
+
|
|
295
364
|
# -*- Agent Functions
|
|
296
365
|
def create_user_memories(
|
|
297
366
|
self,
|
|
@@ -714,6 +783,150 @@ class MemoryManager:
|
|
|
714
783
|
|
|
715
784
|
return sorted_memories_list
|
|
716
785
|
|
|
786
|
+
def optimize_memories(
|
|
787
|
+
self,
|
|
788
|
+
user_id: Optional[str] = None,
|
|
789
|
+
strategy: Union[
|
|
790
|
+
MemoryOptimizationStrategyType, MemoryOptimizationStrategy
|
|
791
|
+
] = MemoryOptimizationStrategyType.SUMMARIZE,
|
|
792
|
+
apply: bool = True,
|
|
793
|
+
) -> List[UserMemory]:
|
|
794
|
+
"""Optimize user memories using the specified strategy.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
user_id: User ID to optimize memories for. Defaults to "default".
|
|
798
|
+
strategy: Optimization strategy. Can be:
|
|
799
|
+
- Enum: MemoryOptimizationStrategyType.SUMMARIZE
|
|
800
|
+
- Instance: Custom MemoryOptimizationStrategy instance
|
|
801
|
+
apply: If True, automatically replace memories in database.
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
List of optimized UserMemory objects.
|
|
805
|
+
"""
|
|
806
|
+
if user_id is None:
|
|
807
|
+
user_id = "default"
|
|
808
|
+
|
|
809
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
810
|
+
raise ValueError(
|
|
811
|
+
"optimize_memories() is not supported with an async DB. Please use aoptimize_memories() instead."
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
# Get user memories
|
|
815
|
+
memories = self.get_user_memories(user_id=user_id)
|
|
816
|
+
if not memories:
|
|
817
|
+
log_debug("No memories to optimize")
|
|
818
|
+
return []
|
|
819
|
+
|
|
820
|
+
# Get strategy instance
|
|
821
|
+
if isinstance(strategy, MemoryOptimizationStrategyType):
|
|
822
|
+
strategy_instance = MemoryOptimizationStrategyFactory.create_strategy(strategy)
|
|
823
|
+
else:
|
|
824
|
+
# Already a strategy instance
|
|
825
|
+
strategy_instance = strategy
|
|
826
|
+
|
|
827
|
+
# Optimize memories using strategy
|
|
828
|
+
optimization_model = self.get_model()
|
|
829
|
+
optimized_memories = strategy_instance.optimize(memories=memories, model=optimization_model)
|
|
830
|
+
|
|
831
|
+
# Apply to database if requested
|
|
832
|
+
if apply:
|
|
833
|
+
log_debug(f"Applying optimized memories to database for user {user_id}")
|
|
834
|
+
|
|
835
|
+
if not self.db:
|
|
836
|
+
log_warning("Memory DB not provided. Cannot apply optimized memories.")
|
|
837
|
+
return optimized_memories
|
|
838
|
+
|
|
839
|
+
# Clear all existing memories for the user
|
|
840
|
+
self.clear_user_memories(user_id=user_id)
|
|
841
|
+
|
|
842
|
+
# Add all optimized memories
|
|
843
|
+
for opt_mem in optimized_memories:
|
|
844
|
+
# Ensure memory has an ID (generate if needed for new memories)
|
|
845
|
+
if not opt_mem.memory_id:
|
|
846
|
+
from uuid import uuid4
|
|
847
|
+
|
|
848
|
+
opt_mem.memory_id = str(uuid4())
|
|
849
|
+
|
|
850
|
+
self.db.upsert_user_memory(memory=opt_mem)
|
|
851
|
+
|
|
852
|
+
optimized_tokens = strategy_instance.count_tokens(optimized_memories)
|
|
853
|
+
log_debug(f"Optimization complete. New token count: {optimized_tokens}")
|
|
854
|
+
|
|
855
|
+
return optimized_memories
|
|
856
|
+
|
|
857
|
+
async def aoptimize_memories(
|
|
858
|
+
self,
|
|
859
|
+
user_id: Optional[str] = None,
|
|
860
|
+
strategy: Union[
|
|
861
|
+
MemoryOptimizationStrategyType, MemoryOptimizationStrategy
|
|
862
|
+
] = MemoryOptimizationStrategyType.SUMMARIZE,
|
|
863
|
+
apply: bool = True,
|
|
864
|
+
) -> List[UserMemory]:
|
|
865
|
+
"""Async version of optimize_memories.
|
|
866
|
+
|
|
867
|
+
Args:
|
|
868
|
+
user_id: User ID to optimize memories for. Defaults to "default".
|
|
869
|
+
strategy: Optimization strategy. Can be:
|
|
870
|
+
- Enum: MemoryOptimizationStrategyType.SUMMARIZE
|
|
871
|
+
- Instance: Custom MemoryOptimizationStrategy instance
|
|
872
|
+
apply: If True, automatically replace memories in database.
|
|
873
|
+
|
|
874
|
+
Returns:
|
|
875
|
+
List of optimized UserMemory objects.
|
|
876
|
+
"""
|
|
877
|
+
if user_id is None:
|
|
878
|
+
user_id = "default"
|
|
879
|
+
|
|
880
|
+
# Get user memories - handle both sync and async DBs
|
|
881
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
882
|
+
memories = await self.aget_user_memories(user_id=user_id)
|
|
883
|
+
else:
|
|
884
|
+
memories = self.get_user_memories(user_id=user_id)
|
|
885
|
+
|
|
886
|
+
if not memories:
|
|
887
|
+
log_debug("No memories to optimize")
|
|
888
|
+
return []
|
|
889
|
+
|
|
890
|
+
# Get strategy instance
|
|
891
|
+
if isinstance(strategy, MemoryOptimizationStrategyType):
|
|
892
|
+
strategy_instance = MemoryOptimizationStrategyFactory.create_strategy(strategy)
|
|
893
|
+
else:
|
|
894
|
+
# Already a strategy instance
|
|
895
|
+
strategy_instance = strategy
|
|
896
|
+
|
|
897
|
+
# Optimize memories using strategy (async)
|
|
898
|
+
optimization_model = self.get_model()
|
|
899
|
+
optimized_memories = await strategy_instance.aoptimize(memories=memories, model=optimization_model)
|
|
900
|
+
|
|
901
|
+
# Apply to database if requested
|
|
902
|
+
if apply:
|
|
903
|
+
log_debug(f"Optimizing memories for user {user_id}")
|
|
904
|
+
|
|
905
|
+
if not self.db:
|
|
906
|
+
log_warning("Memory DB not provided. Cannot apply optimized memories.")
|
|
907
|
+
return optimized_memories
|
|
908
|
+
|
|
909
|
+
# Clear all existing memories for the user
|
|
910
|
+
await self.aclear_user_memories(user_id=user_id)
|
|
911
|
+
|
|
912
|
+
# Add all optimized memories
|
|
913
|
+
for opt_mem in optimized_memories:
|
|
914
|
+
# Ensure memory has an ID (generate if needed for new memories)
|
|
915
|
+
if not opt_mem.memory_id:
|
|
916
|
+
from uuid import uuid4
|
|
917
|
+
|
|
918
|
+
opt_mem.memory_id = str(uuid4())
|
|
919
|
+
|
|
920
|
+
if isinstance(self.db, AsyncBaseDb):
|
|
921
|
+
await self.db.upsert_user_memory(memory=opt_mem)
|
|
922
|
+
elif isinstance(self.db, BaseDb):
|
|
923
|
+
self.db.upsert_user_memory(memory=opt_mem)
|
|
924
|
+
|
|
925
|
+
optimized_tokens = strategy_instance.count_tokens(optimized_memories)
|
|
926
|
+
log_debug(f"Memory optimization complete. New token count: {optimized_tokens}")
|
|
927
|
+
|
|
928
|
+
return optimized_memories
|
|
929
|
+
|
|
717
930
|
# --Memory Manager Functions--
|
|
718
931
|
def determine_tools_for_model(self, tools: List[Callable]) -> List[Union[Function, dict]]:
|
|
719
932
|
# Have to reset each time, because of different user IDs
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Memory optimization strategy implementations."""
|
|
2
|
+
|
|
3
|
+
from agno.memory.strategies.base import MemoryOptimizationStrategy
|
|
4
|
+
from agno.memory.strategies.summarize import SummarizeStrategy
|
|
5
|
+
from agno.memory.strategies.types import (
|
|
6
|
+
MemoryOptimizationStrategyFactory,
|
|
7
|
+
MemoryOptimizationStrategyType,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"MemoryOptimizationStrategy",
|
|
12
|
+
"MemoryOptimizationStrategyFactory",
|
|
13
|
+
"MemoryOptimizationStrategyType",
|
|
14
|
+
"SummarizeStrategy",
|
|
15
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from agno.db.schemas import UserMemory
|
|
5
|
+
from agno.models.base import Model
|
|
6
|
+
from agno.utils.tokens import count_tokens as count_text_tokens
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MemoryOptimizationStrategy(ABC):
|
|
10
|
+
"""Abstract base class for memory optimization strategies.
|
|
11
|
+
|
|
12
|
+
Subclasses must implement optimize() and aoptimize().
|
|
13
|
+
get_system_prompt() is optional and only needed for LLM-based strategies.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def get_system_prompt(self) -> str:
|
|
17
|
+
"""Get system prompt for this optimization strategy.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
System prompt string for LLM-based strategies.
|
|
21
|
+
"""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def optimize(
|
|
26
|
+
self,
|
|
27
|
+
memories: List[UserMemory],
|
|
28
|
+
model: Model,
|
|
29
|
+
) -> List[UserMemory]:
|
|
30
|
+
"""Optimize memories synchronously.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
memories: List of UserMemory objects to optimize
|
|
34
|
+
model: Model to use for optimization (if needed)
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of optimized UserMemory objects
|
|
38
|
+
"""
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
async def aoptimize(
|
|
43
|
+
self,
|
|
44
|
+
memories: List[UserMemory],
|
|
45
|
+
model: Model,
|
|
46
|
+
) -> List[UserMemory]:
|
|
47
|
+
"""Optimize memories asynchronously.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
memories: List of UserMemory objects to optimize
|
|
51
|
+
model: Model to use for optimization (if needed)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of optimized UserMemory objects
|
|
55
|
+
"""
|
|
56
|
+
raise NotImplementedError
|
|
57
|
+
|
|
58
|
+
def count_tokens(self, memories: List[UserMemory]) -> int:
|
|
59
|
+
"""Count total tokens across all memories.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
memories: List of UserMemory objects
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Total token count using tiktoken (or fallback estimation)
|
|
66
|
+
"""
|
|
67
|
+
return sum(count_text_tokens(mem.memory or "") for mem in memories)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Summarize strategy: Combine all memories into single comprehensive summary."""
|
|
2
|
+
|
|
3
|
+
from textwrap import dedent
|
|
4
|
+
from typing import List
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from agno.db.schemas import UserMemory
|
|
8
|
+
from agno.memory.strategies import MemoryOptimizationStrategy
|
|
9
|
+
from agno.models.base import Model
|
|
10
|
+
from agno.models.message import Message
|
|
11
|
+
from agno.utils.dttm import now_epoch_s
|
|
12
|
+
from agno.utils.log import log_debug
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SummarizeStrategy(MemoryOptimizationStrategy):
|
|
16
|
+
"""Combine all memories into single comprehensive summary.
|
|
17
|
+
|
|
18
|
+
This strategy summarizes all memories into one coherent narrative,
|
|
19
|
+
achieving maximum compression by eliminating redundancy. All
|
|
20
|
+
metadata (topics, user_id) is preserved in the summarized memory.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def _get_system_prompt(self) -> str:
|
|
24
|
+
"""Get system prompt for memory summarization.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
System prompt string for LLM
|
|
28
|
+
"""
|
|
29
|
+
return dedent("""\
|
|
30
|
+
You are a memory compression assistant. Your task is to summarize multiple memories about a user
|
|
31
|
+
into a single comprehensive summary while preserving all key facts.
|
|
32
|
+
|
|
33
|
+
Requirements:
|
|
34
|
+
- Combine related information from all memories
|
|
35
|
+
- Preserve all factual information
|
|
36
|
+
- Remove redundancy and consolidate repeated facts
|
|
37
|
+
- Create a coherent narrative about the user
|
|
38
|
+
- Maintain third-person perspective
|
|
39
|
+
- Do not add information not present in the original memories
|
|
40
|
+
|
|
41
|
+
Return only the summarized memory text, nothing else.\
|
|
42
|
+
""")
|
|
43
|
+
|
|
44
|
+
def optimize(
|
|
45
|
+
self,
|
|
46
|
+
memories: List[UserMemory],
|
|
47
|
+
model: Model,
|
|
48
|
+
) -> List[UserMemory]:
|
|
49
|
+
"""Summarize multiple memories into single comprehensive summary.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
memories: List of UserMemory objects to summarize
|
|
53
|
+
model: Model to use for summarization
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List containing single summarized UserMemory object
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If memories list is empty or if user_id cannot be determined
|
|
60
|
+
"""
|
|
61
|
+
# Validate memories list
|
|
62
|
+
if not memories:
|
|
63
|
+
raise ValueError("No Memories found")
|
|
64
|
+
|
|
65
|
+
# Extract user_id from first memory
|
|
66
|
+
user_id = memories[0].user_id
|
|
67
|
+
if user_id is None:
|
|
68
|
+
raise ValueError("Cannot determine user_id: first memory does not have a valid user_id or is None")
|
|
69
|
+
|
|
70
|
+
# Collect all memory contents
|
|
71
|
+
memory_contents = [mem.memory for mem in memories if mem.memory]
|
|
72
|
+
|
|
73
|
+
# Combine topics - get unique topics from all memories
|
|
74
|
+
all_topics: List[str] = []
|
|
75
|
+
for mem in memories:
|
|
76
|
+
if mem.topics:
|
|
77
|
+
all_topics.extend(mem.topics)
|
|
78
|
+
summarized_topics = list(set(all_topics)) if all_topics else None
|
|
79
|
+
|
|
80
|
+
# Check if agent_id and team_id are consistent
|
|
81
|
+
agent_ids = {mem.agent_id for mem in memories if mem.agent_id}
|
|
82
|
+
summarized_agent_id = list(agent_ids)[0] if len(agent_ids) == 1 else None
|
|
83
|
+
|
|
84
|
+
team_ids = {mem.team_id for mem in memories if mem.team_id}
|
|
85
|
+
summarized_team_id = list(team_ids)[0] if len(team_ids) == 1 else None
|
|
86
|
+
|
|
87
|
+
# Create comprehensive prompt for summarization
|
|
88
|
+
combined_content = "\n\n".join([f"Memory {i + 1}: {content}" for i, content in enumerate(memory_contents)])
|
|
89
|
+
|
|
90
|
+
system_prompt = self._get_system_prompt()
|
|
91
|
+
|
|
92
|
+
messages_for_model = [
|
|
93
|
+
Message(role="system", content=system_prompt),
|
|
94
|
+
Message(role="user", content=f"Summarize these memories into a single summary:\n\n{combined_content}"),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Generate summarized content
|
|
98
|
+
response = model.response(messages=messages_for_model)
|
|
99
|
+
summarized_content = response.content or " ".join(memory_contents)
|
|
100
|
+
|
|
101
|
+
# Generate new memory_id
|
|
102
|
+
new_memory_id = str(uuid4())
|
|
103
|
+
|
|
104
|
+
# Create summarized memory
|
|
105
|
+
summarized_memory = UserMemory(
|
|
106
|
+
memory_id=new_memory_id,
|
|
107
|
+
memory=summarized_content.strip(),
|
|
108
|
+
topics=summarized_topics,
|
|
109
|
+
user_id=user_id,
|
|
110
|
+
agent_id=summarized_agent_id,
|
|
111
|
+
team_id=summarized_team_id,
|
|
112
|
+
updated_at=now_epoch_s(),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
log_debug(
|
|
116
|
+
f"Summarized {len(memories)} memories into 1: {self.count_tokens(memories)} -> {self.count_tokens([summarized_memory])} tokens"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return [summarized_memory]
|
|
120
|
+
|
|
121
|
+
async def aoptimize(
|
|
122
|
+
self,
|
|
123
|
+
memories: List[UserMemory],
|
|
124
|
+
model: Model,
|
|
125
|
+
) -> List[UserMemory]:
|
|
126
|
+
"""Async version: Summarize multiple memories into single comprehensive summary.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
memories: List of UserMemory objects to summarize
|
|
130
|
+
model: Model to use for summarization
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List containing single summarized UserMemory object
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
ValueError: If memories list is empty or if user_id cannot be determined
|
|
137
|
+
"""
|
|
138
|
+
# Validate memories list
|
|
139
|
+
if not memories:
|
|
140
|
+
raise ValueError("No Memories found")
|
|
141
|
+
|
|
142
|
+
# Extract user_id from first memory
|
|
143
|
+
user_id = memories[0].user_id
|
|
144
|
+
if user_id is None:
|
|
145
|
+
raise ValueError("Cannot determine user_id: first memory does not have a valid user_id or is None")
|
|
146
|
+
|
|
147
|
+
# Collect all memory contents
|
|
148
|
+
memory_contents = [mem.memory for mem in memories if mem.memory]
|
|
149
|
+
|
|
150
|
+
# Combine topics - get unique topics from all memories
|
|
151
|
+
all_topics: List[str] = []
|
|
152
|
+
for mem in memories:
|
|
153
|
+
if mem.topics:
|
|
154
|
+
all_topics.extend(mem.topics)
|
|
155
|
+
summarized_topics = list(set(all_topics)) if all_topics else None
|
|
156
|
+
|
|
157
|
+
# Check if agent_id and team_id are consistent
|
|
158
|
+
agent_ids = {mem.agent_id for mem in memories if mem.agent_id}
|
|
159
|
+
summarized_agent_id = list(agent_ids)[0] if len(agent_ids) == 1 else None
|
|
160
|
+
|
|
161
|
+
team_ids = {mem.team_id for mem in memories if mem.team_id}
|
|
162
|
+
summarized_team_id = list(team_ids)[0] if len(team_ids) == 1 else None
|
|
163
|
+
|
|
164
|
+
# Create comprehensive prompt for summarization
|
|
165
|
+
combined_content = "\n\n".join([f"Memory {i + 1}: {content}" for i, content in enumerate(memory_contents)])
|
|
166
|
+
|
|
167
|
+
system_prompt = self._get_system_prompt()
|
|
168
|
+
|
|
169
|
+
messages_for_model = [
|
|
170
|
+
Message(role="system", content=system_prompt),
|
|
171
|
+
Message(role="user", content=f"Summarize these memories into a single summary:\n\n{combined_content}"),
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
# Generate summarized content (async)
|
|
175
|
+
response = await model.aresponse(messages=messages_for_model)
|
|
176
|
+
summarized_content = response.content or " ".join(memory_contents)
|
|
177
|
+
|
|
178
|
+
# Generate new memory_id
|
|
179
|
+
new_memory_id = str(uuid4())
|
|
180
|
+
|
|
181
|
+
# Create summarized memory
|
|
182
|
+
summarized_memory = UserMemory(
|
|
183
|
+
memory_id=new_memory_id,
|
|
184
|
+
memory=summarized_content.strip(),
|
|
185
|
+
topics=summarized_topics,
|
|
186
|
+
user_id=user_id,
|
|
187
|
+
agent_id=summarized_agent_id,
|
|
188
|
+
team_id=summarized_team_id,
|
|
189
|
+
updated_at=now_epoch_s(),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
log_debug(
|
|
193
|
+
f"Summarized {len(memories)} memories into 1: {self.count_tokens(memories)} -> {self.count_tokens([summarized_memory])} tokens"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return [summarized_memory]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Memory optimization strategy types and factory."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from agno.memory.strategies import MemoryOptimizationStrategy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MemoryOptimizationStrategyType(str, Enum):
|
|
9
|
+
"""Enumeration of available memory optimization strategies."""
|
|
10
|
+
|
|
11
|
+
SUMMARIZE = "summarize"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MemoryOptimizationStrategyFactory:
|
|
15
|
+
"""Factory for creating memory optimization strategy instances."""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def create_strategy(cls, strategy_type: MemoryOptimizationStrategyType, **kwargs) -> MemoryOptimizationStrategy:
|
|
19
|
+
"""Create an instance of the optimization strategy with given parameters.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
strategy_type: Type of strategy to create
|
|
23
|
+
**kwargs: Additional parameters for strategy initialization
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
MemoryOptimizationStrategy instance
|
|
27
|
+
"""
|
|
28
|
+
strategy_map = {
|
|
29
|
+
MemoryOptimizationStrategyType.SUMMARIZE: cls._create_summarize_strategy,
|
|
30
|
+
}
|
|
31
|
+
return strategy_map[strategy_type](**kwargs)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _create_summarize_strategy(cls, **kwargs) -> MemoryOptimizationStrategy:
|
|
35
|
+
from agno.memory.strategies.summarize import SummarizeStrategy
|
|
36
|
+
|
|
37
|
+
return SummarizeStrategy(**kwargs)
|
agno/models/aimlapi/aimlapi.py
CHANGED
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass, field
|
|
|
2
2
|
from os import getenv
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
4
|
|
|
5
|
+
from agno.exceptions import ModelProviderError
|
|
5
6
|
from agno.models.message import Message
|
|
6
7
|
from agno.models.openai.like import OpenAILike
|
|
7
8
|
|
|
@@ -28,6 +29,23 @@ class AIMLAPI(OpenAILike):
|
|
|
28
29
|
base_url: str = "https://api.aimlapi.com/v1"
|
|
29
30
|
max_tokens: int = 4096
|
|
30
31
|
|
|
32
|
+
def _get_client_params(self) -> Dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Returns client parameters for API requests, checking for AIMLAPI_API_KEY.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dict[str, Any]: A dictionary of client parameters for API requests.
|
|
38
|
+
"""
|
|
39
|
+
if not self.api_key:
|
|
40
|
+
self.api_key = getenv("AIMLAPI_API_KEY")
|
|
41
|
+
if not self.api_key:
|
|
42
|
+
raise ModelProviderError(
|
|
43
|
+
message="AIMLAPI_API_KEY not set. Please set the AIMLAPI_API_KEY environment variable.",
|
|
44
|
+
model_name=self.name,
|
|
45
|
+
model_id=self.id,
|
|
46
|
+
)
|
|
47
|
+
return super()._get_client_params()
|
|
48
|
+
|
|
31
49
|
def _format_message(self, message: Message) -> Dict[str, Any]:
|
|
32
50
|
"""
|
|
33
51
|
Minimal additional formatter that only replaces None with empty string.
|