ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
# Memory
|
||||
|
||||

|
||||
|
||||
## Introduction
|
||||
|
||||
The Aworld Memory module provides a unified memory management mechanism for multi-agent systems. It is designed to enable agents to store, retrieve, and process information, thereby facilitating continuous learning and personalized interactions. The module supports both short-term and long-term memory and offers flexible configuration options to suit various application scenarios.
|
||||
|
||||
Key features include:
|
||||
- **Short-Term Memory**: For quick access to recent interaction content, with various summarization strategies to control context length.
|
||||
- **Long-Term Memory**: Persistently stores key information through structured `UserProfile` and `AgentExperience` models, enabling agents to learn and grow across sessions.
|
||||
- **Flexible Backend Support**: Supports various vector databases and data storage backends, such as ChromaDB and PostgreSQL.
|
||||
- **Customizable Configuration**: Provides detailed configuration options, allowing developers to tailor memory behavior to their needs.
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Short-Term Memory
|
||||
|
||||
Short-term memory is used to store immediate interaction records, such as `User2Agent`, `Agent2Agent`, and interactions between an `Agent` and `LLM/Tool`. It centrally stores all messages through a unified `MemoryStore` (e.g., `InMemoryMemoryStore`) and provides the `get_last_n(last_rounds)` method to quickly retrieve the latest N messages.
|
||||
|
||||
#### Summarization Strategy
|
||||
|
||||
To optimize performance and manage context length, the system includes several automatic summarization strategies:
|
||||
|
||||
| Strategy | Description | Configuration |
|
||||
| --- | --- | --- |
|
||||
| Trimming Strategy | Keeps only the most recent `N` rounds of conversation. | `trim_rounds=100` (default) |
|
||||
| Fixed Step Summary | Creates a summary every `N` messages, retaining only the summary. | `enable_summary=true`<br/>`summary_rounds=5` |
|
||||
| Fixed Context Length Summary | Compresses the conversation history when the total length of unsaved messages exceeds a threshold. | `enable_summary=true`<br/>`summary_context_length=10000` |
|
||||
| Current Round Summary | Compresses the current conversation when the length of the latest message exceeds a threshold. | `enable_summary=true`<br/>`summary_single_context_length=10000` |
|
||||
|
||||
### Long-Term Memory
|
||||
|
||||
Long-term memory enables continuous learning and personalized interactions through two main types: `UserProfile` and `AgentExperience`.
|
||||
|
||||
#### UserProfile
|
||||
|
||||
`UserProfile` systematically captures and stores user information, preferences, and behavioral patterns in a `key-value` structure to build a detailed user profile.
|
||||
|
||||
```python
|
||||
class UserProfileItem(BaseModel):
|
||||
key: str = Field(description="The key of the profile")
|
||||
value: Any = Field(description="The value of the profile")
|
||||
|
||||
class UserProfile(MemoryItem):
|
||||
"""
|
||||
Represents a user profile key-value pair.
|
||||
"""
|
||||
def __init__(self, user_id: str, key: str, value: Any, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
meta = metadata.copy() if metadata else {}
|
||||
meta['user_id'] = user_id
|
||||
user_profile = UserProfileItem(key=key, value=value)
|
||||
super().__init__(content=user_profile, metadata=meta, memory_type="user_profile")
|
||||
```
|
||||
|
||||
#### AgentExperience
|
||||
|
||||
`AgentExperience` focuses on capturing the skills and action sequences an AI agent acquires during task execution. It records problem-solving patterns in a `skill-actions` structure, allowing the agent to learn from past experiences and improve future performance.
|
||||
|
||||
```python
|
||||
class AgentExperienceItem(BaseModel):
|
||||
skill: str = Field(description="The skill demonstrated in the experience")
|
||||
actions: List[str] = Field(description="The actions taken by the agent")
|
||||
|
||||
|
||||
class AgentExperience(MemoryItem):
|
||||
"""
|
||||
Represents an agent's experience, including skills and actions.
|
||||
"""
|
||||
def __init__(self, agent_id: str, skill: str, actions: List[str], metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
meta = metadata.copy() if metadata else {}
|
||||
meta['agent_id'] = agent_id
|
||||
agent_experience = AgentExperienceItem(skill=skill, actions=actions)
|
||||
super().__init__(content=agent_experience, metadata=meta, memory_type="agent_experience")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
Core interactions with the Memory module are defined by the `MemoryBase` abstract class, which provides a unified interface for memory operations.
|
||||
|
||||
```python
|
||||
from aworld.core.memory import MemoryBase, MemoryItem, AgentMemoryConfig
|
||||
from aworld.memory.models import UserProfile, AgentExperience, LongTermMemoryTriggerParams
|
||||
|
||||
class MemoryBase(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def get(self, memory_id) -> Optional[MemoryItem]: ...
|
||||
|
||||
@abstractmethod
|
||||
def get_all(self, filters: dict = None) -> Optional[list[MemoryItem]]: ...
|
||||
|
||||
@abstractmethod
|
||||
def get_last_n(self, last_rounds, ...) -> Optional[list[MemoryItem]]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def add(self, memory_item: MemoryItem, ...): ...
|
||||
|
||||
@abstractmethod
|
||||
def search(self, query, ...) -> Optional[list[MemoryItem]]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def trigger_short_term_memory_to_long_term(self, params: LongTermMemoryTriggerParams, ...): ...
|
||||
|
||||
@abstractmethod
|
||||
async def retrival_user_profile(self, user_id: str, user_input: str, ...) -> Optional[list[UserProfile]]: ...
|
||||
|
||||
@abstractmethod
|
||||
async def retrival_agent_experience(self, agent_id: str, user_input: str, ...) -> Optional[list[AgentExperience]]: ...
|
||||
|
||||
@abstractmethod
|
||||
def update(self, memory_item: MemoryItem): ...
|
||||
|
||||
@abstractmethod
|
||||
def delete(self, memory_id): ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Initializing Memory
|
||||
|
||||
It is recommended to use `MemoryFactory` to initialize and access Memory instances.
|
||||
|
||||
```python
|
||||
from aworld.memory.main import MemoryFactory
|
||||
from aworld.core.memory import MemoryConfig, MemoryLLMConfig
|
||||
|
||||
# Simple initialization
|
||||
memory = MemoryFactory.instance()
|
||||
|
||||
# Initialization with LLM configuration
|
||||
MemoryFactory.init(
|
||||
config=MemoryConfig(
|
||||
provider="aworld",
|
||||
llm_config=MemoryLLMConfig(
|
||||
provider="openai",
|
||||
model_name=os.environ["LLM_MODEL_NAME"],
|
||||
api_key=os.environ["LLM_API_KEY"],
|
||||
base_url=os.environ["LLM_BASE_URL"]
|
||||
)
|
||||
)
|
||||
)
|
||||
memory = MemoryFactory.instance()
|
||||
```
|
||||
|
||||
### Using Short-Term Memory
|
||||
|
||||
The following example demonstrates how to use different message types (`MemorySystemMessage`, `MemoryHumanMessage`, `MemoryAIMessage`, `MemoryToolMessage`) to construct a conversation and store it in short-term memory.
|
||||
|
||||
```python
|
||||
import os
|
||||
from aworld.memory.main import MemoryFactory
|
||||
from aworld.memory.models import (
|
||||
MemorySystemMessage,
|
||||
MemoryHumanMessage,
|
||||
MemoryAIMessage,
|
||||
MemoryToolMessage,
|
||||
MessageMetadata,
|
||||
)
|
||||
from aworld.models.model_response import ToolCall
|
||||
from aworld.core.memory import MemoryConfig
|
||||
|
||||
# Example setup: Initialize MemoryFactory if not already done.
|
||||
# This is usually done once at application startup.
|
||||
if not MemoryFactory.is_initialized():
|
||||
# A default configuration for demonstration purposes.
|
||||
# In a real application, you would configure this properly.
|
||||
MemoryFactory.init(
|
||||
config=MemoryConfig(provider="aworld")
|
||||
)
|
||||
|
||||
# 1. Get a Memory instance
|
||||
memory = MemoryFactory.instance()
|
||||
|
||||
# 2. Define common metadata for the conversation
|
||||
metadata = MessageMetadata(
|
||||
user_id="user-123",
|
||||
session_id="session-abc",
|
||||
task_id="task-xyz",
|
||||
agent_id="agent-007",
|
||||
agent_name="ToolAgent"
|
||||
)
|
||||
|
||||
# 3. Create and add different types of messages to build a conversation flow
|
||||
|
||||
# System message to set the agent's context
|
||||
system_message = MemorySystemMessage(
|
||||
content="You are a helpful assistant that can access tools.",
|
||||
metadata=metadata
|
||||
)
|
||||
memory.add(system_message)
|
||||
|
||||
# User's message (Human)
|
||||
human_message = MemoryHumanMessage(
|
||||
content="What's the weather like in London and what is 2+2?",
|
||||
metadata=metadata
|
||||
)
|
||||
memory.add(human_message)
|
||||
|
||||
# AI's response indicating it will use tools
|
||||
ai_message = MemoryAIMessage(
|
||||
content="I can help with that. I'll use my tools to get the weather and perform the calculation.",
|
||||
tool_calls=[
|
||||
ToolCall(id="call_weather_1", function_name="get_weather", function_arguments='{"city": "London"}'),
|
||||
ToolCall(id="call_calc_2", function_name="calculator", function_arguments='{"expression": "2+2"}')
|
||||
],
|
||||
metadata=metadata
|
||||
)
|
||||
memory.add(ai_message)
|
||||
|
||||
# Results from the tool calls
|
||||
tool_message_1 = MemoryToolMessage(
|
||||
tool_call_id="call_weather_1",
|
||||
content='{"temperature": "15°C", "condition": "Cloudy"}',
|
||||
status="success",
|
||||
metadata=metadata
|
||||
)
|
||||
memory.add(tool_message_1)
|
||||
|
||||
tool_message_2 = MemoryToolMessage(
|
||||
tool_call_id="call_calc_2",
|
||||
content='{"result": 4}',
|
||||
status="success",
|
||||
metadata=metadata
|
||||
)
|
||||
memory.add(tool_message_2)
|
||||
|
||||
# 4. Retrieve the conversation history
|
||||
# The memory store is currently in-memory, so we can retrieve all items.
|
||||
conversation_history = memory.get_all(filters={"session_id": "session-abc"})
|
||||
|
||||
print("--- Conversation History ---")
|
||||
for msg in conversation_history:
|
||||
print(f"Role: {msg.role}, Content: {msg.content}")
|
||||
if isinstance(msg, MemoryAIMessage) and msg.tool_calls:
|
||||
for tc in msg.tool_calls:
|
||||
print(f" -> Tool Call: {tc.function_name}({tc.function_arguments})")
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
`MemoryConfig` allows you to integrate different embedding models and vector databases.
|
||||
|
||||
#### Using Custom Embedding and VectorDB
|
||||
|
||||
```python
|
||||
from aworld.core.memory import MemoryConfig, MemoryLLMConfig, EmbeddingsConfig, VectorDBConfig
|
||||
|
||||
MemoryFactory.init(
|
||||
config=MemoryConfig(
|
||||
provider="aworld",
|
||||
llm_config=MemoryLLMConfig(
|
||||
provider="openai",
|
||||
model_name=os.environ["LLM_MODEL_NAME"],
|
||||
api_key=os.environ["LLM_API_KEY"],
|
||||
base_url=os.environ["LLM_BASE_URL"]
|
||||
),
|
||||
embedding_config=EmbeddingsConfig(
|
||||
provider="ollama", # or huggingface, openai, etc.
|
||||
base_url="http://localhost:11434",
|
||||
model_name="nomic-embed-text"
|
||||
),
|
||||
vector_store_config=VectorDBConfig(
|
||||
provider="chroma",
|
||||
config={
|
||||
"chroma_data_path": "./chroma_db",
|
||||
"collection_name": "aworld",
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
#### Using PostgreSQL as a Backend
|
||||
|
||||
```python
|
||||
from aworld.memory.db import PostgresMemoryStore
|
||||
|
||||
# Initialize the PostgreSQL store
|
||||
postgres_memory_store = PostgresMemoryStore(db_url=os.getenv("MEMORY_STORE_POSTGRES_DSN"))
|
||||
|
||||
# Pass the custom memory store during Factory initialization
|
||||
MemoryFactory.init(
|
||||
custom_memory_store=postgres_memory_store,
|
||||
config=MemoryConfig(
|
||||
# ... other configurations
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### Agent Memory Configuration
|
||||
|
||||
You can fine-tune an agent's memory behavior using `AgentMemoryConfig`. After defining the configuration, pass it to the `Agent` instance during initialization.
|
||||
|
||||
```python
|
||||
import os
|
||||
from aworld.agents.llm_agent import Agent
|
||||
from aworld.config import AgentConfig
|
||||
from aworld.core.memory import AgentMemoryConfig, LongTermConfig
|
||||
|
||||
# 1. Define the AgentMemoryConfig
|
||||
agent_memory_config = AgentMemoryConfig(
|
||||
# Enable short-term memory summarization
|
||||
enable_summary=True,
|
||||
summary_rounds=5,
|
||||
summary_context_length=8000,
|
||||
|
||||
# Keep the last 20 rounds of conversation
|
||||
trim_rounds=20,
|
||||
|
||||
# Enable long-term memory to store user profiles and agent experiences
|
||||
enable_long_term=True,
|
||||
long_term_config=LongTermConfig.create_simple_config(
|
||||
application_id="my-awesome-app",
|
||||
enable_user_profiles=True,
|
||||
enable_agent_experiences=True
|
||||
)
|
||||
)
|
||||
|
||||
# 2. Define the agent's main configuration using AgentConfig
|
||||
# (Ensure your environment variables for the LLM are set)
|
||||
agent_conf = AgentConfig(
|
||||
llm_provider="openai",
|
||||
llm_model_name=os.environ.get("LLM_MODEL_NAME", "gpt-4"),
|
||||
llm_api_key=os.environ.get("LLM_API_KEY"),
|
||||
llm_base_url=os.environ.get("LLM_BASE_URL")
|
||||
)
|
||||
|
||||
# 3. Initialize the Agent, passing the memory configuration
|
||||
my_memory_agent = Agent(
|
||||
conf=agent_conf,
|
||||
name="MyMemoryAgent",
|
||||
system_prompt="You are an agent with advanced memory capabilities.",
|
||||
agent_memory_config=agent_memory_config # Pass the config here
|
||||
)
|
||||
|
||||
# Now, `my_memory_agent` will use the specified memory settings when it runs.
|
||||
# For example, it will automatically create summaries and extract long-term memories.
|
||||
```
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
"""
|
||||
Database implementations for memory storage.
|
||||
"""
|
||||
|
||||
from .postgres import PostgresMemoryStore
|
||||
from .sqlite import SQLiteMemoryStore
|
||||
|
||||
__all__ = [
|
||||
"PostgresMemoryStore",
|
||||
"SQLiteMemoryStore"
|
||||
]
|
||||
@@ -0,0 +1,362 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from typing import Optional
|
||||
|
||||
import pytz # Add pytz for timezone handling
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aworld.core.memory import MemoryStore
|
||||
from aworld.memory.models import (
|
||||
MemoryItem, MemoryAIMessage, MemoryHumanMessage, MemorySummary, MemorySystemMessage, MemoryToolMessage,
|
||||
MessageMetadata,
|
||||
UserProfile, AgentExperience, ConversationSummary
|
||||
)
|
||||
from aworld.models.model_response import ToolCall
|
||||
|
||||
try:
|
||||
from sqlalchemy.orm import declarative_base
|
||||
|
||||
Base = declarative_base()
|
||||
except ImportError:
|
||||
print("SQLAlchemy is not installed. Please install it to use PostgresMemoryStore.")
|
||||
# Get local timezone
|
||||
LOCAL_TZ = pytz.timezone('Asia/Shanghai') # Default to China timezone
|
||||
|
||||
def to_local_time(dt: datetime) -> str:
|
||||
"""Convert UTC datetime to local timezone string."""
|
||||
if not dt:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
dt = pytz.utc.localize(dt)
|
||||
return dt.astimezone(LOCAL_TZ).isoformat()
|
||||
|
||||
def from_iso_time(iso_str: str) -> datetime:
|
||||
"""Convert ISO format string to UTC datetime."""
|
||||
if not iso_str:
|
||||
return datetime.now(pytz.utc)
|
||||
try:
|
||||
dt = datetime.fromisoformat(iso_str)
|
||||
if dt.tzinfo is None:
|
||||
dt = LOCAL_TZ.localize(dt)
|
||||
return dt.astimezone(pytz.utc)
|
||||
except ValueError:
|
||||
return datetime.now(pytz.utc)
|
||||
|
||||
class MemoryItemModel(Base):
|
||||
from sqlalchemy import Column, String, DateTime, Boolean, Integer, Index
|
||||
from sqlalchemy.dialects.postgresql import ARRAY, JSONB
|
||||
|
||||
"""SQLAlchemy model for memory items."""
|
||||
__tablename__ = 'aworld_memory_items'
|
||||
|
||||
id = Column(String, primary_key=True)
|
||||
content = Column(JSONB) # Using JSONB for better performance
|
||||
created_at = Column(DateTime(timezone=True))
|
||||
updated_at = Column(DateTime(timezone=True))
|
||||
memory_meta = Column(JSONB) # Renamed from metadata to memory_meta
|
||||
tags = Column(ARRAY(String))
|
||||
memory_type = Column(String)
|
||||
version = Column(Integer)
|
||||
deleted = Column(Boolean, default=False)
|
||||
|
||||
# Create indexes
|
||||
__table_args__ = (
|
||||
Index('idx_memory_items_meta', memory_meta, postgresql_using='gin'),
|
||||
Index('idx_memory_items_tags', tags, postgresql_using='gin'),
|
||||
Index('idx_memory_items_type', memory_type),
|
||||
Index('idx_memory_items_created', created_at),
|
||||
)
|
||||
|
||||
class MemoryHistoryModel(Base):
|
||||
"""SQLAlchemy model for memory history."""
|
||||
__tablename__ = 'aworld_memory_histories'
|
||||
from sqlalchemy import Column, String, DateTime, ForeignKey
|
||||
|
||||
memory_id = Column(String, ForeignKey('aworld_memory_items.id'), primary_key=True)
|
||||
history_id = Column(String, ForeignKey('aworld_memory_items.id'), primary_key=True)
|
||||
created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
|
||||
|
||||
|
||||
def orm_to_memory_item(orm_item: MemoryItemModel) -> Optional[MemoryItem]:
|
||||
"""Convert ORM model to MemoryItem."""
|
||||
if not orm_item:
|
||||
return None
|
||||
|
||||
memory_meta = orm_item.memory_meta or {}
|
||||
role = memory_meta.get('role')
|
||||
message_type = orm_item.memory_type
|
||||
|
||||
base_data = {
|
||||
'id': orm_item.id,
|
||||
'created_at': to_local_time(orm_item.created_at), # Convert to local time
|
||||
'updated_at': to_local_time(orm_item.updated_at), # Convert to local time
|
||||
'tags': orm_item.tags or [],
|
||||
'version': orm_item.version,
|
||||
'deleted': orm_item.deleted
|
||||
}
|
||||
|
||||
if role == 'system':
|
||||
return MemorySystemMessage(
|
||||
content=orm_item.content,
|
||||
metadata=MessageMetadata(**memory_meta),
|
||||
**base_data
|
||||
)
|
||||
elif role == 'user':
|
||||
return MemoryHumanMessage(
|
||||
metadata=MessageMetadata(**memory_meta),
|
||||
content=orm_item.content,
|
||||
**base_data
|
||||
)
|
||||
elif role == 'assistant':
|
||||
tool_calls_jsons = memory_meta.get('tool_calls', [])
|
||||
tool_calls = []
|
||||
for tool_calls_json in tool_calls_jsons:
|
||||
tool_call = ToolCall.from_dict(tool_calls_json)
|
||||
tool_calls.append(tool_call)
|
||||
return MemoryAIMessage(
|
||||
content=orm_item.content,
|
||||
tool_calls=tool_calls,
|
||||
metadata=MessageMetadata(**memory_meta),
|
||||
**base_data
|
||||
)
|
||||
elif role == 'tool':
|
||||
return MemoryToolMessage(
|
||||
tool_call_id=memory_meta.get('tool_call_id'),
|
||||
content=orm_item.content,
|
||||
status=memory_meta.get('status', 'success'),
|
||||
metadata=MessageMetadata(**memory_meta),
|
||||
**base_data
|
||||
)
|
||||
elif message_type == 'user_profile':
|
||||
if not orm_item.content:
|
||||
return None
|
||||
if not isinstance(orm_item.content, dict):
|
||||
return None
|
||||
|
||||
|
||||
return UserProfile(
|
||||
key=orm_item.content.get('key'),
|
||||
value=orm_item.content.get('value'),
|
||||
user_id=orm_item.memory_meta.get('user_id'),
|
||||
metadata=memory_meta,
|
||||
**base_data
|
||||
)
|
||||
elif message_type == 'agent_experience':
|
||||
if not orm_item.content:
|
||||
return None
|
||||
if not isinstance(orm_item.content, dict):
|
||||
return None
|
||||
return AgentExperience(
|
||||
skill=orm_item.content.get('skill'),
|
||||
actions=orm_item.content.get('actions'),
|
||||
agent_id=orm_item.memory_meta.get('agent_id'),
|
||||
metadata=memory_meta
|
||||
)
|
||||
elif message_type == 'summary':
|
||||
if not orm_item.content:
|
||||
return None
|
||||
if not isinstance(orm_item.content, str):
|
||||
return None
|
||||
# Extract item_ids from metadata
|
||||
item_ids = memory_meta.get('item_ids', [])
|
||||
# Create MessageMetadata from memory_meta
|
||||
summary_metadata = MessageMetadata(
|
||||
agent_id=memory_meta.get('agent_id'),
|
||||
agent_name=memory_meta.get('agent_name'),
|
||||
session_id=memory_meta.get('session_id'),
|
||||
task_id=memory_meta.get('task_id'),
|
||||
user_id=memory_meta.get('user_id')
|
||||
)
|
||||
return MemorySummary(
|
||||
item_ids=item_ids,
|
||||
summary=orm_item.content,
|
||||
metadata=summary_metadata,
|
||||
**base_data
|
||||
)
|
||||
elif message_type == 'conversation_summary':
|
||||
if not orm_item.content:
|
||||
return None
|
||||
if not isinstance(orm_item.content, str):
|
||||
return None
|
||||
# Preserve all custom metadata attributes
|
||||
conversation_summary_metadata = MessageMetadata(**memory_meta)
|
||||
return ConversationSummary(
|
||||
user_id=memory_meta.get('user_id'),
|
||||
session_id=memory_meta.get('session_id'),
|
||||
summary=orm_item.content,
|
||||
metadata=conversation_summary_metadata,
|
||||
**base_data
|
||||
)
|
||||
else:
|
||||
return MemoryItem(**{
|
||||
'id': orm_item.id,
|
||||
'content': orm_item.content,
|
||||
'created_at': to_local_time(orm_item.created_at), # Convert to local time
|
||||
'updated_at': to_local_time(orm_item.updated_at), # Convert to local time
|
||||
'metadata': memory_meta, # Map back to metadata for MemoryItem
|
||||
'tags': orm_item.tags or [],
|
||||
'memory_type': orm_item.memory_type,
|
||||
'version': orm_item.version,
|
||||
'deleted': orm_item.deleted
|
||||
})
|
||||
|
||||
|
||||
def memory_item_to_orm(item: MemoryItem) -> MemoryItemModel:
|
||||
"""Convert MemoryItem to ORM model."""
|
||||
# Handle content serialization
|
||||
content = item.content
|
||||
if isinstance(content, BaseModel):
|
||||
content = content.model_dump() # Use model_dump() instead of model_dump_json() for dict conversion
|
||||
|
||||
return MemoryItemModel(
|
||||
id=item.id,
|
||||
content=content, # Use serialized content
|
||||
created_at=from_iso_time(item.created_at), # Convert to UTC
|
||||
updated_at=from_iso_time(item.updated_at), # Convert to UTC
|
||||
memory_meta=item.metadata, # Map from metadata to memory_meta
|
||||
tags=item.tags,
|
||||
memory_type=item.memory_type,
|
||||
version=item.version,
|
||||
deleted=item.deleted
|
||||
)
|
||||
|
||||
|
||||
class PostgresMemoryStore(MemoryStore):
|
||||
"""
|
||||
PostgreSQL implementation of the memory store using SQLAlchemy.
|
||||
|
||||
This class provides a PostgreSQL-based storage backend for the memory system,
|
||||
implementing all required methods from the MemoryStore interface.
|
||||
"""
|
||||
|
||||
def __init__(self, db_url: str):
|
||||
"""
|
||||
Initialize PostgreSQL memory store.
|
||||
|
||||
Args:
|
||||
db_url (str): SQLAlchemy database URL
|
||||
Format: postgresql+psycopg2://user:password@host:port/dbname
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
self.engine = create_engine(db_url, echo=False, future=True)
|
||||
Base.metadata.create_all(self.engine)
|
||||
self.Session = sessionmaker(bind=self.engine, expire_on_commit=False)
|
||||
|
||||
def _build_filters(self, query, filters: dict = None):
|
||||
"""Build SQLAlchemy query filters."""
|
||||
if not filters:
|
||||
return query.filter(MemoryItemModel.deleted == False)
|
||||
|
||||
query = query.filter(MemoryItemModel.deleted == False)
|
||||
for key, value in filters.items():
|
||||
if value is not None:
|
||||
if key in ['user_id', 'agent_id', 'session_id', 'task_id', 'agent_name', 'tool_call_id']:
|
||||
query = query.filter(MemoryItemModel.memory_meta[key].astext == value)
|
||||
elif key == 'memory_type':
|
||||
# Handle memory_type as a list or single value
|
||||
if isinstance(value, list):
|
||||
query = query.filter(MemoryItemModel.memory_type.in_(value))
|
||||
else:
|
||||
query = query.filter(MemoryItemModel.memory_type == value)
|
||||
return query
|
||||
|
||||
def add(self, memory_item: MemoryItem):
|
||||
"""Add a new memory item to the store."""
|
||||
with self.Session() as session:
|
||||
orm_item = memory_item_to_orm(memory_item)
|
||||
session.add(orm_item)
|
||||
session.commit()
|
||||
|
||||
def get(self, memory_id) -> Optional[MemoryItem]:
|
||||
"""Get a memory item by ID."""
|
||||
with self.Session() as session:
|
||||
orm_item = session.query(MemoryItemModel).filter_by(
|
||||
id=memory_id, deleted=False
|
||||
).first()
|
||||
return orm_to_memory_item(orm_item)
|
||||
|
||||
def get_first(self, filters: dict = None) -> Optional[MemoryItem]:
|
||||
"""Get the first memory item matching the filters."""
|
||||
with self.Session() as session:
|
||||
query = session.query(MemoryItemModel)
|
||||
query = self._build_filters(query, filters)
|
||||
orm_item = query.order_by(MemoryItemModel.created_at.asc()).first()
|
||||
return orm_to_memory_item(orm_item)
|
||||
|
||||
def total_rounds(self, filters: dict = None) -> int:
|
||||
"""Get total number of memory rounds matching the filters."""
|
||||
with self.Session() as session:
|
||||
query = session.query(MemoryItemModel)
|
||||
query = self._build_filters(query, filters)
|
||||
return query.count()
|
||||
|
||||
def get_all(self, filters: dict = None) -> list[MemoryItem]:
|
||||
"""Get all memory items matching the filters."""
|
||||
with self.Session() as session:
|
||||
query = session.query(MemoryItemModel)
|
||||
query = self._build_filters(query, filters)
|
||||
orm_items = query.order_by(MemoryItemModel.created_at.asc()).all()
|
||||
return [orm_to_memory_item(item) for item in orm_items]
|
||||
|
||||
def get_last_n(self, last_rounds: int, filters: dict = None) -> list[MemoryItem]:
|
||||
"""Get the last N memory rounds matching the filters."""
|
||||
with self.Session() as session:
|
||||
query = session.query(MemoryItemModel)
|
||||
query = self._build_filters(query, filters)
|
||||
orm_items = query.order_by(MemoryItemModel.created_at.desc()).limit(last_rounds).all()
|
||||
return [orm_to_memory_item(item) for item in reversed(orm_items)]
|
||||
|
||||
def update(self, memory_item: MemoryItem):
|
||||
"""Update a memory item."""
|
||||
with self.Session() as session:
|
||||
orm_item = session.query(MemoryItemModel).filter_by(id=memory_item.id).first()
|
||||
if orm_item:
|
||||
orm_item.content = memory_item.content
|
||||
orm_item.created_at = from_iso_time(memory_item.created_at)
|
||||
orm_item.updated_at = from_iso_time(memory_item.updated_at) # Convert to UTC
|
||||
orm_item.memory_meta = memory_item.metadata
|
||||
orm_item.tags = memory_item.tags
|
||||
orm_item.memory_type = memory_item.memory_type
|
||||
orm_item.version = memory_item.version
|
||||
orm_item.deleted = memory_item.deleted
|
||||
session.commit()
|
||||
|
||||
def delete(self, memory_id):
|
||||
"""Soft delete a memory item."""
|
||||
with self.Session() as session:
|
||||
orm_item = session.query(MemoryItemModel).filter_by(id=memory_id).first()
|
||||
if orm_item:
|
||||
orm_item.deleted = True
|
||||
orm_item.updated_at = datetime.now(pytz.utc) # Use UTC time
|
||||
session.commit()
|
||||
|
||||
def delete_items(self, message_types: list[str], session_id: str, task_id: str, filters: dict = None):
|
||||
filters = filters or {}
|
||||
filters['memory_type'] = message_types
|
||||
filters['session_id'] = session_id
|
||||
filters['task_id'] = task_id
|
||||
with self.Session() as session:
|
||||
query = session.query(MemoryItemModel)
|
||||
query = self._build_filters(query, filters)
|
||||
query.update({
|
||||
MemoryItemModel.deleted: True,
|
||||
MemoryItemModel.updated_at: datetime.now(pytz.utc) # Use UTC time
|
||||
})
|
||||
session.commit()
|
||||
|
||||
def history(self, memory_id) -> list[MemoryItem] | None:
|
||||
"""Get the history of a memory item."""
|
||||
with self.Session() as session:
|
||||
history_items = session.query(MemoryItemModel).join(
|
||||
MemoryHistoryModel,
|
||||
MemoryHistoryModel.history_id == MemoryItemModel.id
|
||||
).filter(
|
||||
MemoryHistoryModel.memory_id == memory_id
|
||||
).order_by(MemoryItemModel.created_at.asc()).all()
|
||||
|
||||
if not history_items:
|
||||
return None
|
||||
|
||||
return [orm_to_memory_item(item) for item in history_items]
|
||||
@@ -0,0 +1,423 @@
|
||||
import json
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aworld.core.memory import MemoryStore
|
||||
from aworld.memory.models import (
|
||||
MemoryItem, MemoryAIMessage, MemoryHumanMessage, MemorySummary,
|
||||
MemorySystemMessage, MemoryToolMessage, MessageMetadata,
|
||||
UserProfile, AgentExperience, ConversationSummary
|
||||
)
|
||||
from aworld.models.model_response import ToolCall
|
||||
|
||||
|
||||
class SQLiteMemoryStore(MemoryStore):
|
||||
"""
|
||||
SQLite implementation of the memory store.
|
||||
|
||||
This class provides a SQLite-based storage backend for the memory system,
|
||||
implementing all required methods from the MemoryStore interface.
|
||||
"""
|
||||
|
||||
def __init__(self, db_path: str = "./data/aworld_memory.db"):
|
||||
"""
|
||||
Initialize SQLite memory store.
|
||||
|
||||
Args:
|
||||
db_path (str): Path to SQLite database file
|
||||
"""
|
||||
self.db_path = Path(db_path)
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self._init_database()
|
||||
|
||||
def _init_database(self) -> None:
|
||||
"""Initialize database tables and indexes."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS aworld_memory_items (
|
||||
id TEXT PRIMARY KEY,
|
||||
content TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
memory_meta TEXT NOT NULL,
|
||||
tags TEXT NOT NULL,
|
||||
memory_type TEXT NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
deleted BOOLEAN NOT NULL DEFAULT FALSE
|
||||
)
|
||||
""")
|
||||
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS aworld_memory_histories (
|
||||
memory_id TEXT NOT NULL,
|
||||
history_id TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
PRIMARY KEY (memory_id, history_id),
|
||||
FOREIGN KEY (memory_id) REFERENCES aworld_memory_items (id),
|
||||
FOREIGN KEY (history_id) REFERENCES aworld_memory_items (id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Create indexes for better performance
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_items_type ON aworld_memory_items (memory_type)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_items_created ON aworld_memory_items (created_at)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_items_deleted ON aworld_memory_items (deleted)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_items_meta_user_id ON aworld_memory_items (json_extract(memory_meta, '$.user_id'))")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_items_meta_agent_id ON aworld_memory_items (json_extract(memory_meta, '$.agent_id'))")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_items_meta_session_id ON aworld_memory_items (json_extract(memory_meta, '$.session_id'))")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_memory_items_meta_task_id ON aworld_memory_items (json_extract(memory_meta, '$.task_id'))")
|
||||
|
||||
conn.commit()
|
||||
|
||||
def _serialize_content(self, content: Any) -> str:
|
||||
"""Serialize content to JSON string."""
|
||||
if content is None:
|
||||
return ""
|
||||
if isinstance(content, (dict, list, str, int, float, bool)):
|
||||
return json.dumps(content, ensure_ascii=False)
|
||||
if isinstance(content, BaseModel):
|
||||
return content.model_dump_json()
|
||||
return json.dumps(content, ensure_ascii=False, default=str)
|
||||
|
||||
def _deserialize_content(self, content_str: str) -> Any:
|
||||
"""Deserialize content from JSON string."""
|
||||
if not content_str:
|
||||
return None
|
||||
try:
|
||||
return json.loads(content_str)
|
||||
except json.JSONDecodeError:
|
||||
return content_str
|
||||
|
||||
def _serialize_metadata(self, metadata: Dict[str, Any]) -> str:
|
||||
"""Serialize metadata to JSON string."""
|
||||
if not metadata:
|
||||
return "{}"
|
||||
return json.dumps(metadata, ensure_ascii=False)
|
||||
|
||||
def _deserialize_metadata(self, metadata_str: str) -> Dict[str, Any]:
|
||||
"""Deserialize metadata from JSON string."""
|
||||
if not metadata_str:
|
||||
return {}
|
||||
try:
|
||||
return json.loads(metadata_str)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
def _serialize_tags(self, tags: List[str]) -> str:
|
||||
"""Serialize tags list to JSON string."""
|
||||
if not tags:
|
||||
return "[]"
|
||||
return json.dumps(tags, ensure_ascii=False)
|
||||
|
||||
def _deserialize_tags(self, tags_str: str) -> List[str]:
|
||||
"""Deserialize tags from JSON string."""
|
||||
if not tags_str:
|
||||
return []
|
||||
try:
|
||||
return json.loads(tags_str)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
def _memory_item_to_row(self, item: MemoryItem) -> tuple:
|
||||
"""Convert MemoryItem to database row tuple."""
|
||||
content = self._serialize_content(item.content)
|
||||
metadata = self._serialize_metadata(item.metadata)
|
||||
tags = self._serialize_tags(item.tags)
|
||||
|
||||
return (
|
||||
item.id,
|
||||
content,
|
||||
item.created_at or datetime.now().isoformat(),
|
||||
item.updated_at or datetime.now().isoformat(),
|
||||
metadata,
|
||||
tags,
|
||||
item.memory_type,
|
||||
item.version,
|
||||
item.deleted
|
||||
)
|
||||
|
||||
def _row_to_memory_item(self, row: tuple) -> Optional[MemoryItem]:
|
||||
"""Convert database row to MemoryItem."""
|
||||
if not row:
|
||||
return None
|
||||
|
||||
(id_, content, created_at, updated_at, metadata, tags, memory_type, version, deleted) = row
|
||||
|
||||
memory_meta = self._deserialize_metadata(metadata)
|
||||
role = memory_meta.get('role')
|
||||
|
||||
base_data = {
|
||||
'id': id_,
|
||||
'created_at': created_at,
|
||||
'updated_at': updated_at,
|
||||
'tags': self._deserialize_tags(tags),
|
||||
'version': version,
|
||||
'deleted': bool(deleted)
|
||||
}
|
||||
|
||||
# Handle different message types
|
||||
if role == 'system':
|
||||
return MemorySystemMessage(
|
||||
content=self._deserialize_content(content),
|
||||
metadata=MessageMetadata(**memory_meta),
|
||||
**base_data
|
||||
)
|
||||
elif role == 'user':
|
||||
return MemoryHumanMessage(
|
||||
metadata=MessageMetadata(**memory_meta),
|
||||
content=self._deserialize_content(content),
|
||||
**base_data
|
||||
)
|
||||
elif role == 'assistant':
|
||||
tool_calls_jsons = memory_meta.get('tool_calls', [])
|
||||
tool_calls = []
|
||||
for tool_call_json in tool_calls_jsons:
|
||||
tool_call = ToolCall.from_dict(tool_call_json)
|
||||
tool_calls.append(tool_call)
|
||||
return MemoryAIMessage(
|
||||
content=self._deserialize_content(content),
|
||||
tool_calls=tool_calls,
|
||||
metadata=MessageMetadata(**memory_meta),
|
||||
**base_data
|
||||
)
|
||||
elif role == 'tool':
|
||||
return MemoryToolMessage(
|
||||
tool_call_id=memory_meta.get('tool_call_id'),
|
||||
content=self._deserialize_content(content),
|
||||
status=memory_meta.get('status', 'success'),
|
||||
metadata=MessageMetadata(**memory_meta),
|
||||
**base_data
|
||||
)
|
||||
elif memory_type == 'user_profile':
|
||||
content_data = self._deserialize_content(content)
|
||||
if not content_data or not isinstance(content_data, dict):
|
||||
return None
|
||||
return UserProfile(
|
||||
key=content_data.get('key'),
|
||||
value=content_data.get('value'),
|
||||
user_id=memory_meta.get('user_id'),
|
||||
metadata=memory_meta,
|
||||
**base_data
|
||||
)
|
||||
elif memory_type == 'agent_experience':
|
||||
content_data = self._deserialize_content(content)
|
||||
if not content_data or not isinstance(content_data, dict):
|
||||
return None
|
||||
return AgentExperience(
|
||||
skill=content_data.get('skill'),
|
||||
actions=content_data.get('actions'),
|
||||
agent_id=memory_meta.get('agent_id'),
|
||||
metadata=memory_meta,
|
||||
**base_data
|
||||
)
|
||||
elif memory_type == 'summary':
|
||||
content_data = self._deserialize_content(content)
|
||||
if not content_data or not isinstance(content_data, str):
|
||||
return None
|
||||
item_ids = memory_meta.get('item_ids', [])
|
||||
summary_metadata = MessageMetadata(
|
||||
agent_id=memory_meta.get('agent_id'),
|
||||
agent_name=memory_meta.get('agent_name'),
|
||||
session_id=memory_meta.get('session_id'),
|
||||
task_id=memory_meta.get('task_id'),
|
||||
user_id=memory_meta.get('user_id')
|
||||
)
|
||||
return MemorySummary(
|
||||
item_ids=item_ids,
|
||||
summary=content_data,
|
||||
metadata=summary_metadata,
|
||||
**base_data
|
||||
)
|
||||
elif memory_type == 'conversation_summary':
|
||||
content_data = self._deserialize_content(content)
|
||||
if not content_data or not isinstance(content_data, str):
|
||||
return None
|
||||
# Preserve all custom metadata attributes
|
||||
conversation_summary_metadata = MessageMetadata(**memory_meta)
|
||||
return ConversationSummary(
|
||||
user_id=memory_meta.get('user_id'),
|
||||
session_id=memory_meta.get('session_id'),
|
||||
summary=content_data,
|
||||
metadata=conversation_summary_metadata,
|
||||
**base_data
|
||||
)
|
||||
else:
|
||||
return MemoryItem(
|
||||
content=self._deserialize_content(content),
|
||||
metadata=memory_meta,
|
||||
memory_type=memory_type,
|
||||
**base_data
|
||||
)
|
||||
|
||||
def _build_filters(self, filters: Dict[str, Any] = None) -> tuple[str, tuple]:
|
||||
"""Build SQL WHERE clause and parameters from filters."""
|
||||
if not filters:
|
||||
return "WHERE deleted = FALSE", ()
|
||||
|
||||
conditions = ["deleted = FALSE"]
|
||||
params = []
|
||||
|
||||
for key, value in filters.items():
|
||||
if value is not None:
|
||||
if key in ['user_id', 'agent_id', 'session_id', 'task_id', 'agent_name', 'tool_call_id']:
|
||||
conditions.append(f"json_extract(memory_meta, '$.{key}') = ?")
|
||||
params.append(value)
|
||||
elif key == 'memory_type':
|
||||
if isinstance(value, list):
|
||||
placeholders = ','.join(['?' for _ in value])
|
||||
conditions.append(f"memory_type IN ({placeholders})")
|
||||
params.extend(value)
|
||||
else:
|
||||
conditions.append("memory_type = ?")
|
||||
params.append(value)
|
||||
|
||||
where_clause = "WHERE " + " AND ".join(conditions)
|
||||
return where_clause, tuple(params)
|
||||
|
||||
def add(self, memory_item: MemoryItem) -> None:
|
||||
"""Add a new memory item to the store."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
row = self._memory_item_to_row(memory_item)
|
||||
conn.execute("""
|
||||
INSERT INTO aworld_memory_items
|
||||
(id, content, created_at, updated_at, memory_meta, tags, memory_type, version, deleted)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", row)
|
||||
conn.commit()
|
||||
|
||||
def get(self, memory_id: str) -> Optional[MemoryItem]:
|
||||
"""Get a memory item by ID."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.execute("""
|
||||
SELECT id, content, created_at, updated_at, memory_meta, tags, memory_type, version, deleted
|
||||
FROM aworld_memory_items
|
||||
WHERE id = ? AND deleted = FALSE
|
||||
""", (memory_id,))
|
||||
row = cursor.fetchone()
|
||||
return self._row_to_memory_item(row)
|
||||
|
||||
def get_first(self, filters: Dict[str, Any] = None) -> Optional[MemoryItem]:
|
||||
"""Get the first memory item matching the filters."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
where_clause, params = self._build_filters(filters)
|
||||
cursor = conn.execute(f"""
|
||||
SELECT id, content, created_at, updated_at, memory_meta, tags, memory_type, version, deleted
|
||||
FROM aworld_memory_items
|
||||
{where_clause}
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 1
|
||||
""", params)
|
||||
row = cursor.fetchone()
|
||||
return self._row_to_memory_item(row)
|
||||
|
||||
def total_rounds(self, filters: Dict[str, Any] = None) -> int:
|
||||
"""Get total number of memory rounds matching the filters."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
where_clause, params = self._build_filters(filters)
|
||||
cursor = conn.execute(f"""
|
||||
SELECT COUNT(*) FROM aworld_memory_items {where_clause}
|
||||
""", params)
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
def get_all(self, filters: Dict[str, Any] = None) -> List[MemoryItem]:
|
||||
"""Get all memory items matching the filters."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
where_clause, params = self._build_filters(filters)
|
||||
cursor = conn.execute(f"""
|
||||
SELECT id, content, created_at, updated_at, memory_meta, tags, memory_type, version, deleted
|
||||
FROM aworld_memory_items
|
||||
{where_clause}
|
||||
ORDER BY created_at ASC
|
||||
""", params)
|
||||
rows = cursor.fetchall()
|
||||
return [self._row_to_memory_item(row) for row in rows if row]
|
||||
|
||||
def get_last_n(self, last_rounds: int, filters: Dict[str, Any] = None) -> List[MemoryItem]:
|
||||
"""Get the last N memory rounds matching the filters."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
where_clause, params = self._build_filters(filters)
|
||||
cursor = conn.execute(f"""
|
||||
SELECT id, content, created_at, updated_at, memory_meta, tags, memory_type, version, deleted
|
||||
FROM aworld_memory_items
|
||||
{where_clause}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
""", params + (last_rounds,))
|
||||
rows = cursor.fetchall()
|
||||
# Reverse to maintain chronological order
|
||||
return [self._row_to_memory_item(row) for row in reversed(rows) if row]
|
||||
|
||||
def update(self, memory_item: MemoryItem) -> None:
|
||||
"""Update a memory item."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
row = self._memory_item_to_row(memory_item)
|
||||
conn.execute("""
|
||||
UPDATE aworld_memory_items
|
||||
SET content = ?, created_at = ?, updated_at = ?, memory_meta = ?,
|
||||
tags = ?, memory_type = ?, version = ?, deleted = ?
|
||||
WHERE id = ?
|
||||
""", row[1:] + (memory_item.id,))
|
||||
conn.commit()
|
||||
|
||||
def delete(self, memory_id: str) -> None:
|
||||
"""Soft delete a memory item."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute("""
|
||||
UPDATE aworld_memory_items
|
||||
SET deleted = TRUE, updated_at = ?
|
||||
WHERE id = ?
|
||||
""", (datetime.now().isoformat(), memory_id))
|
||||
conn.commit()
|
||||
|
||||
def delete_items(self, message_types: List[str], session_id: str, task_id: str, filters: Dict[str, Any] = None) -> None:
|
||||
"""Delete multiple memory items by message types, session_id, and task_id."""
|
||||
filters = filters or {}
|
||||
filters['memory_type'] = message_types
|
||||
filters['session_id'] = session_id
|
||||
filters['task_id'] = task_id
|
||||
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
where_clause, params = self._build_filters(filters)
|
||||
# Remove the "WHERE" keyword and convert to proper WHERE clause for UPDATE
|
||||
where_conditions = where_clause.replace('WHERE ', '')
|
||||
conn.execute(f"""
|
||||
UPDATE aworld_memory_items
|
||||
SET deleted = TRUE, updated_at = ?
|
||||
WHERE {where_conditions}
|
||||
""", (datetime.now().isoformat(),) + params)
|
||||
conn.commit()
|
||||
|
||||
def history(self, memory_id: str) -> Optional[List[MemoryItem]]:
|
||||
"""Get the history of a memory item."""
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.execute("""
|
||||
SELECT m.id, m.content, m.created_at, m.updated_at, m.memory_meta,
|
||||
m.tags, m.memory_type, m.version, m.deleted
|
||||
FROM aworld_memory_items m
|
||||
JOIN aworld_memory_histories h ON h.history_id = m.id
|
||||
WHERE h.memory_id = ? AND m.deleted = FALSE
|
||||
ORDER BY m.created_at ASC
|
||||
""", (memory_id,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
return [self._row_to_memory_item(row) for row in rows if row]
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close database connections."""
|
||||
# SQLite connections are automatically closed when exiting context managers
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry."""
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Context manager exit."""
|
||||
self.close()
|
||||
@@ -0,0 +1,97 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from aworld.core.memory import EmbeddingsConfig
|
||||
|
||||
|
||||
class EmbeddingsMetadata(BaseModel):
|
||||
memory_id: str = Field(..., description="memory_id")
|
||||
agent_id: Optional[str] = Field(default=None, description="agent_id")
|
||||
session_id: Optional[str] = Field(default=None, description="session_id")
|
||||
task_id: Optional[str] = Field(default=None, description="task_id")
|
||||
user_id: Optional[str] = Field(default=None, description="user_id")
|
||||
application_id: Optional[str] = Field(default=None, description="application_id")
|
||||
memory_type: str = Field(..., description="memory_type")
|
||||
embedding_model: str = Field(..., description="Embedding model")
|
||||
created_at: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Created at")
|
||||
updated_at: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Updated at")
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
class EmbeddingsResult(BaseModel):
|
||||
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="ID")
|
||||
embedding: Optional[list[float]] = Field(default=None, description="Embedding")
|
||||
content: str = Field(..., description="Content")
|
||||
metadata: Optional[EmbeddingsMetadata] = Field(..., description="Metadata")
|
||||
score: Optional[float] = Field(default=None, description="Retrieved relevance score")
|
||||
|
||||
class EmbeddingsResults(BaseModel):
|
||||
docs: Optional[List[EmbeddingsResult]]
|
||||
retrieved_at: int = Field(..., description="Retrieved at")
|
||||
|
||||
class Embeddings(ABC):
|
||||
"""Interface for embedding models.
|
||||
Embeddings are used to convert artifacts and queries into a vector space.
|
||||
"""
|
||||
@abstractmethod
|
||||
def embed_query(self, text: str) -> list[float]:
|
||||
"""Embed query text."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_embed_query(self, text: str) -> list[float]:
|
||||
"""Asynchronous Embed query text."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class EmbeddingsBase(Embeddings):
|
||||
"""
|
||||
Base class for embedding implementations that contains common functionality.
|
||||
"""
|
||||
|
||||
def __init__(self, config: EmbeddingsConfig):
|
||||
"""
|
||||
Initialize EmbeddingsBase with configuration.
|
||||
Args:
|
||||
config (EmbeddingsConfig): Configuration for embedding model and API.
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
"""
|
||||
Abstract method to embed a query string.
|
||||
Args:
|
||||
text (str): Text to embed.
|
||||
Returns:
|
||||
List[float]: Embedding vector.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def async_embed_query(self, text: str) -> List[float]:
|
||||
"""
|
||||
Abstract method to asynchronously embed a query string.
|
||||
Args:
|
||||
text (str): Text to embed.
|
||||
Returns:
|
||||
List[float]: Embedding vector.
|
||||
"""
|
||||
pass
|
||||
|
||||
class EmbeddingFactory:
|
||||
|
||||
@staticmethod
|
||||
def get_embedder(config: EmbeddingsConfig) -> Embeddings:
|
||||
if config.provider == "openai":
|
||||
from aworld.memory.embeddings.openai_compatible import OpenAICompatibleEmbeddings
|
||||
return OpenAICompatibleEmbeddings(config)
|
||||
elif config.provider == "ollama":
|
||||
from aworld.memory.embeddings.ollama import OllamaEmbeddings
|
||||
return OllamaEmbeddings(config)
|
||||
else:
|
||||
raise ValueError(f"Unsupported embedding provider: {config.provider}")
|
||||
@@ -0,0 +1,20 @@
|
||||
from typing import Optional
|
||||
|
||||
from aworld.core.memory import EmbeddingsConfig
|
||||
from aworld.memory.embeddings.base import Embeddings
|
||||
|
||||
|
||||
class EmbedderFactory:
|
||||
|
||||
@staticmethod
|
||||
def get_embedder(config: EmbeddingsConfig) -> Optional[Embeddings]:
|
||||
if not config:
|
||||
return None
|
||||
if config.provider == "openai":
|
||||
from aworld.memory.embeddings.openai_compatible import OpenAICompatibleEmbeddings
|
||||
return OpenAICompatibleEmbeddings(config)
|
||||
elif config.provider == "ollama":
|
||||
from aworld.memory.embeddings.ollama import OllamaEmbeddings
|
||||
return OllamaEmbeddings(config)
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {config.provider}")
|
||||
@@ -0,0 +1,82 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
import aiohttp
|
||||
import requests
|
||||
|
||||
from aworld.core.memory import EmbeddingsConfig
|
||||
from aworld.memory.embeddings.base import EmbeddingsBase
|
||||
|
||||
|
||||
class OllamaEmbeddings(EmbeddingsBase):
|
||||
"""
|
||||
Embedding implementation using Ollama HTTP API.
|
||||
"""
|
||||
def __init__(self, config: EmbeddingsConfig):
|
||||
"""
|
||||
Initialize OllamaEmbeddings with configuration.
|
||||
Args:
|
||||
config (EmbeddingsConfig): Configuration for embedding model and API.
|
||||
"""
|
||||
super().__init__(config)
|
||||
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
"""
|
||||
Embed a query string using Ollama HTTP API.
|
||||
Args:
|
||||
text (str): Text to embed.
|
||||
Returns:
|
||||
List[float]: Embedding vector.
|
||||
"""
|
||||
url = self.config.base_url.rstrip('/') + "/api/embed"
|
||||
payload = {
|
||||
"model": self.config.model_name,
|
||||
"input": text
|
||||
}
|
||||
try:
|
||||
response = requests.post(url, json=payload, timeout=self.config.timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
# Ollama returns {"embedding": [...], ...}
|
||||
logging.debug(f"Ollama embedding response: {data}")
|
||||
return self.resolve_embedding(data)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise RuntimeError(f"Ollama embedding API error: {e}")
|
||||
|
||||
async def async_embed_query(self, text: str) -> List[float]:
|
||||
"""
|
||||
Asynchronously embed a query string using Ollama HTTP API.
|
||||
Args:
|
||||
text (str): Text to embed.
|
||||
Returns:
|
||||
List[float]: Embedding vector.
|
||||
"""
|
||||
url = self.config.base_url.rstrip('/') + "/api/embed"
|
||||
payload = {
|
||||
"model": self.config.model_name,
|
||||
"input": text
|
||||
}
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.config.timeout)) as session:
|
||||
async with session.post(url, json=payload) as resp:
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
return self.resolve_embedding(data)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Ollama async embedding API error: {e}")
|
||||
|
||||
@staticmethod
|
||||
def resolve_embedding(data: dict) -> List[float]:
|
||||
"""
|
||||
Resolve the embedding from the response data.
|
||||
Args:
|
||||
data (dict): Response data from Ollama API.
|
||||
Returns:
|
||||
List[float]: Embedding vector.
|
||||
"""
|
||||
if "embeddings" in data and len(data["embeddings"]) > 0:
|
||||
return data["embeddings"][0]
|
||||
else:
|
||||
return None
|
||||
@@ -0,0 +1,79 @@
|
||||
import asyncio
|
||||
import time
|
||||
import logging
|
||||
from typing import Any, List
|
||||
|
||||
from openai import OpenAI
|
||||
|
||||
from aworld.core.memory import EmbeddingsConfig
|
||||
from aworld.memory.embeddings.base import EmbeddingsBase
|
||||
|
||||
|
||||
class OpenAICompatibleEmbeddings(EmbeddingsBase):
|
||||
"""
|
||||
OpenAI compatible embeddings using OpenAI-compatible HTTP API.
|
||||
|
||||
- text-embedding-v4: [2048、1536、1024(默认)、768、512、256、128、64]
|
||||
- text-embedding-v3: [1024(默认)、512、256、128、64]
|
||||
- text-embedding-v2: [1536]
|
||||
- text-embedding-v1: [1536]
|
||||
"""
|
||||
|
||||
def __init__(self, config: EmbeddingsConfig):
|
||||
"""
|
||||
Initialize OpenAICompatibleEmbeddings with configuration.
|
||||
Args:
|
||||
config (EmbeddingsConfig): Configuration for embedding model and API.
|
||||
"""
|
||||
super().__init__(config)
|
||||
self.client = OpenAI(api_key=config.api_key, base_url=config.base_url)
|
||||
|
||||
|
||||
def embed_query(self, text: str) -> List[float]:
|
||||
"""
|
||||
Embed a query string using OpenAI-compatible HTTP API.
|
||||
Args:
|
||||
text (str): Text to embed.
|
||||
Returns:
|
||||
List[float]: Embedding vector.
|
||||
"""
|
||||
try:
|
||||
response = self.client.embeddings.create(
|
||||
model=self.config.model_name,
|
||||
input=text,
|
||||
dimensions=self.config.dimensions)
|
||||
data = response.data
|
||||
logging.debug(f"OpenAI embedding response: {data}")
|
||||
return self.resolve_embedding(data)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"OpenAI embedding API error: {e}")
|
||||
|
||||
async def async_embed_query(self, text: str) -> List[float]:
|
||||
"""
|
||||
Asynchronously embed a query string using OpenAI-compatible HTTP API.
|
||||
Args:
|
||||
text (str): Text to embed.
|
||||
Returns:
|
||||
List[float]: Embedding vector.
|
||||
"""
|
||||
try:
|
||||
response = self.client.embeddings.create(
|
||||
model=self.config.model_name,
|
||||
input=text,
|
||||
dimensions=self.config.dimensions)
|
||||
data = response.data
|
||||
logging.debug(f"OpenAI embedding response: {data}")
|
||||
return self.resolve_embedding(data)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"OpenAI async embedding API error: {e}")
|
||||
|
||||
@staticmethod
|
||||
def resolve_embedding(data: list[Any]) -> List[float]:
|
||||
"""
|
||||
Resolve the embedding from the response data (OpenAI format).
|
||||
Args:
|
||||
data (dict): Response data from OpenAI API.
|
||||
Returns:
|
||||
List[float]: Embedding vector.
|
||||
"""
|
||||
return data[0].embedding
|
||||
@@ -0,0 +1,19 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
|
||||
from .base import MemoryOrchestrator, MemoryGungnir, MemoryProcessingTask, MemoryProcessingResult
|
||||
from aworld.core.memory import LongTermConfig, TriggerConfig, ExtractionConfig, StorageConfig, ProcessingConfig
|
||||
from .default import DefaultMemoryOrchestrator
|
||||
|
||||
__all__ = [
|
||||
"MemoryOrchestrator",
|
||||
"MemoryGungnir",
|
||||
"LongTermConfig",
|
||||
"TriggerConfig",
|
||||
"ExtractionConfig",
|
||||
"StorageConfig",
|
||||
"ProcessingConfig",
|
||||
"MemoryProcessingTask",
|
||||
"MemoryProcessingResult",
|
||||
"DefaultMemoryOrchestrator"
|
||||
]
|
||||
@@ -0,0 +1,159 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
|
||||
import uuid
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import List, Any, Optional, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from aworld.core.memory import MemoryStore, LongTermConfig
|
||||
from aworld.models.llm import LLMModel
|
||||
from aworld.memory.models import UserProfile, AgentExperience, LongTermExtractParams
|
||||
|
||||
|
||||
class MemoryProcessingResult(BaseModel):
|
||||
"""
|
||||
Represents the result of memory processing operation.
|
||||
"""
|
||||
task_id: str = Field(default=None, description="Task identifier")
|
||||
success: bool = Field(default=False, description="Success flag")
|
||||
user_profiles: Optional[List[UserProfile]] = Field(default_factory=list, description="User profiles")
|
||||
agent_experiences: Optional[List[AgentExperience]] = Field(default_factory=list, description="Agent experiences")
|
||||
finished_at: Optional[str] = Field(default=str(datetime.now().isoformat()), description="Finished timestamp")
|
||||
error_message: Optional[str] = Field(default=None, description="Error message")
|
||||
|
||||
class MemoryProcessingTask(BaseModel):
|
||||
"""
|
||||
Represents a memory processing task containing information needed for long-term memory processing.
|
||||
|
||||
Args:
|
||||
memory_task_id: Task identifier
|
||||
task_type: Task type
|
||||
extract_params: Long-term extract parameters
|
||||
created_at: Creation timestamp
|
||||
finished_at: Finished timestamp
|
||||
"""
|
||||
memory_task_id: str = Field(default=str(uuid.uuid4()), description="Memory task identifier")
|
||||
task_type: Literal['user_profile', 'agent_experience'] = Field(..., description="Memory task type")
|
||||
extract_params: LongTermExtractParams = Field(description="Long-term extract parameters")
|
||||
metadata: dict[str, Any] = Field(default_factory=dict, description="Metadata")
|
||||
created_at: str = Field(default_factory=lambda: datetime.now().isoformat(), description="Creation timestamp")
|
||||
finished_at: str = Field(default=None, description="Finished timestamp")
|
||||
status: Literal['initial', 'processing', 'completed', 'failed'] = Field(default='initial', description="Task status")
|
||||
result: Optional[MemoryProcessingResult] = Field(default=None, description="Processing result")
|
||||
longterm_config: LongTermConfig = Field(description="Long-term memory configuration")
|
||||
|
||||
class MemoryOrchestrator(ABC):
|
||||
|
||||
"""
|
||||
Abstract base class for memory orchestrator that determines when and how to process memories.
|
||||
Responsible for evaluating trigger conditions and creating processing tasks.
|
||||
"""
|
||||
|
||||
def __init__(self, llm_instance: LLMModel,
|
||||
longterm_config: LongTermConfig,
|
||||
embedding_model: Optional[Any] = None,
|
||||
long_term_memory_store: MemoryStore = None) -> None:
|
||||
"""
|
||||
Initialize the memory orchestrator.
|
||||
|
||||
Args:
|
||||
llm_instance: LLM model instance for processing
|
||||
"""
|
||||
self._llm_instance = llm_instance
|
||||
self._longterm_config = longterm_config
|
||||
self._embedding_model = embedding_model
|
||||
self._long_term_memory_store: MemoryStore = long_term_memory_store
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def create_longterm_processing_tasks(self,
|
||||
extract_param_list: list[LongTermExtractParams],
|
||||
longterm_config: LongTermConfig
|
||||
) -> None:
|
||||
"""
|
||||
Create long-term memory processing tasks from the given memory items.
|
||||
|
||||
Args:
|
||||
task_params: List of long-term extract parameters
|
||||
longterm_config: Long-term memory configuration settings
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def retrieve_agent_experience(
|
||||
self,
|
||||
query: str,
|
||||
agent_id: Optional[str] = None,
|
||||
application_id: Optional[str] = "default",
|
||||
) -> List[AgentExperience]:
|
||||
"""
|
||||
Retrieve similar agent experiences from long-term storage for context.
|
||||
|
||||
Args:
|
||||
query: Query string for similarity search
|
||||
agent_id: Agent identifier for filtering
|
||||
application_id: Application identifier for filtering
|
||||
|
||||
Returns:
|
||||
List of similar memory items
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def retrieve_user_profile(
|
||||
self,
|
||||
query: str,
|
||||
user_id: Optional[str] = None,
|
||||
application_id: Optional[str] = "default",
|
||||
) -> List[UserProfile]:
|
||||
"""
|
||||
Retrieve similar user profiles from long-term storage for context.
|
||||
|
||||
Args:
|
||||
query: Query string for similarity search
|
||||
user_id: User identifier for filtering
|
||||
application_id: Application identifier for filtering
|
||||
|
||||
Returns:
|
||||
List of similar memory items
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MemoryGungnir(ABC):
|
||||
"""
|
||||
Abstract base class for memory processing engine (Gungnir - the eternal spear of memory).
|
||||
Responsible for extracting and processing long-term memories from short-term memory items.
|
||||
"""
|
||||
|
||||
def __init__(self, llm_instance: LLMModel) -> None:
|
||||
"""
|
||||
Initialize the memory processing engine.
|
||||
|
||||
Args:
|
||||
llm_instance: LLM model instance for processing
|
||||
"""
|
||||
self._llm_instance = llm_instance
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def process_memory_task(
|
||||
self,
|
||||
task: MemoryProcessingTask
|
||||
) -> MemoryProcessingResult:
|
||||
"""
|
||||
Process a memory task and extract long-term memories.
|
||||
|
||||
Args:
|
||||
task: Memory processing task to execute
|
||||
|
||||
Returns:
|
||||
Processing result containing extracted memories
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import asyncio
|
||||
import json
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Any, List, Literal, Optional, Tuple
|
||||
|
||||
from aworld.core.memory import MemoryItem, LongTermConfig, MemoryStore, MemoryBase
|
||||
from aworld.models.llm import LLMModel, acall_llm_model
|
||||
from .base import MemoryGungnir, MemoryOrchestrator, MemoryProcessingTask, MemoryProcessingResult
|
||||
from ..models import AgentExperience, LongTermExtractParams, UserProfile
|
||||
from ...logs.util import logger
|
||||
|
||||
|
||||
class DefaultMemoryGungnir(MemoryGungnir):
|
||||
"""
|
||||
Default implementation of MemoryGungnir.
|
||||
"""
|
||||
|
||||
def __init__(self, llm_instance: LLMModel):
|
||||
super().__init__(llm_instance)
|
||||
|
||||
async def process_memory_task(self, task: MemoryProcessingTask) -> MemoryProcessingResult:
|
||||
try:
|
||||
return await self._process_memory_task(task)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"🧠 [MEMORY:long-term] Error processing memory task:{task.memory_task_id} failed: {e}" + traceback.format_exc())
|
||||
return MemoryProcessingResult(
|
||||
success=False,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
async def _process_memory_task(self, task: MemoryProcessingTask) -> MemoryProcessingResult:
|
||||
|
||||
logger.debug(
|
||||
f"🧠 [MEMORY:long-term] Processing memory task start:{task.memory_task_id} with task_type:{task.task_type}")
|
||||
# 1. extract long-term memories
|
||||
user_profiles = []
|
||||
agent_experiences = []
|
||||
if task.task_type == "agent_experience":
|
||||
agent_experiences = await self._extract_agent_experience(task)
|
||||
elif task.task_type == "user_profile":
|
||||
user_profiles = await self._extract_user_profile(task)
|
||||
else:
|
||||
raise ValueError(f"Invalid task type: {task.task_type}")
|
||||
|
||||
# 2. return the result
|
||||
result = MemoryProcessingResult(
|
||||
success=True,
|
||||
user_profiles=user_profiles,
|
||||
agent_experiences=agent_experiences,
|
||||
finished_at=datetime.now().isoformat(),
|
||||
)
|
||||
logger.debug(
|
||||
f"🧠 [MEMORY:long-term] Processing memory task end:{task.memory_task_id} with task_type:{task.task_type}")
|
||||
return result
|
||||
|
||||
async def _extract_data_from_llm(self, task: MemoryProcessingTask, prompt: str, parser: callable) -> Optional[List[Any]]:
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
try:
|
||||
llm_response = await acall_llm_model(self._llm_instance, messages=messages)
|
||||
logger.info(f"🧠 [MEMORY:long-term] Extracted data for task {task.memory_task_id}: {llm_response}")
|
||||
result = json.loads(llm_response.content.replace("```json", "").replace("```", ""))
|
||||
parsed_data = parser(result, task)
|
||||
logger.info(f"🧠 [MEMORY:long-term] Parsed data for task {task.memory_task_id}: {parsed_data}")
|
||||
return parsed_data
|
||||
except Exception as e:
|
||||
logger.error(f"🧠 [MEMORY:long-term] Error extracting data for task {task.memory_task_id}: {e}" + traceback.format_exc())
|
||||
return None
|
||||
|
||||
async def _extract_agent_experience(self, task: MemoryProcessingTask) -> Optional[List[AgentExperience]]:
|
||||
to_be_extracted_messages = task.extract_params.to_openai_messages()
|
||||
agent_experiences_prompt = task.longterm_config.get_agent_experience_prompt(
|
||||
messages=str(to_be_extracted_messages))
|
||||
|
||||
def parse_agent_experience(result, task):
|
||||
return [AgentExperience(
|
||||
agent_id=task.extract_params.agent_id,
|
||||
skill=result['skill'],
|
||||
actions=result['actions']
|
||||
)]
|
||||
|
||||
return await self._extract_data_from_llm(task, agent_experiences_prompt, parse_agent_experience)
|
||||
|
||||
async def _extract_user_profile(self, task: MemoryProcessingTask) -> Optional[List[UserProfile]]:
|
||||
to_be_extracted_messages = task.extract_params.to_openai_messages()
|
||||
user_profile_prompt = task.longterm_config.get_user_profile_prompt(
|
||||
messages=str(to_be_extracted_messages))
|
||||
|
||||
def parse_user_profile(result, task):
|
||||
user_profiles = []
|
||||
profile_entries = result if isinstance(result, list) else [result]
|
||||
for profile_entry in profile_entries:
|
||||
if not isinstance(profile_entry, dict) or 'key' not in profile_entry or 'value' not in profile_entry:
|
||||
logger.warning(f"🧠 [MEMORY:long-term] Invalid profile entry format: {profile_entry}")
|
||||
continue
|
||||
user_profiles.append(UserProfile(
|
||||
user_id=task.extract_params.user_id,
|
||||
key=profile_entry['key'],
|
||||
value=profile_entry['value']
|
||||
))
|
||||
return user_profiles
|
||||
|
||||
return await self._extract_data_from_llm(task, user_profile_prompt, parse_user_profile)
|
||||
|
||||
|
||||
class DefaultMemoryOrchestrator(MemoryOrchestrator):
|
||||
"""
|
||||
Simple implementation of MemoryOrchestrator that provides basic memory processing decisions.
|
||||
This orchestrator evaluates trigger conditions and creates processing tasks based on configuration.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
llm_instance: LLMModel,
|
||||
embedding_model: Optional[Any] = None,
|
||||
memory: "MemoryBase" = None
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the simple memory orchestrator.
|
||||
|
||||
Args:
|
||||
llm_instance: LLM model instance for processing
|
||||
"""
|
||||
super().__init__(llm_instance, embedding_model)
|
||||
self.memory_gungnir = DefaultMemoryGungnir(llm_instance)
|
||||
self.memory_tasks: List[MemoryProcessingTask] = []
|
||||
self.memory = memory
|
||||
|
||||
async def create_longterm_processing_tasks(self, task_params: list[LongTermExtractParams],
|
||||
longterm_config: LongTermConfig,
|
||||
force: bool = False) -> None:
|
||||
for task_param in task_params:
|
||||
await self._create_longterm_processing_task(task_param, longterm_config, force)
|
||||
|
||||
async def _create_longterm_processing_task(self, extract_param: LongTermExtractParams,
|
||||
longterm_config: LongTermConfig
|
||||
, force: bool = False) -> None:
|
||||
"""
|
||||
Check if long-term memory processing should be triggered and process if necessary.
|
||||
|
||||
Args:
|
||||
extract_param: Long-term extract parameters
|
||||
longterm_config: Long-term memory configuration settings
|
||||
"""
|
||||
try:
|
||||
# Get all current memory items
|
||||
memory_task = self._create_memory_task(
|
||||
extract_param,
|
||||
longterm_config=longterm_config,
|
||||
force=force
|
||||
)
|
||||
|
||||
if memory_task:
|
||||
logger.info(f"🧠 [MEMORY:long-term] Created processing task {memory_task.memory_task_id} "
|
||||
f"with trigger_reason: {memory_task.metadata.get('trigger_reason', 'unknown')}")
|
||||
await self._add_memory_task(memory_task)
|
||||
if longterm_config.processing.enable_background_processing:
|
||||
asyncio.create_task(self._process_longterm_memory_task(memory_task))
|
||||
else:
|
||||
asyncio.run(self._process_longterm_memory_task(memory_task))
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"🧠 [MEMORY:long-term] Error during long-term memory processing check: {e}" + traceback.format_exc())
|
||||
|
||||
def _should_process_memory(
|
||||
self,
|
||||
extract_param: LongTermExtractParams,
|
||||
longterm_config: LongTermConfig
|
||||
) -> Tuple[bool, str]:
|
||||
|
||||
# 1.Check message count threshold
|
||||
if self._check_message_count_threshold(extract_param.memories, longterm_config):
|
||||
return True, "message_count"
|
||||
|
||||
# 2.Check content importance if enabled
|
||||
if longterm_config.trigger.enable_importance_trigger:
|
||||
if self._check_content_importance(extract_param.memories, longterm_config):
|
||||
return True, "content_importance"
|
||||
|
||||
return False, "not_trigger"
|
||||
|
||||
def _create_memory_task(
|
||||
self,
|
||||
extract_param: LongTermExtractParams,
|
||||
longterm_config: LongTermConfig,
|
||||
force: bool = False
|
||||
) -> Optional[MemoryProcessingTask]:
|
||||
"""
|
||||
Create a memory processing task from the given memory items.
|
||||
|
||||
Args:
|
||||
extract_param: Long-term extract parameters
|
||||
longterm_config: Long-term memory configuration settings
|
||||
|
||||
Returns:
|
||||
Memory processing task
|
||||
"""
|
||||
if not force:
|
||||
# Check if processing should be triggered
|
||||
should_process, reason = self._should_process_memory(
|
||||
extract_param,
|
||||
longterm_config=longterm_config
|
||||
)
|
||||
logger.debug(
|
||||
f"🧠 [MEMORY:long-term] [DefaultMemoryOrchestrator] flag of should_process: {should_process}, reason: {reason}")
|
||||
|
||||
if not should_process:
|
||||
logger.debug(
|
||||
f"🧠 [MEMORY:long-term] [DefaultMemoryOrchestrator] not trigger memory task#{extract_param.extract_type}[{extract_param.session_id}:{extract_param.task_id}]")
|
||||
return None
|
||||
else:
|
||||
reason = "force"
|
||||
|
||||
# create long-term memory task
|
||||
memory_task = MemoryProcessingTask(
|
||||
task_type=extract_param.extract_type,
|
||||
extract_params=extract_param,
|
||||
longterm_config=longterm_config
|
||||
)
|
||||
|
||||
# Add metadata based on configuration
|
||||
memory_task.metadata.update({
|
||||
"trigger_reason": reason,
|
||||
'config_snapshot': {
|
||||
'message_threshold': longterm_config.trigger.message_count_threshold,
|
||||
'user_profile_extraction': longterm_config.extraction.enable_user_profile_extraction,
|
||||
'agent_experience_extraction': longterm_config.extraction.enable_agent_experience_extraction
|
||||
}
|
||||
})
|
||||
|
||||
logger.info(
|
||||
f"🧠 [MEMORY:long-term] [DefaultMemoryOrchestrator] created memory task#{extract_param.extract_type}[{extract_param.session_id}:{extract_param.task_id}]: {memory_task.memory_task_id}, reason: {reason}")
|
||||
return memory_task
|
||||
|
||||
def _check_message_count_threshold(self, memory_items: List[MemoryItem], longterm_config: LongTermConfig) -> bool:
|
||||
"""
|
||||
Check if the message count threshold is reached.
|
||||
|
||||
Args:
|
||||
memory_items: List of memory items to check
|
||||
longterm_config: Long-term memory configuration settings
|
||||
|
||||
Returns:
|
||||
True if threshold is reached, False otherwise
|
||||
"""
|
||||
return len(memory_items) >= longterm_config.trigger.message_count_threshold
|
||||
|
||||
def _check_content_importance(self, memory_items: List[MemoryItem], longterm_config: LongTermConfig) -> bool:
|
||||
"""
|
||||
Check if the content importance threshold is reached.
|
||||
|
||||
Args:
|
||||
memory_items: List of memory items to check
|
||||
longterm_config: Long-term memory configuration settings
|
||||
|
||||
Returns:
|
||||
True if content is important enough, False otherwise
|
||||
"""
|
||||
if not longterm_config.trigger.enable_importance_trigger:
|
||||
return False
|
||||
|
||||
# Check for importance keywords in recent messages
|
||||
recent_items = memory_items[-1:] if len(memory_items) > 1 else memory_items
|
||||
importance_keywords = longterm_config.trigger.importance_keywords
|
||||
|
||||
for item in recent_items:
|
||||
content = item.content.lower()
|
||||
for keyword in importance_keywords:
|
||||
if keyword.lower() in content:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def _process_longterm_memory_task(self, task: MemoryProcessingTask) -> None:
|
||||
"""
|
||||
Process a long-term memory task (placeholder implementation).
|
||||
|
||||
Args:
|
||||
task: MemoryProcessingTask to process
|
||||
"""
|
||||
try:
|
||||
logger.info(f"🧠 [MEMORY:long-term] Processing long-term task {task.memory_task_id} started")
|
||||
# 1. process memory task
|
||||
result = await self.memory_gungnir.process_memory_task(task)
|
||||
|
||||
# 2. store the result
|
||||
await self._store_longterm_memories(result, task)
|
||||
|
||||
logger.info(f"🧠 [MEMORY:long-term] Processing long-term task {task.memory_task_id} completed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"🧠 [MEMORY:long-term] Error processing task#{task.memory_task_id}: {e}" + traceback.format_exc())
|
||||
task.status = "failed"
|
||||
await self._update_task_status(task)
|
||||
|
||||
async def _store_longterm_memories(self, result: MemoryProcessingResult, task: MemoryProcessingTask) -> None:
|
||||
"""
|
||||
Store the long-term memories.
|
||||
"""
|
||||
try:
|
||||
if result.success:
|
||||
# 1. store user profiles
|
||||
await self._handle_store_longterm_memories(result.user_profiles, "user_profile")
|
||||
# 2. store agent experiences
|
||||
await self._handle_store_longterm_memories(result.agent_experiences, "agent_experience")
|
||||
# 3. update the task status
|
||||
task.status = "completed"
|
||||
await self._update_task_status(task)
|
||||
else:
|
||||
logger.error(f"🧠 [MEMORY:long-term] Error storing long-term memories: {result.error_message}")
|
||||
task.status = "failed"
|
||||
await self._update_task_status(task)
|
||||
except Exception as e:
|
||||
logger.error(f"🧠 [MEMORY:long-term] Error storing long-term memories: {e}" + traceback.format_exc())
|
||||
task.status = "failed"
|
||||
await self._update_task_status(task)
|
||||
|
||||
async def _handle_store_longterm_memories(self, memory_items: List[MemoryItem],
|
||||
memory_type: Literal["user_profile", "agent_experience"]) -> None:
|
||||
"""
|
||||
Store the long-term memory_items.
|
||||
|
||||
1. retrieve the long-term memories from the long-term memory store
|
||||
2. compare the memories with the new memories
|
||||
3. if the memories are not in the long-term memory store, store the memories
|
||||
4. if the memories are in the long-term memory store, update the memories
|
||||
"""
|
||||
if not memory_items:
|
||||
logger.debug(f"🧠 [MEMORY:long-term] Storing {memory_type} memories: {memory_items}")
|
||||
return
|
||||
|
||||
for memory_item in memory_items:
|
||||
logger.info(f"🧠 [MEMORY:long-term] Storing {memory_type} memory: {memory_item.content}")
|
||||
await self.memory.add(memory_item)
|
||||
|
||||
async def _add_memory_task(self, task: MemoryProcessingTask) -> None:
|
||||
"""
|
||||
Add a memory task to the memory tasks list.
|
||||
"""
|
||||
self.memory_tasks.append(task)
|
||||
|
||||
async def _update_task_status(self, task: MemoryProcessingTask) -> None:
|
||||
"""
|
||||
Update the task status.
|
||||
"""
|
||||
try:
|
||||
self.memory_tasks = [t for t in self.memory_tasks if t.memory_task_id != task.memory_task_id]
|
||||
self.memory_tasks.append(task)
|
||||
except Exception as e:
|
||||
logger.error(f"🧠 [MEMORY:long-term] Error updating task status: {e}" + traceback.format_exc())
|
||||
|
||||
async def retrieve_agent_experience(self, query: str, agent_id: Optional[str] = None,
|
||||
application_id: Optional[str] = "default") -> List[AgentExperience]:
|
||||
pass
|
||||
|
||||
async def retrieve_user_profile(self, query: str, user_id: Optional[str] = None,
|
||||
application_id: Optional[str] = "default") -> List[UserProfile]:
|
||||
pass
|
||||
@@ -0,0 +1,771 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
import abc
|
||||
import json
|
||||
import traceback
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from aworld.core.memory import MemoryBase, MemoryItem, MemoryStore, MemoryConfig, AgentMemoryConfig
|
||||
from aworld.logs.util import logger
|
||||
from aworld.memory.embeddings.base import EmbeddingsResult, EmbeddingsMetadata
|
||||
from aworld.memory.embeddings.factory import EmbedderFactory
|
||||
from aworld.memory.longterm import DefaultMemoryOrchestrator
|
||||
from aworld.memory.models import AgentExperience, LongTermMemoryTriggerParams, MemoryToolMessage, MessageMetadata, \
|
||||
UserProfileExtractParams, \
|
||||
AgentExperienceExtractParams, UserProfile, MemorySummary, MemoryAIMessage, Fact
|
||||
from aworld.memory.vector.factory import VectorDBFactory
|
||||
from aworld.models.llm import acall_llm_model
|
||||
from aworld.models.utils import num_tokens_from_messages
|
||||
|
||||
AWORLD_MEMORY_EXTRACT_NEW_SUMMARY = """
|
||||
You are presented with a user task, a conversion that may contain the answer, and a previous conversation summary.
|
||||
Please read the conversation carefully and extract new information from the conversation that helps to solve user task
|
||||
<guide>
|
||||
1. if current conversion contain answer of task or related information, must include it in the summary.
|
||||
2. record key step of current conversion. such visited web page, use tools information, etc. example:
|
||||
- step_info:
|
||||
- step_content: the description of step, must be a complete sentence and keep information params of step.
|
||||
- use tools information:
|
||||
- tool_name: search
|
||||
- tool_input: {{
|
||||
"query": "python"
|
||||
}}
|
||||
- step_result: the result of step and evidence information, such link of visited web page for slove task
|
||||
|
||||
3. In your summary, aim to reduce unnecessary information, but make sure your summarized content still provides enough details for the task and does not lose any important information.
|
||||
<guide>
|
||||
|
||||
|
||||
<user_task> {user_task} </user_task>
|
||||
<existed_summary> {existed_summary} </existed_summary>
|
||||
<conversation> {to_be_summary} </conversation>
|
||||
|
||||
## output new summary:
|
||||
"""
|
||||
AWORLD_MEMORY_UPDATE_SUMMARY = """
|
||||
You are presented with a user task, a conversion that may contain the answer, and a previous conversation summary.
|
||||
Please read the conversation carefully and extract new information from the conversation that helps to solve user task, while retaining all relevant details from the previous memory.
|
||||
<user_task> {user_task} </user_task>
|
||||
<existed_summary> {existed_summary} </existed_summary>
|
||||
<conversation> {to_be_summary} </conversation>
|
||||
|
||||
## result summary:
|
||||
"""
|
||||
|
||||
class InMemoryMemoryStore(MemoryStore):
|
||||
def __init__(self):
|
||||
self.memory_items = []
|
||||
|
||||
def add(self, memory_item: MemoryItem):
|
||||
self.memory_items.append(memory_item)
|
||||
|
||||
def get(self, memory_id) -> Optional[MemoryItem]:
|
||||
return next((item for item in self.memory_items if item.id == memory_id), None)
|
||||
|
||||
def get_first(self, filters: dict = None) -> Optional[MemoryItem]:
|
||||
"""Get the first memory item."""
|
||||
filtered_items = self.get_all(filters)
|
||||
if len(filtered_items) == 0:
|
||||
return None
|
||||
return filtered_items[0]
|
||||
|
||||
def total_rounds(self, filters: dict = None) -> int:
|
||||
"""Get the total number of rounds."""
|
||||
return len(self.get_all(filters))
|
||||
|
||||
def get_all(self, filters: dict = None) -> list[MemoryItem]:
|
||||
"""Filter memory items based on filters."""
|
||||
filtered_items = [item for item in self.memory_items if self._filter_memory_item(item, filters)]
|
||||
return filtered_items
|
||||
|
||||
def _filter_memory_item(self, memory_item: MemoryItem, filters: dict = None) -> bool:
|
||||
if memory_item.deleted:
|
||||
return False
|
||||
if filters is None:
|
||||
return True
|
||||
if filters.get('application_id') is not None:
|
||||
if memory_item.application_id is None:
|
||||
return False
|
||||
if memory_item.application_id != filters['application_id']:
|
||||
return False
|
||||
if filters.get('user_id') is not None:
|
||||
if memory_item.user_id is None:
|
||||
return False
|
||||
if memory_item.user_id != filters['user_id']:
|
||||
return False
|
||||
if filters.get('agent_id') is not None:
|
||||
if memory_item.agent_id is None:
|
||||
return False
|
||||
if memory_item.agent_id != filters['agent_id']:
|
||||
return False
|
||||
if filters.get('agent_name') is not None:
|
||||
if memory_item.agent_id is None:
|
||||
return False
|
||||
if memory_item.agent_id != filters['agent_id']:
|
||||
return False
|
||||
if filters.get('task_id') is not None:
|
||||
if memory_item.task_id is None:
|
||||
return False
|
||||
if memory_item.task_id != filters['task_id']:
|
||||
return False
|
||||
if filters.get('session_id') is not None:
|
||||
if memory_item.session_id is None:
|
||||
return False
|
||||
if memory_item.session_id != filters['session_id']:
|
||||
return False
|
||||
if filters.get('tool_call_id') is not None:
|
||||
if memory_item.metadata.get("tool_call_id") is None:
|
||||
return False
|
||||
if memory_item.metadata.get("tool_call_id") != filters['tool_call_id']:
|
||||
return False
|
||||
if filters.get('memory_type') is not None:
|
||||
if memory_item.memory_type is None:
|
||||
return False
|
||||
elif isinstance(filters['memory_type'], list) and memory_item.memory_type not in filters['memory_type']:
|
||||
return False
|
||||
elif isinstance(filters['memory_type'], str) and memory_item.memory_type != filters['memory_type']:
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_last_n(self, last_rounds, filters: dict = None) -> list[MemoryItem]:
|
||||
return self.get_all(filters=filters)[-last_rounds:]
|
||||
|
||||
def update(self, memory_item: MemoryItem):
|
||||
for index, item in enumerate(self.memory_items):
|
||||
if item.id == memory_item.id:
|
||||
self.memory_items[index] = memory_item
|
||||
break
|
||||
|
||||
def delete(self, memory_id):
|
||||
exists = self.get(memory_id)
|
||||
if exists:
|
||||
exists.deleted = True
|
||||
|
||||
def delete_items(self, message_types: list[str], session_id: str, task_id: str, filters: dict = None):
|
||||
for item in self.memory_items:
|
||||
if item.memory_type in message_types and item.session_id == session_id and item.task_id == task_id:
|
||||
item.deleted = True
|
||||
|
||||
def history(self, memory_id) -> list[MemoryItem] | None:
|
||||
exists = self.get(memory_id)
|
||||
if exists:
|
||||
return exists.histories
|
||||
return None
|
||||
|
||||
MEMORY_HOLDER = {}
|
||||
class MemoryFactory:
|
||||
|
||||
@classmethod
|
||||
def init(cls, custom_memory_store: MemoryStore = None, config: MemoryConfig = MemoryConfig(provider="aworld")):
|
||||
if custom_memory_store:
|
||||
MEMORY_HOLDER["instance"] = AworldMemory(
|
||||
memory_store=custom_memory_store,
|
||||
config=config
|
||||
)
|
||||
else:
|
||||
MEMORY_HOLDER["instance"] = AworldMemory(
|
||||
memory_store=InMemoryMemoryStore(),
|
||||
config=config
|
||||
)
|
||||
logger.info(f"Memory init success")
|
||||
|
||||
|
||||
@classmethod
|
||||
def instance(cls) -> "MemoryBase":
|
||||
"""
|
||||
Get the in-memory memory instance.
|
||||
Returns:
|
||||
MemoryBase: In-memory memory instance.
|
||||
"""
|
||||
if MEMORY_HOLDER.get("instance"):
|
||||
logger.info(f"instance use cached memory instance")
|
||||
return MEMORY_HOLDER["instance"]
|
||||
MEMORY_HOLDER["instance"] = MemoryFactory.from_config(
|
||||
config=MemoryConfig(provider="aworld"),
|
||||
memory_store=InMemoryMemoryStore()
|
||||
)
|
||||
logger.info(f"instance use new memory instance")
|
||||
return MEMORY_HOLDER["instance"]
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: MemoryConfig, memory_store: MemoryStore = None) -> "MemoryBase":
|
||||
"""
|
||||
Initialize a Memory instance from a configuration dictionary.
|
||||
|
||||
Args:
|
||||
config (dict): Configuration dictionary.
|
||||
|
||||
Returns:
|
||||
MemoryBase: Memory instance.
|
||||
"""
|
||||
if config.provider == "aworld":
|
||||
logger.info("🧠 [MEMORY]setup memory store: aworld")
|
||||
return AworldMemory(
|
||||
memory_store=memory_store or InMemoryMemoryStore(),
|
||||
config=config
|
||||
)
|
||||
elif config.provider == "mem0":
|
||||
from aworld.memory.mem0.mem0_memory import Mem0Memory
|
||||
logger.info("🧠 [MEMORY]setup memory store: mem0")
|
||||
return Mem0Memory(
|
||||
memory_store=memory_store or InMemoryMemoryStore(),
|
||||
config=config
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid memory store type: {config.get('memory_store')}")
|
||||
|
||||
|
||||
class Memory(MemoryBase):
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
def __init__(self, memory_store: MemoryStore, config: MemoryConfig, **kwargs):
|
||||
self.memory_store = memory_store
|
||||
self.config = config
|
||||
|
||||
# Initialize llm_model components
|
||||
self._llm_instance = config.get_llm_instance()
|
||||
|
||||
# Initialize embedding and vector database components
|
||||
self._embedder = EmbedderFactory.get_embedder(config.embedding_config)
|
||||
self._vector_db = VectorDBFactory.get_vector_db(config.vector_store_config)
|
||||
|
||||
# Initialize long-term memory components
|
||||
self.memory_orchestrator = DefaultMemoryOrchestrator(
|
||||
self._llm_instance,
|
||||
embedding_model=self._embedder,
|
||||
memory=self
|
||||
)
|
||||
|
||||
@property
|
||||
def default_llm_instance(self):
|
||||
if not self._llm_instance:
|
||||
raise ValueError("LLM instance is not initialized")
|
||||
return self._llm_instance
|
||||
|
||||
|
||||
def _build_history_context(self, messages) -> str:
|
||||
"""Build the history context string from a list of messages.
|
||||
|
||||
Args:
|
||||
messages: List of message objects with 'role', 'content', and optional 'tool_calls'.
|
||||
Returns:
|
||||
Concatenated context string.
|
||||
"""
|
||||
history_context = ""
|
||||
for item in messages:
|
||||
history_context += (f"\n\n{item['role']}: {item['content']}, "
|
||||
f"{'tool_calls:' + json.dumps(item['tool_calls']) if 'tool_calls' in item and item['tool_calls'] else ''}")
|
||||
return history_context
|
||||
|
||||
async def _call_llm_summary(self, summary_messages: list, agent_memory_config: AgentMemoryConfig) -> str:
|
||||
"""Call LLM to generate summary and log the process.
|
||||
|
||||
Args:
|
||||
summary_messages: List of messages to send to LLM.
|
||||
Returns:
|
||||
Summary content string.
|
||||
"""
|
||||
llm_response = await acall_llm_model(
|
||||
self.default_llm_instance,
|
||||
messages=summary_messages,
|
||||
# model_name=agent_memory_config.summary_model,
|
||||
stream=False,
|
||||
)
|
||||
logger.debug(f"🧠 [MEMORY:short-term] [Summary] Creating summary memory, history messages: {summary_messages}")
|
||||
return llm_response.content
|
||||
|
||||
def _get_parsed_history_messages(self, history_items: list[MemoryItem]) -> list[dict]:
|
||||
"""Get and format history messages for summary.
|
||||
|
||||
Args:
|
||||
history_items: list[MemoryItem]
|
||||
Returns:
|
||||
List of parsed message dicts
|
||||
"""
|
||||
parsed_messages = [
|
||||
{
|
||||
'role': message.metadata['role'],
|
||||
'content': message.content,
|
||||
'tool_calls': message.metadata.get('tool_calls') if message.metadata.get('tool_calls') else None
|
||||
}
|
||||
for message in history_items]
|
||||
return parsed_messages
|
||||
|
||||
async def async_gen_multi_rounds_summary(self, to_be_summary: list[MemoryItem], agent_memory_config: AgentMemoryConfig) -> str:
|
||||
logger.info(
|
||||
f"🧠 [MEMORY:short-term] [Summary] Creating summary memory, history messages")
|
||||
if len(to_be_summary) == 0:
|
||||
return ""
|
||||
parsed_messages = self._get_parsed_history_messages(to_be_summary)
|
||||
history_context = self._build_history_context(parsed_messages)
|
||||
|
||||
summary_messages = [
|
||||
{"role": "user", "content": agent_memory_config.summary_prompt.format(context=history_context)}
|
||||
]
|
||||
|
||||
return await self._call_llm_summary(summary_messages)
|
||||
|
||||
async def async_gen_summary(self, filters: dict, last_rounds: int, agent_memory_config: AgentMemoryConfig) -> str:
|
||||
"""A tool for summarizing the conversation history."""
|
||||
|
||||
logger.info(f"🧠 [MEMORY:short-term] [Summary] Creating summary memory, history messages [filters -> {filters}, "
|
||||
f"last_rounds -> {last_rounds}]")
|
||||
history_items = self.memory_store.get_last_n(last_rounds, filters=filters)
|
||||
if len(history_items) == 0:
|
||||
return ""
|
||||
parsed_messages = self._get_parsed_history_messages(history_items)
|
||||
history_context = self._build_history_context(parsed_messages)
|
||||
|
||||
summary_messages = [
|
||||
{"role": "user", "content": agent_memory_config.summary_prompt.format(context=history_context)}
|
||||
]
|
||||
|
||||
return await self._call_llm_summary(summary_messages)
|
||||
|
||||
async def async_gen_cur_round_summary(self, to_be_summary: MemoryItem, filters: dict, last_rounds: int, agent_memory_config: AgentMemoryConfig) -> str:
|
||||
if not agent_memory_config.enable_summary or len(to_be_summary.content) < agent_memory_config.summary_single_context_length:
|
||||
return to_be_summary.content
|
||||
|
||||
logger.info(f"🧠 [MEMORY:short-term] [Summary] Creating summary memory, history messages [filters -> {filters}, "
|
||||
f"last_rounds -> {last_rounds}]: to be summary content is {to_be_summary.content}")
|
||||
history_items = self.memory_store.get_last_n(last_rounds, filters=filters)
|
||||
if len(history_items) == 0:
|
||||
return ""
|
||||
parsed_messages = self._get_parsed_history_messages(history_items)
|
||||
|
||||
# Append the to_be_summary
|
||||
parsed_messages.append({
|
||||
"role": to_be_summary.metadata['role'],
|
||||
"content": f"{to_be_summary.content}",
|
||||
'tool_call_id': to_be_summary.metadata['tool_call_id'],
|
||||
})
|
||||
history_context = self._build_history_context(parsed_messages)
|
||||
|
||||
summary_messages = [
|
||||
{"role": "user", "content": agent_memory_config.summary_prompt.format(context=history_context)}
|
||||
]
|
||||
|
||||
return await self._call_llm_summary(summary_messages)
|
||||
|
||||
def search(self, query, limit=100, memory_type="message", threshold=0.8, filters=None) -> Optional[list[MemoryItem]]:
|
||||
pass
|
||||
|
||||
async def add(self, memory_item: MemoryItem, filters: dict = None, agent_memory_config: AgentMemoryConfig = None):
|
||||
await self._add(memory_item, filters, agent_memory_config)
|
||||
# self.post_add(memory_item, filters, memory_config)
|
||||
|
||||
@abc.abstractmethod
|
||||
async def _add(self, memory_item: MemoryItem, filters: dict = None, agent_memory_config: AgentMemoryConfig = None):
|
||||
pass
|
||||
|
||||
async def post_add(self, memory_item: MemoryItem, filters: dict = None, agent_memory_config: AgentMemoryConfig = None):
|
||||
try:
|
||||
await self.post_process_long_terms(memory_item, filters, agent_memory_config)
|
||||
except Exception as err:
|
||||
logger.warning(f"🧠 [MEMORY:long-term] Error during long-term memory processing: {err}, traceback is {traceback.format_exc()}")
|
||||
|
||||
async def post_process_long_terms(self, memory_item: MemoryItem, filters: dict = None, agent_memory_config: AgentMemoryConfig = None):
|
||||
"""Post process long-term memory."""
|
||||
# check if memory_item is "message"
|
||||
if memory_item.memory_type != 'message':
|
||||
return
|
||||
|
||||
if not agent_memory_config:
|
||||
return
|
||||
|
||||
# check if long-term memory is enabled
|
||||
if not agent_memory_config.enable_long_term:
|
||||
return
|
||||
|
||||
# check if long-term memory config is valid
|
||||
long_term_config = agent_memory_config.long_term_config
|
||||
if not long_term_config:
|
||||
return
|
||||
|
||||
await self.trigger_short_term_memory_to_long_term(LongTermMemoryTriggerParams(
|
||||
agent_id=memory_item.agent_id,
|
||||
session_id=memory_item.session_id,
|
||||
task_id=memory_item.task_id,
|
||||
user_id=memory_item.user_id,
|
||||
application_id=memory_item.application_id
|
||||
), agent_memory_config)
|
||||
|
||||
async def trigger_short_term_memory_to_long_term(self, params: LongTermMemoryTriggerParams, agent_memory_config: AgentMemoryConfig = None):
|
||||
logger.info(f"🧠 [MEMORY:long-term] Trigger short-term memory to long-term memory, params is {params}")
|
||||
if not agent_memory_config:
|
||||
return
|
||||
|
||||
# check if long-term memory is enabled
|
||||
if not agent_memory_config.enable_long_term:
|
||||
return
|
||||
|
||||
# check if long-term memory config is valid
|
||||
long_term_config = agent_memory_config.long_term_config
|
||||
if not long_term_config:
|
||||
return
|
||||
|
||||
# get all memories of current task
|
||||
task_memory_items = self.memory_store.get_all({
|
||||
'memory_type': 'message',
|
||||
'agent_id': params.agent_id,
|
||||
'application_id': params.application_id,
|
||||
'session_id': params.session_id,
|
||||
'task_id': params.task_id
|
||||
})
|
||||
|
||||
task_params = []
|
||||
|
||||
# Check if user profile extraction is enabled
|
||||
if long_term_config.extraction.enable_user_profile_extraction:
|
||||
if params.user_id:
|
||||
user_profile_task_params = UserProfileExtractParams(
|
||||
user_id=params.user_id,
|
||||
session_id=params.session_id,
|
||||
task_id=params.task_id,
|
||||
application_id=params.application_id,
|
||||
memories=task_memory_items
|
||||
)
|
||||
task_params.append(user_profile_task_params)
|
||||
logger.info(f"🧠 [MEMORY:long-term] add user profile extraction task params is {user_profile_task_params}")
|
||||
else:
|
||||
logger.warning(f"🧠 [MEMORY:long-term] memory_item.user_id is None, skip user profile extraction")
|
||||
|
||||
# Check if agent experience extraction is enabled
|
||||
if long_term_config.extraction.enable_agent_experience_extraction:
|
||||
if params.agent_id:
|
||||
agent_experience_task_params = AgentExperienceExtractParams(
|
||||
agent_id=params.agent_id,
|
||||
session_id=params.session_id,
|
||||
task_id=params.task_id,
|
||||
application_id=params.application_id,
|
||||
memories=task_memory_items
|
||||
)
|
||||
task_params.append(agent_experience_task_params)
|
||||
logger.debug(f"🧠 [MEMORY:long-term] add agent experience extraction task params is {agent_experience_task_params}")
|
||||
else:
|
||||
logger.warning(
|
||||
f"🧠 [MEMORY:long-term] memory_item.agent_id is None, skip agent experience extraction")
|
||||
|
||||
await self.memory_orchestrator.create_longterm_processing_tasks(task_params, agent_memory_config.long_term_config, params.force)
|
||||
|
||||
async def retrival_user_profile(self, user_id: str, user_input: str, threshold: float = 0.5, limit: int = 3, filters: dict = None) -> Optional[list[UserProfile]]:
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
return self.search(user_input, limit=limit,memory_type='user_profile',threshold=threshold, filters={
|
||||
'user_id': user_id,
|
||||
**filters
|
||||
})
|
||||
|
||||
async def retrival_facts(self, user_id: str, user_input: str, threshold: float = 0.5, limit: int = 3, filters: dict = None) -> Optional[list[Fact]]:
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
return self.search(user_input, limit=limit,memory_type='fact',threshold=threshold, filters={
|
||||
'user_id': user_id,
|
||||
**filters
|
||||
})
|
||||
|
||||
|
||||
async def retrival_agent_experience(self, agent_id: str, user_input: str, threshold: float = 0.5, limit: int = 3, filters: dict = None) -> Optional[list[AgentExperience]]:
|
||||
if not filters:
|
||||
filters = {}
|
||||
return self.search(user_input, limit=limit, memory_type='agent_experience',threshold=threshold, filters={
|
||||
'agent_id': agent_id,
|
||||
**filters
|
||||
})
|
||||
|
||||
async def retrival_similar_user_messages_history(self, user_id: str, user_input: str, threshold: float = 0.5, limit: int = 10, filters: dict = None) -> Optional[list[MemoryItem]]:
|
||||
if not filters:
|
||||
filters = {}
|
||||
return self.search(user_input, limit=limit, memory_type='message', threshold=threshold, filters={
|
||||
'role': 'user',
|
||||
'user_id': user_id,
|
||||
**filters
|
||||
})
|
||||
|
||||
|
||||
def delete(self, memory_id):
|
||||
pass
|
||||
|
||||
def update(self, memory_item: MemoryItem):
|
||||
pass
|
||||
|
||||
class AworldMemory(Memory):
|
||||
def __init__(self, memory_store: MemoryStore, config: MemoryConfig, **kwargs):
|
||||
super().__init__(memory_store=memory_store, config=config, **kwargs)
|
||||
self.summary = {}
|
||||
|
||||
def _filter_incomplete_message_pairs(self, message_items: list[MemoryItem]) -> list[MemoryItem]:
|
||||
"""
|
||||
Filter out incomplete message pairs to ensure only complete [ai, tool] message pair sequences are retained.
|
||||
|
||||
For sequence [ai,tool,ai,tool,ai,tool,ai,tool,tool,ai,tool,tool,tool],
|
||||
identify the complete subsequence [ai,tool,ai,tool,ai,tool,ai,tool,tool],
|
||||
i.e., remove the incomplete part in the last group [ai,tool,tool,tool].
|
||||
|
||||
Args:
|
||||
message_items: List of message items
|
||||
|
||||
Returns:
|
||||
Filtered message items list, retaining only complete [ai, tool] pairs
|
||||
"""
|
||||
if len(message_items) < 2:
|
||||
return message_items
|
||||
|
||||
# Find the last AI message in the sequence
|
||||
last_ai_index = -1
|
||||
for i in range(len(message_items) - 1, -1, -1):
|
||||
if isinstance(message_items[i], MemoryAIMessage):
|
||||
last_ai_index = i
|
||||
break
|
||||
|
||||
# If no AI message found, return empty list
|
||||
if last_ai_index == -1:
|
||||
return []
|
||||
|
||||
# Remove everything from the last AI message onwards
|
||||
# This removes the last incomplete [ai, tool, tool, ...] group
|
||||
return message_items[:last_ai_index]
|
||||
|
||||
async def _add(self, memory_item: MemoryItem, filters: dict = None, agent_memory_config: AgentMemoryConfig = None):
|
||||
self.memory_store.add(memory_item)
|
||||
|
||||
# save to vector store
|
||||
self._save_to_vector_db(memory_item)
|
||||
|
||||
# Check if we need to create or update summary
|
||||
if agent_memory_config and agent_memory_config.enable_summary:
|
||||
if memory_item.memory_type == "message":
|
||||
await self._summary_agent_task_memory(memory_item, agent_memory_config)
|
||||
|
||||
async def _summary_agent_task_memory(self, memory_item: MemoryItem, agent_memory_config: AgentMemoryConfig):
|
||||
# obtain assistant un summary messages
|
||||
|
||||
# get init messages
|
||||
agent_task_total_message = self.get_all(
|
||||
filters={
|
||||
"agent_id": memory_item.agent_id,
|
||||
"session_id": memory_item.session_id,
|
||||
"task_id": memory_item.task_id,
|
||||
"memory_type": ["init","message","summary"]
|
||||
}
|
||||
)
|
||||
to_be_summary_items = [item for item in agent_task_total_message if item.memory_type == "message" and not item.has_summary]
|
||||
|
||||
# 序列中删除最后一组完整的 [ai,tool] 组合
|
||||
to_be_summary_items = self._filter_incomplete_message_pairs(to_be_summary_items)
|
||||
|
||||
check_need_summary,trigger_reason = self._check_need_summary(to_be_summary_items, agent_memory_config)
|
||||
logger.info(f"🧠 [MEMORY:short-term] [Summary] check_need_summary: {check_need_summary}, trigger_reason: {trigger_reason}")
|
||||
|
||||
if not check_need_summary:
|
||||
return
|
||||
|
||||
existed_summary_items = [item for item in agent_task_total_message if item.memory_type == "summary"]
|
||||
user_task_items = [item for item in agent_task_total_message if item.memory_type == "init"]
|
||||
# generate summary
|
||||
summary_content = await self._gen_multi_rounds_summary(user_task_items, existed_summary_items, to_be_summary_items, agent_memory_config)
|
||||
logger.debug(f"🧠 [MEMORY:short-term] [Summary] summary_content: {summary_content}")
|
||||
|
||||
summary_metadata = MessageMetadata(
|
||||
agent_id=memory_item.agent_id,
|
||||
agent_name=memory_item.agent_name,
|
||||
session_id=memory_item.session_id,
|
||||
task_id=memory_item.task_id,
|
||||
user_id=memory_item.user_id
|
||||
)
|
||||
summary_memory = MemorySummary(
|
||||
item_ids=[item.id for item in to_be_summary_items],
|
||||
summary=summary_content,
|
||||
metadata=summary_metadata,
|
||||
created_at=to_be_summary_items[0].created_at
|
||||
)
|
||||
|
||||
# add summary to memory
|
||||
self.memory_store.add(summary_memory)
|
||||
|
||||
# mark memory item summary flag
|
||||
for summary_item in to_be_summary_items:
|
||||
summary_item.mark_has_summary()
|
||||
self.memory_store.update(summary_item)
|
||||
logger.info(f"🧠 [MEMORY:short-term] [Summary] [{trigger_reason}]Creating summary memory finished: content is {summary_content[:100]}")
|
||||
|
||||
|
||||
def _check_need_summary(self, to_be_summary_items: list[MemoryItem], agent_memory_config: AgentMemoryConfig) -> Tuple[bool,str]:
|
||||
if len(to_be_summary_items) <= 0:
|
||||
return False, "EMPTY"
|
||||
if isinstance(to_be_summary_items[-1], MemoryAIMessage):
|
||||
if to_be_summary_items[-1].tool_calls and len(to_be_summary_items[-1].tool_calls) > 0:
|
||||
return False,"last message has tool_calls"
|
||||
if len(to_be_summary_items) == 0:
|
||||
return False, "items is empty"
|
||||
if len(to_be_summary_items) >= agent_memory_config.summary_rounds:
|
||||
return True, "summary_rounds"
|
||||
if num_tokens_from_messages([item.to_openai_message() for item in to_be_summary_items]) > agent_memory_config.summary_context_length:
|
||||
return True, "summary_context_length"
|
||||
return False, "unknown"
|
||||
|
||||
async def _gen_multi_rounds_summary(self, user_task_items: list[MemoryItem], existed_summary_items: list[MemorySummary],
|
||||
to_be_summary_items: list[MemoryItem], agent_memory_config: AgentMemoryConfig) -> str:
|
||||
|
||||
if len(to_be_summary_items) == 0:
|
||||
return ""
|
||||
|
||||
# get user task, existed summary, to be summary
|
||||
user_task = [{"role": item.metadata['role'], "content": item.content} for item in user_task_items]
|
||||
existed_summary = [{"summary_item_ids": item.summary_item_ids, "content": item.content} for item in existed_summary_items]
|
||||
to_be_summary = [{"role": item.metadata['role'], "content": item.content} for item in to_be_summary_items]
|
||||
|
||||
# generate summary
|
||||
summary_messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": AWORLD_MEMORY_EXTRACT_NEW_SUMMARY.format(
|
||||
user_task=user_task,
|
||||
existed_summary=existed_summary,
|
||||
to_be_summary=to_be_summary
|
||||
)
|
||||
}
|
||||
]
|
||||
llm_summary = await self._call_llm_summary(summary_messages, agent_memory_config)
|
||||
tool_use_content = "\n\n the following is the tool use history:\n"
|
||||
for item in to_be_summary_items:
|
||||
if item.metadata.get('summary_content'):
|
||||
tool_use_content += f"{item.metadata.get('summary_content', '')}\n"
|
||||
|
||||
return f"{llm_summary}{tool_use_content}"
|
||||
|
||||
|
||||
|
||||
|
||||
def _save_to_vector_db(self, memory_item: MemoryItem):
|
||||
try:
|
||||
if not memory_item.embedding_text:
|
||||
logger.debug(f"memory_item.embedding_text is None, skip save to vector store")
|
||||
return
|
||||
if self._vector_db and self._embedder:
|
||||
embedding = self._embedder.embed_query(memory_item.embedding_text)
|
||||
# save to vector store
|
||||
embedding_meta = EmbeddingsMetadata(
|
||||
memory_id=memory_item.id,
|
||||
agent_id = memory_item.agent_id,
|
||||
session_id = memory_item.session_id,
|
||||
task_id = memory_item.task_id,
|
||||
user_id = memory_item.user_id,
|
||||
application_id = memory_item.application_id,
|
||||
memory_type=memory_item.memory_type,
|
||||
created_at=memory_item.created_at,
|
||||
updated_at=memory_item.updated_at,
|
||||
embedding_model=self.config.embedding_config.model_name,
|
||||
)
|
||||
embedding_item= EmbeddingsResult(embedding = embedding, content=memory_item.embedding_text, metadata=embedding_meta)
|
||||
|
||||
self._vector_db.insert(self.config.vector_store_config.config['collection_name'], [embedding_item])
|
||||
else:
|
||||
logger.warning(f"memory_store or embedder is None, skip save to vector store")
|
||||
except Exception as err:
|
||||
logger.warning(f"save_to_vector, failed is {err}")
|
||||
|
||||
def update(self, memory_item: MemoryItem):
|
||||
self.memory_store.update(memory_item)
|
||||
|
||||
def delete(self, memory_id):
|
||||
self.memory_store.delete(memory_id)
|
||||
|
||||
def delete_items(self, message_types: list[str], session_id: str, task_id: str, filters: dict = None):
|
||||
self.memory_store.delete_items(message_types, session_id, task_id, filters)
|
||||
|
||||
def get(self, memory_id) -> Optional[MemoryItem]:
|
||||
return self.memory_store.get(memory_id)
|
||||
|
||||
def get_all(self, filters: dict = None) -> list[MemoryItem]:
|
||||
return self.memory_store.get_all(filters=filters)
|
||||
|
||||
def get_last_n(self, last_rounds, filters: dict = None, agent_memory_config: AgentMemoryConfig = None) -> list[MemoryItem]:
|
||||
"""
|
||||
Retrieve the last N rounds of conversation memory, including initialization messages, unsummarized messages, and summary messages.
|
||||
|
||||
Workflow:
|
||||
1. Fetch all relevant messages (init, message, summary types)
|
||||
2. Extract initialization messages (init type)
|
||||
3. Get unsummarized messages (message type not summarized) and summary messages (summary type)
|
||||
4. If total messages <= requested rounds, return all messages
|
||||
5. Otherwise, return the last N rounds while ensuring tool message integrity
|
||||
|
||||
Args:
|
||||
last_rounds (int): Number of recent message rounds to retrieve
|
||||
filters (dict): Filter conditions, must contain agent_id, session_id, task_id
|
||||
agent_memory_config (AgentMemoryConfig): Agent memory configuration
|
||||
|
||||
Returns:
|
||||
list[MemoryItem]: Returns a combined list of memories in the following order:
|
||||
1. Initialization messages (if any)
|
||||
2. Last N rounds of unsummarized messages and summary messages
|
||||
|
||||
Note:
|
||||
- When the most recent message is a tool message, may return more than last_rounds
|
||||
messages to ensure tool call integrity
|
||||
- Returns empty list if filters is empty
|
||||
"""
|
||||
if last_rounds < 0:
|
||||
return []
|
||||
|
||||
if not filters:
|
||||
return []
|
||||
|
||||
# get all messages
|
||||
agent_task_total_message = self.get_all(
|
||||
filters={
|
||||
"agent_id": filters.get('agent_id'),
|
||||
"session_id": filters.get('session_id'),
|
||||
"task_id": filters.get('task_id'),
|
||||
"memory_type": ["init", "message", "summary"]
|
||||
}
|
||||
)
|
||||
|
||||
init_items = [item for item in agent_task_total_message if item.memory_type == "init"]
|
||||
|
||||
# if last_rounds is 0, return init_items
|
||||
if last_rounds == 0:
|
||||
return init_items
|
||||
|
||||
# get unsummarized messages and summary messages
|
||||
result_items = [item for item in agent_task_total_message if (item.memory_type == "message" and not item.has_summary) or (item.memory_type == 'summary')]
|
||||
|
||||
# if total messages <= requested rounds, return all messages
|
||||
if len(result_items) <= last_rounds:
|
||||
result_items = init_items + result_items
|
||||
else:
|
||||
# Ensure tool message completeness: LLM API requires the preceding tool_calls message
|
||||
# to be included when processing a tool message. If the first message in our window
|
||||
# is a tool message, we need to expand the window to include its associated tool_calls.
|
||||
while isinstance(result_items[-last_rounds], MemoryToolMessage):
|
||||
last_rounds = last_rounds + 1
|
||||
result_items = init_items + result_items[-last_rounds:]
|
||||
|
||||
result_items.sort(key=lambda x: x.created_at, reverse=False)
|
||||
return result_items
|
||||
|
||||
|
||||
|
||||
def search(self, query, limit=100, memory_type="message", threshold=0.8, filters=None) -> Optional[list[MemoryItem]]:
|
||||
if self._vector_db:
|
||||
if not filters:
|
||||
filters = {}
|
||||
filters['memory_type'] = memory_type
|
||||
embedding = self._embedder.embed_query(query)
|
||||
results = self._vector_db.search(self.config.vector_store_config.config['collection_name'], [embedding], filters, threshold, limit)
|
||||
memory_items = []
|
||||
if results and results.docs:
|
||||
for result in results.docs:
|
||||
memory_item = self.memory_store.get(result.metadata.memory_id)
|
||||
if memory_item:
|
||||
memory_item.metadata['score'] = result.score
|
||||
memory_items.append(memory_item)
|
||||
return memory_items
|
||||
else:
|
||||
logger.warning(f"vector_db is None, skip search")
|
||||
return []
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aworld.config import ConfigDict
|
||||
from aworld.core.memory import MemoryStore, MemoryConfig, MemoryItem, AgentMemoryConfig
|
||||
from aworld.logs.util import logger
|
||||
from aworld.memory.main import Memory
|
||||
from aworld.models.llm import get_llm_model
|
||||
|
||||
|
||||
class Mem0Memory(Memory):
|
||||
def __init__(self, memory_store: MemoryStore, config: MemoryConfig | None = None, **kwargs):
|
||||
super().__init__(memory_store, config, **kwargs)
|
||||
self.config = config
|
||||
|
||||
conf = ConfigDict(
|
||||
llm_provider=config.llm_provider,
|
||||
llm_model_name=os.getenv("MEM_LLM_MODEL_NAME") if os.getenv("MEM_LLM_MODEL_NAME") else os.getenv(
|
||||
'LLM_MODEL_NAME'),
|
||||
llm_temperature=os.getenv("MEM_LLM_TEMPERATURE") if os.getenv("MEM_LLM_TEMPERATURE") else 1.0,
|
||||
llm_base_url=os.getenv("MEM_LLM_BASE_URL") if os.getenv("MEM_LLM_BASE_URL") else os.getenv('LLM_BASE_URL'),
|
||||
llm_api_key=os.getenv("MEM_LLM_API_KEY") if os.getenv("MEM_LLM_API_KEY") else os.getenv('LLM_API_KEY')
|
||||
)
|
||||
self.config.llm_instance = get_llm_model(conf=conf, streaming=False)
|
||||
|
||||
# Check for required packages
|
||||
try:
|
||||
# also disable mem0's telemetry when ANONYMIZED_TELEMETRY=False
|
||||
if os.getenv('ANONYMIZED_TELEMETRY', 'true').lower()[0] in 'fn0':
|
||||
os.environ['MEM_TELEMETRY'] = 'False'
|
||||
from mem0 import Memory as Mem0
|
||||
except ImportError:
|
||||
raise ImportError('mem0 is required when enable_memory=True. Please install it with `pip install mem0`.')
|
||||
|
||||
# Initialize Mem0 with the configuration
|
||||
config_dict = self.config.full_config_dict
|
||||
self.mem0 = Mem0.from_config(config_dict=self.config.full_config_dict)
|
||||
self.memory_store = memory_store
|
||||
|
||||
def _add(self, memory_item: MemoryItem, filters: dict = None, agent_memory_config: AgentMemoryConfig = None):
|
||||
# generate summary memory if needed
|
||||
message_filters = {
|
||||
"memory_type": "message"
|
||||
}
|
||||
if filters:
|
||||
message_filters = {
|
||||
"memory_type": "message",
|
||||
"agent_id": memory_item.metadata.get("agent_id"),
|
||||
"task_id": memory_item.metadata.get("task_id"),
|
||||
"user_id": memory_item.metadata.get("user_id"),
|
||||
"session_id": memory_item.metadata.get("session_id"),
|
||||
}
|
||||
if self._need_summary(memory_item, message_filters):
|
||||
self.create_summary_memory(
|
||||
agent_id=memory_item.metadata.get("agent_id"),
|
||||
task_id=memory_item.metadata.get("task_id"),
|
||||
user_id=memory_item.metadata.get("user_id"),
|
||||
session_id=memory_item.metadata.get("session_id"),
|
||||
filters=message_filters
|
||||
)
|
||||
self.memory_store.add(memory_item)
|
||||
|
||||
def _need_summary(self, memory_item, message_filters):
|
||||
"""
|
||||
Check if a summary is needed based on the current step.
|
||||
1. If the number of messages is greater than the summary rounds.
|
||||
2. If the message is a message and the content is greater than the summary single context length.
|
||||
"""
|
||||
return self.memory_store.total_rounds(message_filters) > self.config.summary_rounds or (
|
||||
memory_item.memory_type == 'message' and len(
|
||||
memory_item.content) >= self.config.summary_single_context_length)
|
||||
|
||||
def create_summary_memory(self, agent_id, task_id, user_id, session_id, filters: dict) -> None:
|
||||
"""
|
||||
Create a summary memory if needed based on the current step.
|
||||
"""
|
||||
logger.info(f'Creating summary memory, {filters}')
|
||||
|
||||
# Get all messages
|
||||
all_messages = self.memory_store.get_all(filters=filters)
|
||||
|
||||
# Separate messages into those to keep as-is and those to process for memory
|
||||
summary_messages = []
|
||||
messages_to_process = []
|
||||
|
||||
for msg in all_messages:
|
||||
if isinstance(msg, MemoryItem) and msg.memory_type in {'summary'}:
|
||||
# Keep system and memory messages as they are
|
||||
summary_messages.append(msg)
|
||||
elif msg.memory_type in {'init'}:
|
||||
messages_to_process.append(msg)
|
||||
else:
|
||||
if len(msg.content) > 0:
|
||||
messages_to_process.append(msg)
|
||||
if messages_to_process[-1].metadata.get("tool_calls"):
|
||||
messages_to_process = messages_to_process[:-1]
|
||||
# Need at least 1 message to create a meaningful summary
|
||||
if len(messages_to_process) < 1:
|
||||
logger.info('Not enough non-memory messages to summarize')
|
||||
return
|
||||
# Create a procedural memory
|
||||
|
||||
memory_content = self._create_summary_memory(messages_to_process)
|
||||
|
||||
if not memory_content:
|
||||
logger.warning('Failed to create procedural memory')
|
||||
return
|
||||
|
||||
# Add the summary message
|
||||
summary_message = MemoryItem(content=memory_content, memory_type='summary', metadata={
|
||||
"role": "user",
|
||||
"agent_id": agent_id,
|
||||
"session_id": session_id,
|
||||
"task_id": task_id,
|
||||
"user_id": user_id,
|
||||
})
|
||||
summary_messages.append(summary_message)
|
||||
|
||||
# Update the history
|
||||
[self.memory_store.delete(m.id) for m in messages_to_process]
|
||||
self.memory_store.add(summary_message)
|
||||
|
||||
logger.info(f'Messages consolidated: {len(messages_to_process)} messages converted to procedural memory')
|
||||
|
||||
def _create_summary_memory(self, messages: list[MemoryItem]) -> str | None:
|
||||
|
||||
parsed_messages = [{'role': message.metadata['role'], 'content': message.content if not message.metadata.get(
|
||||
'tool_calls') else message.content + "\n\n" + self.__format_tool_call(message.metadata.get('tool_calls'))}
|
||||
for message in
|
||||
messages] # TODO add tool_call from metadata['tool_calls'] such as [{"id": "fc-7b66b01a-f125-44d5-9f32-5e3723384d8e", "type": "function", "function": {"name": "mcp__amap-amap-sse__maps_geo", "arguments": "{\"address\": \"\u676d\u5dde\", \"city\": \"\u676d\u5dde\"}"}}] append to content
|
||||
try:
|
||||
results = self.mem0.add(
|
||||
messages=parsed_messages,
|
||||
agent_id=messages[-1].metadata.get('agent_id'),
|
||||
memory_type='procedural_memory'
|
||||
)
|
||||
if len(results.get('results', [])):
|
||||
logger.info(f'creating summary memory result: {results}')
|
||||
return results.get('results', [])[0].get('memory')
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f'Error creating summary memory: {e}')
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
def __format_tool_call(self, tool_calls):
|
||||
return json.dumps(tool_calls, default=lambda o: o.model_dump_json() if isinstance(o, BaseModel) else str(o))
|
||||
|
||||
def update(self, memory_item: MemoryItem):
|
||||
self.memory_store.update(memory_item)
|
||||
|
||||
def delete(self, memory_id):
|
||||
self.memory_store.delete(memory_id)
|
||||
|
||||
def get(self, memory_id) -> Optional[MemoryItem]:
|
||||
# self.memory_store.get(memory_id)
|
||||
return self.memory_store.get(
|
||||
memory_id,
|
||||
)
|
||||
|
||||
def get_all(self, filters: dict = None) -> list[MemoryItem]:
|
||||
return self.memory_store.get_all(
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
def get_last_n(self, last_rounds, add_first_message=True, filters: dict = None, memory_config: MemoryConfig = None) -> list[MemoryItem]:
|
||||
"""
|
||||
Get last n memories.
|
||||
|
||||
Args:
|
||||
last_rounds (int): Number of memories to retrieve.
|
||||
add_first_message (bool):
|
||||
|
||||
Returns:
|
||||
list[MemoryItem]: List of latest memories.
|
||||
"""
|
||||
return self.memory_store.get_last_n(
|
||||
last_rounds=last_rounds,
|
||||
filters=filters,
|
||||
)
|
||||
@@ -0,0 +1,499 @@
|
||||
import uuid
|
||||
from abc import abstractmethod
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from typing import Any, Dict, List, Optional, Literal
|
||||
|
||||
from aworld.models.model_response import ToolCall
|
||||
|
||||
class MemoryItem(BaseModel):
|
||||
id: str = Field(description="id")
|
||||
content: Any = Field(description="content")
|
||||
created_at: Optional[str] = Field(None, description="created at")
|
||||
updated_at: Optional[str] = Field(None, description="updated at")
|
||||
metadata: dict = Field(
|
||||
description="metadata, use to store additional information, such as user_id, agent_id, run_id, task_id, etc.")
|
||||
tags: list[str] = Field(description="tags")
|
||||
histories: list["MemoryItem"] = Field(default_factory=list)
|
||||
deleted: bool = Field(default=False)
|
||||
memory_type: Literal["init", "message", "summary", "agent_experience", "user_profile", "fact", "conversation_summary"] = Field(default="message")
|
||||
version: int = Field(description="version")
|
||||
|
||||
def __init__(self, **data):
|
||||
# Set default values for optional fields
|
||||
if "id" not in data:
|
||||
data["id"] = str(uuid.uuid4())
|
||||
if "created_at" not in data:
|
||||
data["created_at"] = datetime.now().isoformat()
|
||||
if "updated_at" not in data:
|
||||
data["updated_at"] = data["created_at"]
|
||||
if "metadata" not in data:
|
||||
data["metadata"] = {}
|
||||
if "tags" not in data:
|
||||
data["tags"] = []
|
||||
if "version" not in data:
|
||||
data["version"] = 1
|
||||
|
||||
super().__init__(**data)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "MemoryItem":
|
||||
"""Create a MemoryItem instance from a dictionary.
|
||||
|
||||
Args:
|
||||
data (dict): A dictionary containing the memory item data.
|
||||
|
||||
Returns:
|
||||
MemoryItem: An instance of MemoryItem.
|
||||
"""
|
||||
return cls(**data)
|
||||
|
||||
@property
|
||||
def user_id(self) -> str:
|
||||
return self.metadata.get('user_id')
|
||||
|
||||
@property
|
||||
def session_id(self) -> str:
|
||||
return self.metadata.get('session_id')
|
||||
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
return self.metadata.get('task_id')
|
||||
|
||||
@property
|
||||
def agent_id(self) -> str:
|
||||
return self.metadata.get('agent_id')
|
||||
|
||||
@property
|
||||
def agent_name(self) -> str:
|
||||
return self.metadata.get('agent_name')
|
||||
|
||||
@property
|
||||
def application_id(self) -> str:
|
||||
return self.metadata.get('application_id', 'default')
|
||||
|
||||
@property
|
||||
def embedding_text(self) -> Optional[str]:
|
||||
return self.content
|
||||
|
||||
def mark_has_summary(self):
|
||||
self.metadata['summary'] = True
|
||||
|
||||
@property
|
||||
def has_summary(self) -> bool:
|
||||
return self.metadata.get('summary', False)
|
||||
|
||||
@property
|
||||
def content_length(self) -> int:
|
||||
return len(self.content)
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.metadata.get('status', 'ACCEPTED')
|
||||
|
||||
@status.setter
|
||||
def status(self, value: Literal["DRAFT", "ACCEPTED", "DISCARD"]) -> None:
|
||||
self.metadata['status'] = value
|
||||
|
||||
@abstractmethod
|
||||
def to_openai_message(self) -> dict:
|
||||
pass
|
||||
|
||||
|
||||
class MessageMetadata(BaseModel):
|
||||
"""
|
||||
Metadata for memory messages, including user, session, task, and agent information.
|
||||
Args:
|
||||
user_id (str): The ID of the user.
|
||||
session_id (str): The ID of the session.
|
||||
task_id (str): The ID of the task.
|
||||
agent_id (str): The ID of the agent.
|
||||
"""
|
||||
agent_id: str = Field(description="The ID of the agent")
|
||||
agent_name: Optional[str] = Field(description="The name of the agent")
|
||||
session_id: Optional[str] = Field(default=None,description="The ID of the session")
|
||||
task_id: Optional[str] = Field(default=None,description="The ID of the task")
|
||||
user_id: Optional[str] = Field(default=None, description="The ID of the user")
|
||||
summary_content: Optional[str] = Field(default=None, description="The summary of the memory item")
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
@property
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return self.model_dump()
|
||||
|
||||
class AgentExperienceItem(BaseModel):
|
||||
skill: str = Field(description="The skill demonstrated in the experience")
|
||||
actions: List[str] = Field(description="The actions taken by the agent")
|
||||
|
||||
|
||||
class AgentExperience(MemoryItem):
|
||||
"""
|
||||
Represents an agent's experience, including skills and actions.
|
||||
All custom attributes are stored in content and metadata.
|
||||
Args:
|
||||
agent_id (str): The ID of the agent.
|
||||
skill (str): The skill demonstrated in the experience.
|
||||
actions (List[str]): The actions taken by the agent.
|
||||
metadata (Optional[Dict[str, Any]]): Additional metadata.
|
||||
"""
|
||||
def __init__(self, agent_id: str, skill: str, actions: List[str], metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
meta = metadata.copy() if metadata else {}
|
||||
meta['agent_id'] = agent_id
|
||||
agent_experience = AgentExperienceItem(skill=skill, actions=actions)
|
||||
super().__init__(content=agent_experience, metadata=meta, memory_type="agent_experience")
|
||||
|
||||
@property
|
||||
def agent_id(self) -> str:
|
||||
return self.metadata['agent_id']
|
||||
|
||||
@property
|
||||
def skill(self) -> str:
|
||||
return self.content.skill
|
||||
|
||||
@property
|
||||
def actions(self) -> List[str]:
|
||||
return self.content.actions
|
||||
|
||||
@property
|
||||
def embedding_text(self):
|
||||
return f"skill:{self.skill}, actions:{self.actions}"
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": "system",
|
||||
"content": self.content
|
||||
}
|
||||
|
||||
|
||||
class UserProfileItem(BaseModel):
|
||||
key: str = Field(description="The key of the profile")
|
||||
value: Any = Field(description="The value of the profile")
|
||||
|
||||
class UserProfile(MemoryItem):
|
||||
"""
|
||||
Represents a user profile key-value pair.
|
||||
All custom attributes are stored in content and metadata.
|
||||
Args:
|
||||
user_id (str): The ID of the user.
|
||||
key (str): The profile key.
|
||||
value (Any): The profile value.
|
||||
metadata (Optional[Dict[str, Any]]): Additional metadata.
|
||||
"""
|
||||
def __init__(self, user_id: str, key: str, value: Any, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> None:
|
||||
meta = metadata.copy() if metadata else {}
|
||||
meta['user_id'] = user_id
|
||||
user_profile = UserProfileItem(key=key, value=value)
|
||||
super().__init__(content=user_profile, metadata=meta, memory_type="user_profile", **kwargs)
|
||||
|
||||
@property
|
||||
def user_id(self) -> str:
|
||||
return self.metadata['user_id']
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
return self.content.key
|
||||
|
||||
@property
|
||||
def value(self) -> Any:
|
||||
return self.content.value
|
||||
|
||||
@property
|
||||
def item(self) -> UserProfileItem:
|
||||
return self.content
|
||||
|
||||
@property
|
||||
def embedding_text(self):
|
||||
return f"key:{self.key} value:{self.value}"
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": "system",
|
||||
"content": self.content
|
||||
}
|
||||
|
||||
class Fact(MemoryItem):
|
||||
"""
|
||||
Represents Fact from conversation.
|
||||
Args:
|
||||
user_id (str): The ID of the user.
|
||||
content (str): fact.
|
||||
metadata (Optional[Dict[str, Any]]): Additional metadata.
|
||||
"""
|
||||
def __init__(self, user_id: str = None, agent_id: str = None, content: str = None, metadata: Optional[Dict[str, Any]] = None, **kwargs) -> None:
|
||||
meta = metadata.copy() if metadata else {}
|
||||
if user_id:
|
||||
meta['user_id'] = user_id
|
||||
elif metadata.get('user_id'):
|
||||
meta['user_id'] = metadata.get('user_id')
|
||||
|
||||
if 'memory_type' in kwargs:
|
||||
kwargs.pop("memory_type")
|
||||
super().__init__(content=content, metadata=meta, memory_type="fact", **kwargs)
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
return self.content.key
|
||||
|
||||
@property
|
||||
def value(self) -> Any:
|
||||
return self.content.value
|
||||
|
||||
@property
|
||||
def embedding_text(self):
|
||||
return self.content
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": "user",
|
||||
"content": self.content
|
||||
}
|
||||
|
||||
class MemorySummary(MemoryItem):
|
||||
"""
|
||||
Represents a memory summary.
|
||||
All custom attributes are stored in content and metadata.
|
||||
Args:
|
||||
item_ids (str): The IDS of the agent.
|
||||
summary (str): The summary text.
|
||||
metadata (Optional[Dict[str, Any]]): Additional metadata.
|
||||
"""
|
||||
def __init__(self, item_ids: list[str], summary: str, metadata: MessageMetadata, **kwargs) -> None:
|
||||
meta = metadata.to_dict
|
||||
meta['item_ids'] = item_ids
|
||||
meta['role'] = "user"
|
||||
super().__init__(content=summary, metadata=meta, memory_type="summary", **kwargs)
|
||||
|
||||
@property
|
||||
def summary_item_ids(self):
|
||||
return self.metadata['item_ids']
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": "user",
|
||||
"content": self.content
|
||||
}
|
||||
|
||||
|
||||
class ConversationSummary(MemoryItem):
|
||||
"""
|
||||
Represents a conversation summary.
|
||||
All custom attributes are stored in content and metadata.
|
||||
Args:
|
||||
user_id (str): The ID of the user.
|
||||
session_id (str): The ID of the session.
|
||||
summary (str): The summary text of the conversation.
|
||||
metadata (MessageMetadata): Metadata object containing additional information.
|
||||
"""
|
||||
|
||||
def __init__(self, user_id: str, session_id: str, summary: str, metadata: MessageMetadata, **kwargs) -> None:
|
||||
meta = metadata.to_dict
|
||||
meta['user_id'] = user_id
|
||||
meta['session_id'] = session_id
|
||||
super().__init__(content=summary, metadata=meta, memory_type="conversation_summary", **kwargs)
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": "assistant",
|
||||
"content": self.content
|
||||
}
|
||||
|
||||
|
||||
class MemoryMessage(MemoryItem):
|
||||
"""
|
||||
Represents a memory message with role, user, session, task, and agent information.
|
||||
Args:
|
||||
role (str): The role of the message sender.
|
||||
metadata (MessageMetadata): Metadata object containing user, session, task, and agent IDs.
|
||||
content (Optional[Any]): Content of the message.
|
||||
"""
|
||||
def __init__(self, role: str, metadata: MessageMetadata, content: Optional[Any] = None, memory_type="message", **kwargs) -> None:
|
||||
meta = metadata.to_dict
|
||||
meta['role'] = role
|
||||
super().__init__(content=content, metadata=meta, memory_type=memory_type, **kwargs)
|
||||
|
||||
@property
|
||||
def role(self) -> str:
|
||||
return self.metadata['role']
|
||||
|
||||
@property
|
||||
def user_id(self) -> str:
|
||||
return self.metadata['user_id']
|
||||
|
||||
@property
|
||||
def session_id(self) -> str:
|
||||
return self.metadata['session_id']
|
||||
|
||||
@property
|
||||
def task_id(self) -> str:
|
||||
return self.metadata['task_id']
|
||||
|
||||
def set_task_id(self, task_id):
|
||||
self.metadata['task_id'] = task_id
|
||||
|
||||
@property
|
||||
def agent_id(self) -> str:
|
||||
return self.metadata['agent_id']
|
||||
|
||||
@abstractmethod
|
||||
def to_openai_message(self) -> dict:
|
||||
pass
|
||||
|
||||
class MemorySystemMessage(MemoryMessage):
|
||||
"""
|
||||
Represents a system message with role and content.
|
||||
Args:
|
||||
metadata (MessageMetadata): Metadata object containing user, session, task, and agent IDs.
|
||||
content (str): The content of the message.
|
||||
"""
|
||||
def __init__(self, content: str, metadata: MessageMetadata, **kwargs) -> None:
|
||||
super().__init__(role="system", metadata=metadata, content=content, memory_type="init", **kwargs)
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": self.role,
|
||||
"content": self.content
|
||||
}
|
||||
|
||||
@property
|
||||
def embedding_text(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
class MemoryHumanMessage(MemoryMessage):
|
||||
"""
|
||||
Represents a human message with role and content.
|
||||
Args:
|
||||
metadata (MessageMetadata): Metadata object containing user, session, task, and agent IDs.
|
||||
content (str): The content of the message.
|
||||
"""
|
||||
def __init__(self, metadata: MessageMetadata, content: Any, memory_type = "init", **kwargs) -> None:
|
||||
super().__init__(role="user", metadata=metadata, content=content, memory_type=memory_type, **kwargs)
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": self.role,
|
||||
"content": self.content
|
||||
}
|
||||
|
||||
class MemoryAIMessage(MemoryMessage):
|
||||
"""
|
||||
Represents an AI message with role and content.
|
||||
Args:
|
||||
metadata (MessageMetadata): Metadata object containing user, session, task, and agent IDs.
|
||||
content (str): The content of the message.
|
||||
"""
|
||||
def __init__(self, content: str, tool_calls: Optional[List[ToolCall]] = [], metadata: MessageMetadata = None, **kwargs) -> None:
|
||||
meta = metadata.to_dict
|
||||
if tool_calls:
|
||||
meta['tool_calls'] = [tool_call.to_dict() for tool_call in tool_calls]
|
||||
super().__init__(role="assistant", metadata=MessageMetadata(**meta), content=content, **kwargs)
|
||||
|
||||
@property
|
||||
def tool_calls(self) -> List[ToolCall]:
|
||||
if "tool_calls" not in self.metadata or not self.metadata['tool_calls']:
|
||||
return None
|
||||
tc = [ToolCall(**tool_call) for tool_call in self.metadata['tool_calls']]
|
||||
return tc if len(tc) > 0 else None
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": self.role,
|
||||
"content": self.content,
|
||||
"tool_calls": [tool_call.to_dict() for tool_call in self.tool_calls or []] or None
|
||||
}
|
||||
|
||||
class MemoryToolMessage(MemoryMessage):
|
||||
"""
|
||||
Represents a tool message with role, content, tool_call_id, and status.
|
||||
Args:
|
||||
metadata (MessageMetadata): Metadata object containing user, session, task, and agent IDs.
|
||||
tool_call_id (str): The ID of the tool call.
|
||||
status (Literal["success", "error"]): The status of the tool call.
|
||||
content (str): The content of the message.
|
||||
"""
|
||||
def __init__(self, tool_call_id: str, content: Any, status: Literal["success", "error"] = "success", metadata: MessageMetadata = None, **kwargs) -> None:
|
||||
metadata.tool_call_id = tool_call_id
|
||||
metadata.status = status
|
||||
super().__init__(role="tool", metadata=metadata, content=content, **kwargs)
|
||||
|
||||
@property
|
||||
def tool_call_id(self) -> str:
|
||||
return self.metadata['tool_call_id']
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.metadata['status']
|
||||
|
||||
@property
|
||||
def embedding_text(self) -> Optional[str]:
|
||||
return None
|
||||
|
||||
def to_openai_message(self) -> dict:
|
||||
return {
|
||||
"role": self.role,
|
||||
"content": self.content,
|
||||
"tool_call_id": self.tool_call_id,
|
||||
}
|
||||
|
||||
|
||||
class LongTermExtractParams(BaseModel):
|
||||
session_id: str = Field(description="The ID of the session")
|
||||
task_id: Optional[str] = Field(description="The ID of the task")
|
||||
memories: List[MemoryItem] = Field(default_factory=list, description="The list of memories to process")
|
||||
|
||||
application_id: Optional[str] = Field(default=None, description="The ID of the application")
|
||||
extract_type: Literal["user_profile", "agent_experience"] = Field(description="The type of long-term extract")
|
||||
|
||||
def to_openai_messages(self) -> List[dict]:
|
||||
return [memory.to_openai_message() for memory in self.memories]
|
||||
|
||||
class UserProfileExtractParams(LongTermExtractParams):
|
||||
user_id: Optional[str] = Field(description="The ID of the user")
|
||||
|
||||
def __init__(self, user_id: str, session_id: str, task_id: str, memories: List[MemoryItem] = None, application_id: str = None, **kwargs) -> None:
|
||||
kwargs = {
|
||||
"user_id": user_id,
|
||||
"session_id": session_id,
|
||||
"task_id": task_id,
|
||||
"memories": memories or [],
|
||||
"application_id": application_id,
|
||||
"extract_type": "user_profile",
|
||||
**kwargs
|
||||
}
|
||||
super().__init__(**kwargs)
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
class AgentExperienceExtractParams(LongTermExtractParams):
|
||||
agent_id: str = Field(default=None, description="The ID of the agent")
|
||||
|
||||
def __init__(self, agent_id: str, session_id: str, task_id: str, memories: List[MemoryItem] = None,
|
||||
application_id: str = None,**kwargs) -> None:
|
||||
super().__init__(session_id=session_id,
|
||||
task_id=task_id,
|
||||
memories=memories,
|
||||
application_id=application_id,
|
||||
extract_type="agent_experience", **kwargs)
|
||||
self.agent_id = agent_id
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
class LongTermMemoryTriggerParams(BaseModel):
|
||||
"""
|
||||
Metadata for memory messages, including user, session, task, and agent information.
|
||||
Args:
|
||||
user_id (str): The ID of the user.
|
||||
session_id (str): The ID of the session.
|
||||
task_id (str): The ID of the task.
|
||||
agent_id (str): The ID of the agent.
|
||||
"""
|
||||
agent_id: str = Field(default=None, description="The ID of the agent")
|
||||
session_id: str = Field(default=None, description="The ID of the session")
|
||||
task_id: str = Field(default=None, description="The ID of the task")
|
||||
user_id: Optional[str] = Field(default=None, description="The ID of the user")
|
||||
application_id: Optional[str] = Field(default="default", description="The ID of the application, namespace for memory")
|
||||
force: Optional[bool] = Field(default=False, description="Whether to force trigger long-term memory")
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
from aworld.core.memory import MemoryItem
|
||||
|
||||
|
||||
def build_history_context(history_messages: list[MemoryItem]) -> str:
|
||||
"""
|
||||
Build history context from history messages.
|
||||
"""
|
||||
history_context = ""
|
||||
for message in history_messages:
|
||||
if message.role == "user":
|
||||
history_context += f"User: {message.content}\n"
|
||||
else:
|
||||
history_context += f"Agent: {message.content}\n"
|
||||
return history_context
|
||||
@@ -0,0 +1,123 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List
|
||||
|
||||
from aworld.memory.embeddings.base import EmbeddingsResults, EmbeddingsResult
|
||||
|
||||
|
||||
class VectorDB(ABC):
|
||||
"""Abstract base class for vector databases.
|
||||
|
||||
This class defines the standard interface that all vector database implementations
|
||||
must follow. It provides methods for storing, retrieving, and searching vectors.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
"""Check if a collection exists.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
|
||||
Returns:
|
||||
bool: True if collection exists, False otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete_collection(self, collection_name: str):
|
||||
"""Delete a collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection to delete
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], filter: dict, threshold: float, limit: int
|
||||
) -> Optional[EmbeddingsResults]:
|
||||
"""Search for nearest neighbors based on vector similarity.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
vectors (list[list[float | int]]): Query vectors
|
||||
filter (dict): Filter conditions
|
||||
threshold (float): Threshold for similarity search
|
||||
limit (int): Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
Optional[EmbeddingsResults]: Search results or None if collection doesn't exist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def query(
|
||||
self, collection_name: str, filter: dict, limit: Optional[int] = None
|
||||
) -> Optional[EmbeddingsResults]:
|
||||
"""Query items from the collection based on filter.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
filter (dict): Filter conditions
|
||||
limit (Optional[int]): Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
Optional[EmbeddingsResults]: Query results or None if collection doesn't exist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get(self, collection_name: str) -> Optional[EmbeddingsResults]:
|
||||
"""Get all items in the collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
|
||||
Returns:
|
||||
Optional[EmbeddingsResults]: All items in the collection or None if collection doesn't exist
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def insert(self, collection_name: str, items: list[EmbeddingsResult]):
|
||||
"""Insert items into the collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
items (list[EmbeddingsResult]): List of embedding results to insert
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def upsert(self, collection_name: str, items: list[EmbeddingsResult]):
|
||||
"""Update or insert items in the collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
items (list[EmbeddingsResult]): List of embedding results to upsert
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def delete(
|
||||
self,
|
||||
collection_name: str,
|
||||
ids: Optional[list[str]] = None,
|
||||
filter: Optional[dict] = None,
|
||||
):
|
||||
"""Delete items from the collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
ids (Optional[list[str]]): List of item IDs to delete
|
||||
filter (Optional[dict]): Filter conditions for items to delete
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset(self):
|
||||
"""Reset the database.
|
||||
|
||||
This will delete all collections and item entries.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,343 @@
|
||||
"""
|
||||
ChromaDB vector database implementation for aworld.
|
||||
|
||||
This implementation is based on the open-webui project's ChromaDB vector database code.
|
||||
Special thanks to the open-webui contributors for their excellent work.
|
||||
|
||||
Reference: https://github.com/open-webui/open-webui/blob/main/backend/open_webui/retrieval/vector/dbs/chroma.py
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from aworld.memory.embeddings.base import EmbeddingsResults, EmbeddingsMetadata, EmbeddingsResult
|
||||
from aworld.memory.vector.dbs.base import VectorDB
|
||||
|
||||
|
||||
class ChromaVectorDB(VectorDB):
|
||||
"""ChromaDB implementation of the VectorDB interface."""
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
import chromadb
|
||||
from chromadb import Settings
|
||||
settings_dict = {
|
||||
"allow_reset": True,
|
||||
"anonymized_telemetry": False,
|
||||
}
|
||||
if config.get('chroma_client_auth_provider') is not None:
|
||||
settings_dict["chroma_client_auth_provider"] = config.get('chroma_client_auth_provider')
|
||||
if config.get('chroma_client_auth_credentials') is not None:
|
||||
settings_dict["chroma_client_auth_credentials"] = config.get('chroma_client_auth_credentials')
|
||||
|
||||
if config.get('chroma_http_host') is not None:
|
||||
self.client = chromadb.HttpClient(
|
||||
host=config.get('chroma_http_host'),
|
||||
port=config.get('chroma_http_port'),
|
||||
headers=config.get('chroma_http_headers'),
|
||||
ssl=config.get('chroma_http_ssl'),
|
||||
tenant=config.get('chroma_tenant'),
|
||||
database=config.get('chroma_database'),
|
||||
settings=Settings(**settings_dict),
|
||||
)
|
||||
else:
|
||||
from chromadb import DEFAULT_TENANT
|
||||
from chromadb import DEFAULT_DATABASE
|
||||
self.client = chromadb.PersistentClient(
|
||||
path=config.get('chroma_data_path'),
|
||||
settings=Settings(**settings_dict),
|
||||
tenant=config.get('chroma_tenant', DEFAULT_TENANT),
|
||||
database=config.get('chroma_database', DEFAULT_DATABASE),
|
||||
)
|
||||
|
||||
def has_collection(self, collection_name: str) -> bool:
|
||||
# Check if the collection exists based on the collection name.
|
||||
collection_names = [collection.name for collection in self.client.list_collections()]
|
||||
return collection_name in collection_names
|
||||
|
||||
def delete_collection(self, collection_name: str):
|
||||
# Delete the collection based on the collection name.
|
||||
return self.client.delete_collection(name=collection_name)
|
||||
|
||||
def search(
|
||||
self, collection_name: str, vectors: list[list[float | int]], filter: dict, threshold: float, limit: int
|
||||
) -> Optional[EmbeddingsResults]:
|
||||
"""Search for nearest neighbors based on vector similarity.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
vectors (list[list[float | int]]): Query vectors
|
||||
filter (dict): Filter conditions using ChromaDB operators ($eq, $and, etc.)
|
||||
threshold (float): Similarity threshold
|
||||
limit (int): Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
Optional[EmbeddingsResults]: Search results or None if collection doesn't exist
|
||||
"""
|
||||
try:
|
||||
if not self.has_collection(collection_name):
|
||||
return []
|
||||
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
# Convert simple key-value filters to ChromaDB operator format
|
||||
where_conditions = []
|
||||
if filter:
|
||||
for key, value in filter.items():
|
||||
if value:
|
||||
where_conditions.append({key: {"$eq": value}})
|
||||
where_filter = {"$and": where_conditions} if len(where_conditions) > 1 else where_conditions[0]
|
||||
else:
|
||||
where_filter = None
|
||||
|
||||
result = collection.query(
|
||||
query_embeddings=vectors,
|
||||
where=where_filter,
|
||||
n_results=limit,
|
||||
)
|
||||
|
||||
# chromadb has cosine distance, 2 (worst) -> 0 (best). Re-ordering to 0 -> 1
|
||||
# https://docs.trychroma.com/docs/collections/configure cosine equation
|
||||
distances: list = result["distances"][0]
|
||||
distances = [2 - dist for dist in distances]
|
||||
distances = [[dist / 2 for dist in distances]]
|
||||
|
||||
docs = self._convert2_embedding_result_with_score(result=result, distances=distances,
|
||||
threshold=threshold)
|
||||
|
||||
return EmbeddingsResults(
|
||||
**{
|
||||
"docs": docs,
|
||||
"retrieved_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
return None
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logging.error(f"Error in search: {e}")
|
||||
return None
|
||||
|
||||
def query(
|
||||
self, collection_name: str, filter: dict, limit: Optional[int] = None
|
||||
) -> Optional[EmbeddingsResults]:
|
||||
"""Query items from the collection based on filter.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
filter (dict): Filter conditions
|
||||
limit (Optional[int]): Maximum number of results to return
|
||||
|
||||
Returns:
|
||||
Optional[EmbeddingsResults]: Query results or None if collection doesn't exist
|
||||
"""
|
||||
try:
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
where_conditions = []
|
||||
if filter:
|
||||
for key, value in filter.items():
|
||||
if value:
|
||||
where_conditions.append({key: {"$eq": value}})
|
||||
where_filter = {"$and": where_conditions} if len(where_conditions) > 1 else where_conditions[0]
|
||||
else:
|
||||
where_filter = None
|
||||
|
||||
result = collection.get(
|
||||
where=where_filter,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
docs = self._convert2EmbeddingResult(result)
|
||||
|
||||
return EmbeddingsResults(
|
||||
**{
|
||||
"docs": docs,
|
||||
"retrieved_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def get(self, collection_name: str) -> Optional[EmbeddingsResults]:
|
||||
"""Get all items in the collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
|
||||
Returns:
|
||||
Optional[EmbeddingsResults]: All items in the collection or None if collection doesn't exist
|
||||
"""
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
result = collection.get()
|
||||
docs = self._convert2EmbeddingResult(result)
|
||||
return EmbeddingsResults(
|
||||
**{
|
||||
"docs": docs,
|
||||
"retrieved_at": int(time.time()),
|
||||
}
|
||||
)
|
||||
return None
|
||||
|
||||
def _convert2_embedding_result_with_score(self, result, distances=None, threshold=None):
|
||||
"""Convert ChromaDB result to list of EmbeddingsResult.
|
||||
|
||||
Args:
|
||||
result (dict): ChromaDB query result containing documents, metadatas and ids
|
||||
distances (Optional[List[List[float]]]): Similarity scores from search results
|
||||
|
||||
Returns:
|
||||
list[EmbeddingsResult]: List of embedding results with content and metadata
|
||||
"""
|
||||
|
||||
docs = []
|
||||
# Flatten distances if provided (ChromaDB returns nested list)
|
||||
scores = distances[0] if distances else [None] * len(result.get("documents", []))
|
||||
|
||||
# ChromaDB returns nested lists for all fields
|
||||
documents = result.get("documents", [[]])[0]
|
||||
metadatas = result.get("metadatas", [[]])[0]
|
||||
ids = result.get("ids", [[]])[0]
|
||||
|
||||
for document, metadata, id, score in zip(
|
||||
documents,
|
||||
metadatas,
|
||||
ids,
|
||||
scores
|
||||
):
|
||||
# Metadata is already a dict since we stored it that way
|
||||
metadata_obj = EmbeddingsMetadata.model_validate(metadata)
|
||||
if threshold and score < threshold:
|
||||
continue
|
||||
|
||||
docs.append(
|
||||
EmbeddingsResult(
|
||||
id=id,
|
||||
embedding=None, # We don't need embeddings for retrieved results
|
||||
content=document,
|
||||
metadata=metadata_obj,
|
||||
score=score
|
||||
)
|
||||
)
|
||||
return docs
|
||||
|
||||
def _convert2EmbeddingResult(self, result):
|
||||
"""Convert ChromaDB result to list of EmbeddingsResult.
|
||||
|
||||
Args:
|
||||
result (dict): ChromaDB query result containing documents, metadatas and ids
|
||||
distances (Optional[List[List[float]]]): Similarity scores from search results
|
||||
|
||||
Returns:
|
||||
list[EmbeddingsResult]: List of embedding results with content and metadata
|
||||
"""
|
||||
|
||||
docs = []
|
||||
|
||||
# ChromaDB returns nested lists for all fields
|
||||
documents = result.get("documents", [])
|
||||
metadatas = result.get("metadatas", [])
|
||||
ids = result.get("ids", [])
|
||||
|
||||
for document, metadata, id in zip(
|
||||
documents,
|
||||
metadatas,
|
||||
ids
|
||||
):
|
||||
# Metadata is already a dict since we stored it that way
|
||||
metadata_obj = EmbeddingsMetadata.model_validate(metadata)
|
||||
|
||||
docs.append(
|
||||
EmbeddingsResult(
|
||||
id=id,
|
||||
embedding=None, # We don't need embeddings for retrieved results
|
||||
content=document,
|
||||
metadata=metadata_obj,
|
||||
score=None
|
||||
)
|
||||
)
|
||||
return docs
|
||||
|
||||
def insert(self, collection_name: str, items: list[EmbeddingsResult]):
|
||||
"""Insert the items into the collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
items (list[EmbeddingsResult]): List of embedding results to insert
|
||||
"""
|
||||
collection = self.client.get_or_create_collection(
|
||||
name=collection_name, metadata={"hnsw:space": "cosine"}
|
||||
)
|
||||
|
||||
ids = [item.id for item in items]
|
||||
documents = [item.content for item in items]
|
||||
embeddings = [item.embedding for item in items]
|
||||
# Convert metadata to dict and remove None values
|
||||
metadatas = []
|
||||
for item in items:
|
||||
metadata_dict = item.metadata.model_dump()
|
||||
# Remove None values and convert all values to strings to ensure compatibility
|
||||
cleaned_metadata = {
|
||||
k: str(v) if v is not None else ""
|
||||
for k, v in metadata_dict.items()
|
||||
}
|
||||
metadatas.append(cleaned_metadata)
|
||||
|
||||
from chromadb.utils.batch_utils import create_batches
|
||||
for batch in create_batches(
|
||||
api=self.client,
|
||||
documents=documents,
|
||||
embeddings=embeddings,
|
||||
ids=ids,
|
||||
metadatas=metadatas,
|
||||
):
|
||||
collection.add(*batch)
|
||||
|
||||
def upsert(self, collection_name: str, items: list[EmbeddingsResult]):
|
||||
"""Update or insert items in the collection.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the collection
|
||||
items (list[EmbeddingsResult]): List of embedding results to upsert
|
||||
"""
|
||||
collection = self.client.get_or_create_collection(
|
||||
name=collection_name, metadata={"hnsw:space": "cosine"}
|
||||
)
|
||||
|
||||
ids = [item.id for item in items]
|
||||
documents = [item.content for item in items]
|
||||
embeddings = [item.embedding for item in items]
|
||||
# Convert metadata to dict instead of JSON string
|
||||
metadatas = [item.metadata.model_dump() for item in items]
|
||||
|
||||
collection.upsert(
|
||||
ids=ids, documents=documents, embeddings=embeddings, metadatas=metadatas
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
collection_name: str,
|
||||
ids: Optional[list[str]] = None,
|
||||
filter: Optional[dict] = None,
|
||||
):
|
||||
# Delete the items from the collection based on the ids.
|
||||
try:
|
||||
collection = self.client.get_collection(name=collection_name)
|
||||
if collection:
|
||||
if ids:
|
||||
collection.delete(ids=ids)
|
||||
elif filter:
|
||||
collection.delete(where=filter)
|
||||
else:
|
||||
self.client.delete_collection(name=collection_name)
|
||||
except Exception as e:
|
||||
# If collection doesn't exist, that's fine - nothing to delete
|
||||
logging.debug(
|
||||
f"Attempted to delete from non-existent collection {collection_name}. Ignoring."
|
||||
)
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
# Resets the database. This will delete all collections and item entries.
|
||||
return self.client.reset()
|
||||
@@ -0,0 +1,22 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from aworld.core.memory import VectorDBConfig
|
||||
from aworld.memory.vector.dbs.base import VectorDB
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class VectorDBFactory:
|
||||
|
||||
@staticmethod
|
||||
def get_vector_db(vector_db_config: VectorDBConfig) -> Optional[VectorDB]:
|
||||
if not vector_db_config:
|
||||
return None
|
||||
if vector_db_config.provider == "chroma":
|
||||
from aworld.memory.vector.dbs.chroma import ChromaVectorDB
|
||||
return ChromaVectorDB(vector_db_config.config)
|
||||
else:
|
||||
raise ValueError(f"Vector database {vector_db_config.provider} is not supported")
|
||||
Reference in New Issue
Block a user