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

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
@@ -0,0 +1,345 @@
# Memory
![](../../readme_assets/arch_memory.png)
## 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")