ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# Replay Buffer
|
||||
|
||||
A multi-process capable replay buffer system for storing and sampling experience data.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-process Support**: Safe concurrent access using shared memory and locks
|
||||
- **Flexible Querying**: Powerful query builder for filtering stored data
|
||||
- **Task-based Organization**: Data organized by task_id and agent_id
|
||||
- **Capacity Management**: FIFO eviction when reaching max capacity
|
||||
- **Custom Sampling**: Implement custom sampling logic through Sampler interface
|
||||
- **Data Conversion**: Custom data conversion through Converter interface
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Writing Data
|
||||
|
||||
```python
|
||||
from aworld.replay_buffer import ReplayBuffer, DataRow, ExpMeta, Experience
|
||||
from aworld.core.common import ActionModel, Observation
|
||||
|
||||
# Create a data row
|
||||
data = DataRow(
|
||||
exp_meta=ExpMeta(
|
||||
task_id="task_1",
|
||||
task_name="my_task",
|
||||
agent_id="agent_1",
|
||||
step=1,
|
||||
execute_time=time.time()
|
||||
),
|
||||
exp_data=Experience(
|
||||
state=Observation(),
|
||||
action=ActionModel()
|
||||
)
|
||||
)
|
||||
|
||||
# Store data
|
||||
replay_buffer.store(data)
|
||||
```
|
||||
|
||||
### Reading Data
|
||||
|
||||
```python
|
||||
from aworld.replay_buffer.query_filter import QueryBuilder
|
||||
|
||||
# Basic example
|
||||
replay_buffer = ReplayBuffer()
|
||||
query_condition = QueryBuilder().eq("exp_meta.task_name", "test_task").build()
|
||||
data = replay_buffer.sample(sampler=RandomTaskSample(),
|
||||
query_condition=query_condition,
|
||||
converter=DefaultConverter(),
|
||||
batch_size=1000)
|
||||
|
||||
# Query Task by task_id
|
||||
query = QueryBuilder().eq("exp_meta.task_id", "task_1").build()
|
||||
data = replay_buffer.sample_task(query_condition=query, batch_size=10)
|
||||
|
||||
# Query Task by agent_id
|
||||
query = QueryBuilder().eq("exp_meta.agent_id", "agent_1").build()
|
||||
data = replay_buffer.sample_task(query_condition=query, batch_size=5)
|
||||
```
|
||||
## Multi-processing Example
|
||||
|
||||
```python
|
||||
import multiprocessing
|
||||
from aworld.replay_buffer.storage.multi_proc_mem import MultiProcMemoryStorage
|
||||
|
||||
manager = multiprocessing.Manager()
|
||||
replay_buffer = ReplayBuffer(
|
||||
storage=MultiProcMemoryStorage(
|
||||
data_dict=manager.dict(),
|
||||
fifo_queue=manager.list(),
|
||||
lock=manager.Lock(),
|
||||
max_capacity=10000
|
||||
)
|
||||
)
|
||||
|
||||
# Start writer processes
|
||||
processes = [
|
||||
multiprocessing.Process(target=write_processing, args=(replay_buffer, f"task_{i}"))
|
||||
for i in range(4)
|
||||
]
|
||||
```
|
||||
## Query Builder Examples
|
||||
|
||||
### Simple Equality
|
||||
```python
|
||||
QueryBuilder().eq("exp_meta.task_id", "123").build()
|
||||
```
|
||||
|
||||
### Complex Conditions
|
||||
```python
|
||||
QueryBuilder()
|
||||
.eq("exp_meta.task_id", "123")
|
||||
.and_()
|
||||
.eq("exp_meta.agent_id", "456")
|
||||
.build()
|
||||
```
|
||||
### Nested Conditions
|
||||
```python
|
||||
QueryBuilder()
|
||||
.eq("exp_meta.task_id", "123")
|
||||
.and_()
|
||||
.nested(
|
||||
QueryBuilder()
|
||||
.eq("exp_meta.agent_id", "111")
|
||||
.or_()
|
||||
.eq("exp_meta.agent_id", "222")
|
||||
)
|
||||
.build()
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
|
||||
from aworld.replay_buffer.base import ReplayBuffer, DataRow, ExpMeta, Experience
|
||||
from aworld.replay_buffer.event_replay_buffer import EventReplayBuffer
|
||||
|
||||
__all__ = [
|
||||
'ReplayBuffer',
|
||||
'DataRow',
|
||||
'ExpMeta',
|
||||
'Experience',
|
||||
'EventReplayBuffer',
|
||||
]
|
||||
@@ -0,0 +1,410 @@
|
||||
import random
|
||||
import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, TypeVar
|
||||
from abc import ABC, abstractmethod
|
||||
from math import ceil
|
||||
|
||||
from aworld.core.common import ActionModel, Observation
|
||||
from aworld.replay_buffer.query_filter import QueryCondition, QueryFilter
|
||||
from aworld.logs.util import logger
|
||||
from aworld.utils.serialized_util import to_serializable
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
@dataclass
|
||||
class Experience:
|
||||
'''
|
||||
Experience of agent.
|
||||
'''
|
||||
state: Observation
|
||||
actions: List[ActionModel]
|
||||
reward_t: float = None
|
||||
adv_t: float = None
|
||||
v_t: float = None
|
||||
messages: List[Dict] = None
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"state": to_serializable(self.state),
|
||||
"actions": to_serializable(self.actions),
|
||||
"reward_t": self.reward_t,
|
||||
"adv_t": self.adv_t,
|
||||
"v_t": self.v_t,
|
||||
"messages": self.messages
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpMeta:
|
||||
'''
|
||||
Experience meta data.
|
||||
'''
|
||||
task_id: str
|
||||
task_name: str
|
||||
agent_id: str
|
||||
step: int
|
||||
execute_time: float
|
||||
pre_agent: str
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"task_name": self.task_name,
|
||||
"agent_id": self.agent_id,
|
||||
"step": self.step,
|
||||
"execute_time": self.execute_time,
|
||||
"pre_agent": self.pre_agent
|
||||
}
|
||||
@dataclass
|
||||
class DataRow:
|
||||
'''
|
||||
Data row for storing data.
|
||||
'''
|
||||
exp_meta: ExpMeta
|
||||
exp_data: Experience
|
||||
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"exp_meta": self.exp_meta.to_dict(),
|
||||
"exp_data": self.exp_data.to_dict(),
|
||||
"id": self.id
|
||||
}
|
||||
|
||||
|
||||
class Storage(ABC):
|
||||
'''
|
||||
Storage for storing and sampling data.
|
||||
'''
|
||||
|
||||
@abstractmethod
|
||||
def add(self, data: DataRow):
|
||||
'''
|
||||
Add data to the storage.
|
||||
Args:
|
||||
data (DataRow): Data to add.
|
||||
'''
|
||||
|
||||
@abstractmethod
|
||||
def add_batch(self, data_batch: List[DataRow]):
|
||||
'''
|
||||
Add batch of data to the storage.
|
||||
Args:
|
||||
data_batch (List[DataRow]): List of data to add.
|
||||
'''
|
||||
|
||||
@abstractmethod
|
||||
def size(self, query_condition: QueryCondition = None) -> int:
|
||||
'''
|
||||
Get the size of the storage.
|
||||
Returns:
|
||||
int: Size of the storage.
|
||||
'''
|
||||
|
||||
@abstractmethod
|
||||
def get_paginated(self, page: int, page_size: int, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
'''
|
||||
Get paginated data from the storage.
|
||||
Args:
|
||||
page (int): Page number.
|
||||
page_size (int): Number of data per page.
|
||||
Returns:
|
||||
List[DataRow]: List of data.
|
||||
'''
|
||||
|
||||
@abstractmethod
|
||||
def get_all(self, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
'''
|
||||
Get all data from the storage.
|
||||
Returns:
|
||||
List[DataRow]: List of data.
|
||||
'''
|
||||
|
||||
@abstractmethod
|
||||
def get_by_task_id(self, task_id: str) -> List[DataRow]:
|
||||
'''
|
||||
Get data by task_id from the storage.
|
||||
Args:
|
||||
task_id (str): Task id.
|
||||
Returns:
|
||||
List[DataRow]: List of data.
|
||||
'''
|
||||
|
||||
@abstractmethod
|
||||
def get_bacth_by_task_ids(self, task_ids: List[str]) -> Dict[str, List[DataRow]]:
|
||||
'''
|
||||
Get batch of data by task_ids from the storage.
|
||||
Args:
|
||||
task_ids (List[str]): List of task ids.
|
||||
Returns:
|
||||
Dict[str, List[DataRow]]: Dictionary of data.
|
||||
The key is the task_id and the value is the list of data.
|
||||
The list of data is sorted by step.
|
||||
'''
|
||||
|
||||
|
||||
class Sampler(ABC):
|
||||
'''
|
||||
Sample data from the storage.
|
||||
'''
|
||||
|
||||
def sample(self,
|
||||
storage: Storage,
|
||||
batch_size: int,
|
||||
query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
'''
|
||||
Sample data from the storage.
|
||||
Args:
|
||||
storage (Storage): Storage to sample from.
|
||||
batch_size (int): Number of data to sample.
|
||||
query_condition (QueryCondition, optional): Query condition. Defaults to None.
|
||||
Returns:
|
||||
List[DataRow]
|
||||
'''
|
||||
|
||||
|
||||
class TaskSampler(Sampler):
|
||||
'''
|
||||
Sample task data from storage, returns Dict[str, List[DataRow]] where:
|
||||
- key is task_id
|
||||
- value is list of task all data rows
|
||||
'''
|
||||
|
||||
def sorted_by_step(self, task_experience: List[DataRow]) -> List[DataRow]:
|
||||
'''
|
||||
Sort the task experience by step and execute_time.
|
||||
Args:
|
||||
task_experience (List[DataRow]): List of task experience.
|
||||
Returns:
|
||||
List[DataRow]: List of task experience sorted by step and execute_time.
|
||||
'''
|
||||
return sorted(task_experience, key=lambda x: (x.exp_meta.step, x.exp_meta.execute_time))
|
||||
|
||||
def sample(self,
|
||||
storage: Storage,
|
||||
batch_size: int,
|
||||
query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
task_ids = self.sample_task_ids(storage, batch_size, query_condition)
|
||||
return storage.get_bacth_by_task_ids(task_ids)
|
||||
|
||||
def sample_tasks(self,
|
||||
storage: Storage,
|
||||
batch_size: int,
|
||||
query_condition: QueryCondition = None) -> Dict[str, List[DataRow]]:
|
||||
'''
|
||||
Sample data from the storage.
|
||||
Args:
|
||||
storage (Storage): Storage to sample from.
|
||||
batch_size (int): Number of data to sample.
|
||||
query_condition (QueryCondition, optional): Query condition. Defaults to None.
|
||||
Returns:
|
||||
Dict[str, List[DataRow]]: Dictionary of sampled data.
|
||||
The key is the task_id and the value is the list of data.
|
||||
The list of data is sorted by step.
|
||||
'''
|
||||
task_ids = self.sample_task_ids(storage, batch_size, query_condition)
|
||||
raws = storage.get_bacth_by_task_ids(task_ids)
|
||||
return {task_id: self.sorted_by_step(raws) for task_id, raws in raws.items()}
|
||||
|
||||
@abstractmethod
|
||||
def sample_task_ids(self,
|
||||
storage: Storage,
|
||||
batch_size: int,
|
||||
query_condition: QueryCondition = None) -> List[str]:
|
||||
'''
|
||||
Sample task_ids from the storage.
|
||||
Args:
|
||||
storage (Storage): Storage to sample from.
|
||||
batch_size (int): Number of task_ids to sample.
|
||||
query_condition (QueryCondition, optional): Query condition. Defaults to None.
|
||||
Returns:
|
||||
List[str]: List of task_ids.
|
||||
'''
|
||||
|
||||
|
||||
class Converter(ABC):
|
||||
'''
|
||||
Convert data to dataset row.
|
||||
'''
|
||||
|
||||
@abstractmethod
|
||||
def to_dataset_row(self, task_experience: List[DataRow]) -> T:
|
||||
'''
|
||||
Convert task experience to dataset row.
|
||||
Args:
|
||||
task_experience (List[DataRow]): List of task experience.
|
||||
Returns:
|
||||
T: type of dataset row.
|
||||
'''
|
||||
|
||||
|
||||
class InMemoryStorage(Storage):
|
||||
'''
|
||||
In-memory storage for storing and sampling data.
|
||||
'''
|
||||
|
||||
def __init__(self, max_capacity: int = 10000):
|
||||
self._data: Dict[str, List[DataRow]] = {}
|
||||
self._max_capacity = max_capacity
|
||||
self._fifo_queue = [] # (task_id)
|
||||
|
||||
def add(self, data: DataRow):
|
||||
if not data:
|
||||
raise ValueError("Data is required")
|
||||
if not data.exp_meta:
|
||||
raise ValueError("exp_meta is required")
|
||||
|
||||
while self.size() >= self._max_capacity and self._fifo_queue:
|
||||
oldest_task_id = self._fifo_queue.pop(0)
|
||||
if oldest_task_id in self._data:
|
||||
del self._data[oldest_task_id]
|
||||
|
||||
if data.exp_meta.task_id not in self._data:
|
||||
self._data[data.exp_meta.task_id] = []
|
||||
self._data[data.exp_meta.task_id].append(data)
|
||||
self._fifo_queue.append(data.exp_meta.task_id)
|
||||
|
||||
if data.exp_meta.task_id not in self._data:
|
||||
self._data[data.exp_meta.task_id] = []
|
||||
self._data[data.exp_meta.task_id].append(data)
|
||||
|
||||
def add_batch(self, data_batch: List[DataRow]):
|
||||
for data in data_batch:
|
||||
self.add(data)
|
||||
|
||||
def size(self, query_condition: QueryCondition = None) -> int:
|
||||
return len(self.get_all(query_condition))
|
||||
|
||||
def get_paginated(self, page: int, page_size: int, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
if page < 1:
|
||||
raise ValueError("Page must be greater than 0")
|
||||
if page_size < 1:
|
||||
raise ValueError("Page size must be greater than 0")
|
||||
all_data = self.get_all(query_condition)
|
||||
start_index = (page - 1) * page_size
|
||||
end_index = start_index + page_size
|
||||
return all_data[start_index:end_index]
|
||||
|
||||
def get_all(self, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
all_data = []
|
||||
query_filter = None
|
||||
if query_condition:
|
||||
query_filter = QueryFilter(query_condition)
|
||||
for data in self._data.values():
|
||||
if query_filter:
|
||||
all_data.extend(query_filter.filter(data))
|
||||
else:
|
||||
all_data.extend(data)
|
||||
return all_data
|
||||
|
||||
def get_by_task_id(self, task_id: str) -> List[DataRow]:
|
||||
return self._data.get(task_id, [])
|
||||
|
||||
def get_bacth_by_task_ids(self, task_ids: List[str]) -> Dict[str, List[DataRow]]:
|
||||
return {task_id: self._data.get(task_id, []) for task_id in task_ids}
|
||||
|
||||
def clear(self):
|
||||
self._data = {}
|
||||
self._fifo_queue = []
|
||||
|
||||
|
||||
class RandomTaskSample(TaskSampler):
|
||||
'''
|
||||
Randomly sample data from the storage.
|
||||
'''
|
||||
|
||||
def sample_task_ids(self,
|
||||
storage: Storage,
|
||||
batch_size: int,
|
||||
query_condition: QueryCondition = None) -> List[str]:
|
||||
total_size = storage.size(query_condition)
|
||||
if total_size <= batch_size:
|
||||
return storage.get_all(query_condition)
|
||||
|
||||
sampled_task_ids = set()
|
||||
page_size = min(100, batch_size * 2)
|
||||
total_pages = ceil(total_size/page_size)
|
||||
visited_pages = set()
|
||||
while len(sampled_task_ids) < batch_size and len(visited_pages) < total_pages:
|
||||
page = random.choice(
|
||||
[p for p in range(1, total_pages+1) if p not in visited_pages])
|
||||
visited_pages.add(page)
|
||||
|
||||
current_page = storage.get_paginated(
|
||||
page, page_size, query_condition)
|
||||
if not current_page:
|
||||
continue
|
||||
current_page_task_ids = set(
|
||||
[data.exp_meta.task_id for data in current_page if data.exp_meta.task_id not in sampled_task_ids])
|
||||
sample_count = min(len(current_page_task_ids),
|
||||
batch_size - len(sampled_task_ids))
|
||||
sampled_task_ids.update(random.sample(
|
||||
list(current_page_task_ids), sample_count))
|
||||
|
||||
return list(sampled_task_ids)
|
||||
|
||||
|
||||
class DefaultConverter(Converter):
|
||||
'''
|
||||
Default converter do nothing.
|
||||
'''
|
||||
|
||||
def to_dataset_row(self, task_experience: List[DataRow]) -> List[DataRow]:
|
||||
return task_experience
|
||||
|
||||
|
||||
class ReplayBuffer:
|
||||
'''
|
||||
Replay buffer for storing and sampling data.
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
storage: Storage = InMemoryStorage()
|
||||
):
|
||||
self._storage = storage
|
||||
|
||||
def store(self, data: DataRow):
|
||||
'''
|
||||
Store data in the replay buffer.
|
||||
'''
|
||||
if not data:
|
||||
raise ValueError("Data is required")
|
||||
self._storage.add(data)
|
||||
|
||||
def store_batch(self, data_batch: List[DataRow]):
|
||||
'''
|
||||
Store batch of data in the replay buffer.
|
||||
'''
|
||||
if not data_batch:
|
||||
logger.warning("Data batch is required")
|
||||
return
|
||||
self._storage.add_batch(data_batch)
|
||||
|
||||
def sample_task(self,
|
||||
sampler: TaskSampler = RandomTaskSample(),
|
||||
query_condition: QueryCondition = None,
|
||||
converter: Converter = DefaultConverter(),
|
||||
batch_size: int = 1000) -> List[T]:
|
||||
'''
|
||||
Sample Task from the replay buffer and convert to dataset row.
|
||||
DefaultConverter return List[DataRow]
|
||||
'''
|
||||
sampled_task = sampler.sample_tasks(
|
||||
self._storage, batch_size, query_condition)
|
||||
return [converter.to_dataset_row(task_experiences) for task_experiences in sampled_task.values()]
|
||||
|
||||
def sample(self,
|
||||
sampler: Sampler = RandomTaskSample(),
|
||||
query_condition: QueryCondition = None,
|
||||
converter: Converter = DefaultConverter(),
|
||||
batch_size: int = 1000) -> List[T]:
|
||||
'''
|
||||
Sample data from the replay buffer and convert to dataset row.
|
||||
DefaultConverter return List[DataRow]
|
||||
'''
|
||||
sampled_data = sampler.sample(
|
||||
self._storage, batch_size, query_condition)
|
||||
return converter.to_dataset_row(sampled_data)
|
||||
@@ -0,0 +1,218 @@
|
||||
import json
|
||||
import os.path
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from aworld import import_package
|
||||
from aworld.core.agent.base import is_agent_by_name
|
||||
from aworld.core.event.base import Message, Constants
|
||||
from aworld.logs.util import logger
|
||||
from aworld.replay_buffer.base import ReplayBuffer, DataRow, ExpMeta, Experience, InMemoryStorage, Storage
|
||||
from aworld.runners.state_manager import RuntimeStateManager, EventRuntimeStateManager
|
||||
from aworld.utils.serialized_util import to_serializable
|
||||
from aworld.utils.common import get_local_ip
|
||||
|
||||
|
||||
class EventReplayBuffer(ReplayBuffer):
|
||||
'''
|
||||
Event replay buffer for storing and sampling data.
|
||||
Adds the ability to build DataRow from messages and export data to files.
|
||||
'''
|
||||
def __init__(
|
||||
self,
|
||||
storage: Storage = InMemoryStorage()
|
||||
):
|
||||
super().__init__(storage)
|
||||
self.task_agent_map = {}
|
||||
|
||||
async def get_trajectory(self, messages: List[Message], task_id: str, state_mng: RuntimeStateManager = None) -> List[Dict[str, Any]] | None:
|
||||
if not messages:
|
||||
return None
|
||||
valid_agent_messages = await self._filter_replay_messages(messages, task_id)
|
||||
if not valid_agent_messages:
|
||||
return None
|
||||
data_rows = []
|
||||
try:
|
||||
for msg in valid_agent_messages:
|
||||
data_row = self.build_data_row_from_message(msg, state_mng)
|
||||
if data_row:
|
||||
data_rows.append(data_row)
|
||||
if not data_rows:
|
||||
logger.warn(f"No valid agent messages found for task: {task_id}")
|
||||
return None
|
||||
|
||||
self.store_batch(data_rows)
|
||||
trajectory = [to_serializable(data_row) for data_row in data_rows]
|
||||
|
||||
self.export(data_rows, task_id)
|
||||
|
||||
return trajectory
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save trajectories: {str(e)}.{traceback.format_exc()}")
|
||||
return None
|
||||
|
||||
async def _filter_replay_messages(self, messages: List[Message], task_id: str) -> List[Message]:
|
||||
results = []
|
||||
logger.info(f"Retrieving agent messages for task: {task_id}")
|
||||
for message in messages:
|
||||
if message.task_id != task_id or message.category != Constants.AGENT:
|
||||
continue
|
||||
sender = message.sender
|
||||
receiver = message.receiver
|
||||
if not sender or not receiver or not is_agent_by_name(receiver):
|
||||
continue
|
||||
agent_as_tool = message.headers.get("agent_as_tool", False)
|
||||
if agent_as_tool:
|
||||
continue
|
||||
results.append(message)
|
||||
return results
|
||||
|
||||
def build_data_row_from_message(self, message: Message, state_manager: RuntimeStateManager = None) -> DataRow:
|
||||
'''
|
||||
Build DataRow from a message.
|
||||
|
||||
Args:
|
||||
message (Dict): Message data containing necessary metadata and experience data
|
||||
|
||||
Returns:
|
||||
DataRow: The constructed data row
|
||||
|
||||
Raises:
|
||||
ValueError: When the message is missing required fields
|
||||
'''
|
||||
if not message:
|
||||
raise ValueError("Message cannot be empty")
|
||||
|
||||
agent_id = message.receiver
|
||||
task_id = message.context.task_id
|
||||
task_name = message.context.get_task().name
|
||||
pre_agent = message.sender
|
||||
task_agent_id = f"{task_id}_{agent_id}"
|
||||
if task_agent_id not in self.task_agent_map:
|
||||
self.task_agent_map[task_agent_id] = 0
|
||||
self.task_agent_map[task_agent_id] += 1
|
||||
id = f"{task_agent_id}_{self.task_agent_map[task_agent_id]}"
|
||||
|
||||
# Build ExpMeta
|
||||
exp_meta = ExpMeta(
|
||||
task_id=task_id,
|
||||
task_name=task_name,
|
||||
agent_id=agent_id,
|
||||
step=self.task_agent_map[task_agent_id],
|
||||
execute_time=message.timestamp,
|
||||
pre_agent=pre_agent
|
||||
)
|
||||
|
||||
if not state_manager:
|
||||
state_manager = EventRuntimeStateManager.instance()
|
||||
observation = message.payload
|
||||
node = state_manager._find_node(message.id)
|
||||
if node is None or not node.results:
|
||||
logger.error(f"Node result not found for message id: {message.id}, node: {node}")
|
||||
return None
|
||||
agent_results = []
|
||||
for handle_result in node.results:
|
||||
result = handle_result.result
|
||||
if isinstance(result, Message) and isinstance(result.payload, list):
|
||||
agent_results.extend(result.payload)
|
||||
messages = self._get_llm_messages_from_memory(message)
|
||||
|
||||
# Build Experience
|
||||
exp_data = Experience(
|
||||
state=observation,
|
||||
actions=agent_results,
|
||||
messages=messages
|
||||
)
|
||||
|
||||
# Build and return DataRow
|
||||
return DataRow(exp_meta=exp_meta, exp_data=exp_data, id=id)
|
||||
|
||||
def _get_llm_messages_from_memory(self, message: Message):
|
||||
context = message.context
|
||||
return context.context_info.get("llm_input", [])
|
||||
|
||||
def export(self, data_rows: List[DataRow], task_id: str) -> None:
|
||||
'''
|
||||
Export data rows to a specified file.
|
||||
|
||||
Args:
|
||||
data_rows (List[DataRow]): List of data rows to export
|
||||
filepath (str): Path of the export file
|
||||
|
||||
Raises:
|
||||
ValueError: When the data rows list is empty or the file path is invalid
|
||||
'''
|
||||
enable_file_export = os.getenv("EXPORT_REPLAY_FILES", "false").lower() == "true"
|
||||
enable_oss_export = os.getenv("EXPORT_REPLAY_TO_OSS", "false").lower() == "true"
|
||||
if not enable_file_export and not enable_oss_export:
|
||||
return
|
||||
|
||||
if not data_rows:
|
||||
logger.warn("Data rows list cannot be empty")
|
||||
return
|
||||
|
||||
try:
|
||||
# Convert data rows to dictionary list
|
||||
data_dicts = [to_serializable(data_row) for data_row in data_rows]
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d")
|
||||
export_dir = os.getenv('REPLAY_EXPORT_DIRECTORY', None)
|
||||
replay_dir = os.path.join(export_dir or "./trace_data", timestamp, get_local_ip(), "replays")
|
||||
os.makedirs(replay_dir, exist_ok=True)
|
||||
filepath = os.path.join(replay_dir, f"task_replay_{task_id}.json")
|
||||
|
||||
if enable_file_export:
|
||||
logger.info(f"Exporting {len(data_rows)} data rows to {filepath}")
|
||||
# Write to JSON file
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data_dicts, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"Successfully exported {len(data_rows)} data rows to {os.path.abspath(filepath)}")
|
||||
|
||||
if enable_oss_export:
|
||||
logger.info(f"Exporting {len(data_rows)} data rows to oss")
|
||||
self.export_to_oss(data_dicts, filepath)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to export replay datas: {e}")
|
||||
raise
|
||||
|
||||
def export_to_oss(self, datas, filepath):
|
||||
import_package("oss2")
|
||||
import oss2
|
||||
|
||||
# Get OSS credentials from environment variables
|
||||
access_key_id = os.getenv('OSS_ACCESS_KEY_ID')
|
||||
access_key_secret = os.getenv('OSS_ACCESS_KEY_SECRET')
|
||||
endpoint = os.getenv('OSS_ENDPOINT')
|
||||
bucket_name = os.getenv('OSS_BUCKET_NAME')
|
||||
bucket = None
|
||||
|
||||
if not all([access_key_id, access_key_secret, endpoint, bucket_name]):
|
||||
logger.warn("Missing required OSS environment variables")
|
||||
return
|
||||
else:
|
||||
try:
|
||||
# Initialize OSS client
|
||||
auth = oss2.Auth(access_key_id, access_key_secret)
|
||||
bucket = oss2.Bucket(auth, endpoint, bucket_name)
|
||||
except Exception as e:
|
||||
logger.warn(
|
||||
f"Failed to initialize OSS client, endpoint: {endpoint}, bucket: {bucket_name}. Error: {str(e)}")
|
||||
return
|
||||
|
||||
# Upload to OSS
|
||||
try:
|
||||
# Get the relative path
|
||||
abs_path = os.path.abspath(filepath)
|
||||
path_parts = abs_path.split(os.sep)
|
||||
if len(path_parts) >= 4:
|
||||
# Get the last 4 parts of the path
|
||||
relative_path = os.sep.join(path_parts[-4:])
|
||||
oss_key = relative_path
|
||||
else:
|
||||
oss_key = f"replay_buffer/{os.path.basename(filepath)}"
|
||||
logger.info(f"Uploading replay datas to OSS: {oss_key}")
|
||||
bucket.put_object_from_file(oss_key, filepath)
|
||||
logger.info(f"Successfully uploaded {filepath} to OSS: {oss_key}")
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to upload {filepath} to OSS: {str(e)}")
|
||||
@@ -0,0 +1,190 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
processor.py
|
||||
Used to clean raw trace data into standard storage structure for reinforcement learning training.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import datetime
|
||||
from typing import Any
|
||||
import threading
|
||||
|
||||
from aworld.utils import import_package
|
||||
from aworld.replay_buffer.base import DataRow, Experience, ExpMeta
|
||||
from aworld.logs.util import logger
|
||||
from aworld.utils.common import get_local_ip
|
||||
|
||||
|
||||
class ReplayBufferExporter:
|
||||
def __init__(self):
|
||||
"""Initialize ReplayBufferExporter instance"""
|
||||
self._file_locks = {}
|
||||
self._lock_dict_lock = threading.Lock()
|
||||
self._task_output_paths = {}
|
||||
|
||||
def _get_file_lock(self, file_path):
|
||||
"""Get the lock for the specified file"""
|
||||
with self._lock_dict_lock:
|
||||
if file_path not in self._file_locks:
|
||||
self._file_locks[file_path] = threading.Lock()
|
||||
return self._file_locks[file_path]
|
||||
|
||||
def replay_buffer_exporter(self, spans: list[dict[str, Any]], output_dir: str):
|
||||
"""
|
||||
Process spans, only process spans with 'step_execution_' prefix, and group by task_id to output to different files
|
||||
|
||||
Args:
|
||||
spans: span data list
|
||||
output_dir: output directory path
|
||||
"""
|
||||
# Ensure output directory exists
|
||||
import_package("oss2")
|
||||
import oss2
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Get OSS credentials from environment variables
|
||||
enable_oss_export = os.getenv("EXPORT_REPLAY_TRACE_TO_OSS", "false").lower() == "true"
|
||||
access_key_id = os.getenv('OSS_ACCESS_KEY_ID')
|
||||
access_key_secret = os.getenv('OSS_ACCESS_KEY_SECRET')
|
||||
endpoint = os.getenv('OSS_ENDPOINT')
|
||||
bucket_name = os.getenv('OSS_BUCKET_NAME')
|
||||
bucket = None
|
||||
|
||||
if not all([access_key_id, access_key_secret, endpoint, bucket_name]):
|
||||
enable_oss_export = False
|
||||
logger.warn("Missing required OSS environment variables")
|
||||
else:
|
||||
try:
|
||||
# Initialize OSS client
|
||||
auth = oss2.Auth(access_key_id, access_key_secret)
|
||||
bucket = oss2.Bucket(auth, endpoint, bucket_name)
|
||||
except Exception as e:
|
||||
enable_oss_export = False
|
||||
logger.warn(f"Failed to initialize OSS client, endpoint: {endpoint}, bucket: {bucket_name}. Error: {str(e)}")
|
||||
|
||||
# Group by task_id
|
||||
task_groups = {}
|
||||
|
||||
for span_data in spans:
|
||||
# Only process spans with 'step_execution_' prefix
|
||||
if not span_data['name'].startswith('step_execution_'):
|
||||
continue
|
||||
|
||||
attr = span_data.get('attributes', {})
|
||||
exp_id = attr.get('exp_id')
|
||||
task_id = attr.get('task_id', '')
|
||||
|
||||
if not exp_id or not task_id:
|
||||
continue
|
||||
|
||||
if task_id not in task_groups:
|
||||
task_groups[task_id] = {}
|
||||
|
||||
if exp_id not in task_groups[task_id]:
|
||||
task_groups[task_id][exp_id] = {
|
||||
'exp_meta': None,
|
||||
'exp_data': None
|
||||
}
|
||||
|
||||
# Process step_execution span
|
||||
task_name = attr.get('task_name', '')
|
||||
agent_id = attr.get('agent_id', '')
|
||||
step = attr.get('step', 0)
|
||||
execute_time = float(span_data.get('start_time', 0).split('.')[0].replace(' ', '').replace('-', '').replace(':', ''))
|
||||
|
||||
observation = {}
|
||||
action = []
|
||||
messages = []
|
||||
pre_agent = None
|
||||
if 'observation' in attr:
|
||||
try:
|
||||
observation = json.loads(attr['observation'])
|
||||
except:
|
||||
observation = attr['observation']
|
||||
|
||||
if 'actions' in attr:
|
||||
try:
|
||||
action = json.loads(attr['actions'])
|
||||
except:
|
||||
action = attr['actions']
|
||||
|
||||
if 'messages' in attr:
|
||||
try:
|
||||
messages = json.loads(attr['messages'])
|
||||
except:
|
||||
messages = attr['messages']
|
||||
|
||||
pre_agent = attr.get('pre_agent', '')
|
||||
reward = attr.get('reward', 0.0)
|
||||
adv = attr.get('adv_t', 0.0)
|
||||
v = attr.get('v_t', 0.0)
|
||||
|
||||
exp_meta = ExpMeta(task_id, task_name, agent_id, step, execute_time, pre_agent)
|
||||
exp_data = Experience(observation, action, reward, adv, v, messages)
|
||||
|
||||
task_groups[task_id][exp_id]['exp_meta'] = exp_meta
|
||||
task_groups[task_id][exp_id]['exp_data'] = exp_data
|
||||
|
||||
# Process data for each task_id
|
||||
for task_id, exp_groups in task_groups.items():
|
||||
# Merge data and generate final Experience object
|
||||
data_rows = []
|
||||
|
||||
# Read existing data (if any)
|
||||
output_path = self._task_output_paths.get(task_id)
|
||||
if not output_path:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d")
|
||||
replay_dir = os.path.join(output_dir or "./trace_data", timestamp, get_local_ip(), "replays")
|
||||
replay_dataset_path = os.getenv("REPLAY_TRACE_DATASET_PATH", replay_dir)
|
||||
export_dir = os.path.abspath(replay_dataset_path)
|
||||
os.makedirs(export_dir, exist_ok=True)
|
||||
output_path = os.path.join(export_dir, f"task_replay_{task_id}.json")
|
||||
self._task_output_paths[task_id] = output_path
|
||||
|
||||
# Use thread lock to protect read and write operations
|
||||
file_lock = self._get_file_lock(output_path)
|
||||
with file_lock:
|
||||
if os.path.exists(output_path):
|
||||
try:
|
||||
with open(output_path, 'r', encoding='utf-8') as f:
|
||||
existing_data = json.load(f)
|
||||
data_rows.extend([DataRow(
|
||||
ExpMeta(**row['exp_meta']),
|
||||
Experience(**row['exp_data']),
|
||||
row['id']
|
||||
) for row in existing_data])
|
||||
except Exception as e:
|
||||
print(f"Failed to read existing file {output_path}: {str(e)}")
|
||||
|
||||
# Add new data
|
||||
for exp_id, group in exp_groups.items():
|
||||
if group['exp_meta'] and group['exp_data']:
|
||||
row = DataRow(group['exp_meta'], group['exp_data'], exp_id)
|
||||
data_rows.append(row)
|
||||
|
||||
# Sort by execute_time
|
||||
data_rows.sort(key=lambda x: x.exp_meta.execute_time)
|
||||
|
||||
# Export to json
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump([row.to_dict() for row in data_rows], f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"Processing completed, exported {len(data_rows)} experiences to {output_path}")
|
||||
|
||||
if enable_oss_export:
|
||||
# Upload to OSS
|
||||
try:
|
||||
# Get the relative path
|
||||
abs_path = os.path.abspath(output_path)
|
||||
path_parts = abs_path.split(os.sep)
|
||||
if len(path_parts) >= 4:
|
||||
# Get the last 4 parts of the path
|
||||
relative_path = os.sep.join(path_parts[-4:])
|
||||
oss_key = relative_path
|
||||
else:
|
||||
oss_key = f"replay_buffer/{os.path.basename(output_path)}"
|
||||
bucket.put_object_from_file(oss_key, output_path)
|
||||
logger.info(f"Successfully uploaded {output_path} to OSS: {oss_key}")
|
||||
except Exception as e:
|
||||
logger.warn(f"Failed to upload {output_path} to OSS: {str(e)}")
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
from typing import Any, List, TypeVar, Union, Literal, TypedDict, Dict
|
||||
|
||||
DataRow = TypeVar('DataRow')
|
||||
|
||||
|
||||
class BaseCondition(TypedDict):
|
||||
field: str
|
||||
value: Any
|
||||
op: Literal[
|
||||
'eq', 'ne', 'gt', 'gte', 'lt', 'lte',
|
||||
'in', 'not_in', 'like', 'not_like',
|
||||
'is_null', 'is_not_null'
|
||||
]
|
||||
|
||||
|
||||
class LogicalCondition(TypedDict):
|
||||
and_: List['QueryCondition']
|
||||
or_: List['QueryCondition']
|
||||
|
||||
|
||||
QueryCondition = Union[BaseCondition, LogicalCondition]
|
||||
|
||||
|
||||
class QueryBuilder:
|
||||
'''
|
||||
Query builder for replay buffer. result example:
|
||||
{
|
||||
"and": [
|
||||
{"field": "field1", "value": "value1", "op": "eq"},
|
||||
{"or": [{"field": "field2", "value": "value2", "op": "eq"}, {"field": "field3", "value": "value3", "op": "eq"}]}
|
||||
]
|
||||
}
|
||||
'''
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.conditions: List[Dict[str, any]] = []
|
||||
self.logical_ops: List[str] = []
|
||||
|
||||
def eq(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "value": value, "op": "eq"})
|
||||
return self
|
||||
|
||||
def ne(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "value": value, "op": "ne"})
|
||||
return self
|
||||
|
||||
def gt(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "value": value, "op": "gt"})
|
||||
return self
|
||||
|
||||
def gte(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "value": value, "op": "gte"})
|
||||
return self
|
||||
|
||||
def lt(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "value": value, "op": "lt"})
|
||||
return self
|
||||
|
||||
def lte(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "value": value, "op": "lte"})
|
||||
return self
|
||||
|
||||
def in_(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "value": value, "op": "in"})
|
||||
return self
|
||||
|
||||
def not_in(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append(
|
||||
{"field": field, "value": value, "op": "not_in"})
|
||||
return self
|
||||
|
||||
def like(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "value": value, "op": "like"})
|
||||
return self
|
||||
|
||||
def not_like(self, field: str, value: any) -> 'QueryBuilder':
|
||||
self.conditions.append(
|
||||
{"field": field, "value": value, "op": "not_like"})
|
||||
return self
|
||||
|
||||
def is_null(self, field: str) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "op": "is_null"})
|
||||
return self
|
||||
|
||||
def is_not_null(self, field: str) -> 'QueryBuilder':
|
||||
self.conditions.append({"field": field, "op": "is_not_null"})
|
||||
return self
|
||||
|
||||
def and_(self) -> 'QueryBuilder':
|
||||
self.logical_ops.append("and_")
|
||||
return self
|
||||
|
||||
def or_(self) -> 'QueryBuilder':
|
||||
self.logical_ops.append("or_")
|
||||
return self
|
||||
|
||||
def nested(self, builder: 'QueryBuilder') -> 'QueryBuilder':
|
||||
self.conditions.append({"nested": builder.build()})
|
||||
return self
|
||||
|
||||
def build(self) -> QueryCondition:
|
||||
conditions = self.conditions # all conditions(including nested)
|
||||
operators = self.logical_ops
|
||||
|
||||
# Validate condition and operator counts (n conditions need n-1 operators)
|
||||
if len(operators) != len(conditions) - 1:
|
||||
raise ValueError("Mismatch between condition and operator counts")
|
||||
|
||||
# Use stack to handle operator precedence (simplified version supporting and/or)
|
||||
stack: List[Union[Dict[str, any], str]] = []
|
||||
|
||||
for i, item in enumerate(conditions):
|
||||
if i == 0:
|
||||
# First element goes directly to stack (condition or nested)
|
||||
stack.append(item)
|
||||
continue
|
||||
|
||||
# Pop stack top as left operand
|
||||
left = stack.pop()
|
||||
op = operators[i-1] # Current operator (and/or)
|
||||
right = item # Right operand (current condition)
|
||||
|
||||
# Build logical expression: {op: [left, right]}
|
||||
expr = {op: [left, right]}
|
||||
# Push result back to stack for further operations
|
||||
stack.append(expr)
|
||||
|
||||
# Process nested conditions (recursive unfolding)
|
||||
def process_nested(cond: any) -> any:
|
||||
if isinstance(cond, dict):
|
||||
if "nested" in cond:
|
||||
# Recursively process sub-conditions
|
||||
return process_nested(cond["nested"])
|
||||
# Recursively process child elements
|
||||
return {k: process_nested(v) for k, v in cond.items()}
|
||||
elif isinstance(cond, list):
|
||||
return [process_nested(item) for item in cond]
|
||||
return cond
|
||||
|
||||
# Final result: only one element left in stack, return after processing nested
|
||||
result = stack[0] if stack else None
|
||||
return process_nested(result) if result else None
|
||||
|
||||
|
||||
class QueryFilter:
|
||||
'''
|
||||
Query filter for replay buffer.
|
||||
'''
|
||||
|
||||
def __init__(self, query_condition: QueryCondition) -> None:
|
||||
self.query_condition = query_condition
|
||||
|
||||
def _get_field_value(self, row: DataRow, field: str) -> Any:
|
||||
'''
|
||||
Get field value from row.
|
||||
'''
|
||||
obj = row
|
||||
for part in field.split('.'):
|
||||
obj = getattr(obj, part, None)
|
||||
if obj is None:
|
||||
break
|
||||
return obj
|
||||
|
||||
def _do_check(self, row: DataRow, condition: QueryCondition) -> bool:
|
||||
"""
|
||||
check if row match condition
|
||||
"""
|
||||
if condition is None:
|
||||
return True
|
||||
if "field" in condition and "op" in condition:
|
||||
field_val = self._get_field_value(row, condition["field"])
|
||||
op = condition["op"]
|
||||
target_val = condition["value"]
|
||||
|
||||
if op == "eq":
|
||||
return field_val == target_val
|
||||
if op == "ne":
|
||||
return field_val != target_val
|
||||
if op == "gt":
|
||||
return field_val > target_val
|
||||
if op == "gte":
|
||||
return field_val >= target_val
|
||||
if op == "lt":
|
||||
return field_val < target_val
|
||||
if op == "lte":
|
||||
return field_val <= target_val
|
||||
if op == "in":
|
||||
return field_val in target_val
|
||||
if op == "not_in":
|
||||
return field_val not in target_val
|
||||
if op == "like":
|
||||
return target_val in field_val
|
||||
if op == "not_like":
|
||||
return target_val not in field_val
|
||||
if op == "is_null":
|
||||
return field_val is None
|
||||
if op == "is_not_null":
|
||||
return field_val is not None
|
||||
|
||||
return False
|
||||
|
||||
elif "and_" in condition or "or_" in condition:
|
||||
if "and_" in condition:
|
||||
return all(self._do_check(row, c) for c in condition["and_"])
|
||||
if "or_" in condition:
|
||||
return any(self._do_check(row, c) for c in condition["or_"])
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def check_condition(self, row: DataRow) -> bool:
|
||||
"""
|
||||
check if row match condition
|
||||
"""
|
||||
return self._do_check(row, self.query_condition)
|
||||
|
||||
def filter(self, rows: List[DataRow]) -> List[DataRow]:
|
||||
"""filter rows by condition
|
||||
Args:
|
||||
rows (List[DataRow]): List of rows to filter.
|
||||
query_condition (QueryCondition): Query condition.
|
||||
Returns:
|
||||
List[DataRow]: List of rows that match the condition.
|
||||
"""
|
||||
condition = self.query_condition
|
||||
if not condition:
|
||||
return rows
|
||||
return [row for row in rows if self.check_condition(row)]
|
||||
@@ -0,0 +1,2 @@
|
||||
# coding: utf-8
|
||||
# Copyright (c) 2025 inclusionAI.
|
||||
@@ -0,0 +1,155 @@
|
||||
import multiprocessing
|
||||
import traceback
|
||||
import pickle
|
||||
from typing import Dict, List
|
||||
from aworld.replay_buffer.base import Storage, DataRow
|
||||
from aworld.replay_buffer.query_filter import QueryCondition, QueryFilter
|
||||
from aworld.logs.util import logger
|
||||
|
||||
|
||||
class MultiProcMemoryStorage(Storage):
|
||||
|
||||
"""
|
||||
Memory storage for multi-process.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
data_dict: Dict[str, str],
|
||||
fifo_queue: List[str],
|
||||
lock: multiprocessing.Lock,
|
||||
max_capacity: int = 10000):
|
||||
self._data: Dict[str, str] = data_dict
|
||||
self._fifo_queue = fifo_queue
|
||||
self._max_capacity = max_capacity
|
||||
self._lock = lock
|
||||
|
||||
def _save_to_shared_memory(self, data, task_id):
|
||||
serialized_data = pickle.dumps(data)
|
||||
try:
|
||||
if task_id not in self._data or not self._data[task_id]:
|
||||
shm = multiprocessing.shared_memory.SharedMemory(
|
||||
create=True, size=len(serialized_data))
|
||||
shm.buf[:len(serialized_data)] = serialized_data
|
||||
self._data[task_id] = shm.name
|
||||
shm.close()
|
||||
return
|
||||
shm = multiprocessing.shared_memory.SharedMemory(
|
||||
name=self._data[task_id], create=False)
|
||||
if len(serialized_data) > shm.size:
|
||||
shm.close()
|
||||
shm.unlink()
|
||||
shm = multiprocessing.shared_memory.SharedMemory(
|
||||
create=True, size=len(serialized_data))
|
||||
shm.buf[:len(serialized_data)] = serialized_data
|
||||
self._data[task_id] = shm.name
|
||||
else:
|
||||
shm.buf[:len(serialized_data)] = serialized_data
|
||||
except FileNotFoundError:
|
||||
shm = multiprocessing.shared_memory.SharedMemory(
|
||||
create=True, size=len(serialized_data))
|
||||
shm.buf[:len(serialized_data)] = serialized_data
|
||||
self._data[task_id] = shm.name
|
||||
shm.close()
|
||||
|
||||
def _load_from_shared_memory(self, task_id):
|
||||
try:
|
||||
if task_id not in self._data or not self._data[task_id]:
|
||||
return []
|
||||
try:
|
||||
multiprocessing.shared_memory.SharedMemory(
|
||||
name=self._data[task_id], create=False)
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
shm = multiprocessing.shared_memory.SharedMemory(
|
||||
name=self._data[task_id])
|
||||
data = pickle.loads(shm.buf.tobytes())
|
||||
shm.close()
|
||||
return data
|
||||
except Exception as e:
|
||||
stack_trace = traceback.format_exc()
|
||||
logger.error(
|
||||
f"_load_from_shared_memory error: {e}\nStack trace:\n{stack_trace}")
|
||||
return []
|
||||
|
||||
def _delete_from_shared_memory(self, task_id):
|
||||
try:
|
||||
if task_id not in self._data or not self._data[task_id]:
|
||||
return
|
||||
shm = multiprocessing.shared_memory.SharedMemory(
|
||||
name=self._data[task_id])
|
||||
shm.close()
|
||||
shm.unlink()
|
||||
del self._data[task_id]
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def add(self, data: DataRow):
|
||||
if not data:
|
||||
raise ValueError("Data is required")
|
||||
if not data.exp_meta:
|
||||
raise ValueError("exp_meta is required")
|
||||
|
||||
with self._lock:
|
||||
current_size = sum(len(self._load_from_shared_memory(task_id))
|
||||
for task_id in self._data.keys())
|
||||
while current_size >= self._max_capacity and self._fifo_queue:
|
||||
oldest_task_id = self._fifo_queue.pop(0)
|
||||
if oldest_task_id in self._data.keys():
|
||||
current_size -= len(self._load_from_shared_memory(oldest_task_id))
|
||||
self._delete_from_shared_memory(oldest_task_id)
|
||||
|
||||
task_id = data.exp_meta.task_id
|
||||
existing_data = self._load_from_shared_memory(task_id)
|
||||
existing_data.append(data)
|
||||
self._save_to_shared_memory(existing_data, task_id)
|
||||
self._fifo_queue.append(task_id)
|
||||
|
||||
def add_batch(self, data_batch: List[DataRow]):
|
||||
with self._lock:
|
||||
for data in data_batch:
|
||||
self.add(data)
|
||||
|
||||
def size(self, query_condition: QueryCondition = None) -> int:
|
||||
with self._lock:
|
||||
return len(self._get_all_without_lock(query_condition))
|
||||
|
||||
def get_paginated(self, page: int, page_size: int, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
with self._lock:
|
||||
if page < 1:
|
||||
raise ValueError("Page must be greater than 0")
|
||||
if page_size < 1:
|
||||
raise ValueError("Page size must be greater than 0")
|
||||
all_data = self._get_all_without_lock(query_condition)
|
||||
start_index = (page - 1) * page_size
|
||||
end_index = start_index + page_size
|
||||
return all_data[start_index:end_index]
|
||||
|
||||
def _get_all_without_lock(self, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
all_data = []
|
||||
query_filter = None
|
||||
if query_condition:
|
||||
query_filter = QueryFilter(query_condition)
|
||||
for task_id in self._data.keys():
|
||||
local_data = self._load_from_shared_memory(task_id)
|
||||
if query_filter:
|
||||
all_data.extend(query_filter.filter(local_data))
|
||||
else:
|
||||
all_data.extend(local_data)
|
||||
return all_data
|
||||
|
||||
def get_all(self, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
with self._lock:
|
||||
return self._get_all_without_lock(query_condition)
|
||||
|
||||
def get_by_task_id(self, task_id: str) -> List[DataRow]:
|
||||
with self._lock:
|
||||
if task_id in self._data.keys():
|
||||
return self._load_from_shared_memory(task_id)
|
||||
|
||||
def get_bacth_by_task_ids(self, task_ids: List[str]) -> Dict[str, List[DataRow]]:
|
||||
with self._lock:
|
||||
result = {}
|
||||
for task_id in task_ids:
|
||||
if task_id in self._data.keys():
|
||||
result[task_id] = self._load_from_shared_memory(task_id)
|
||||
return result
|
||||
@@ -0,0 +1,223 @@
|
||||
import json
|
||||
from pydantic import parse_obj_as
|
||||
from typing import Any, List, Dict
|
||||
from aworld.replay_buffer.base import Storage, DataRow, ExpMeta, Experience
|
||||
from aworld.replay_buffer.query_filter import QueryCondition, QueryBuilder
|
||||
from aworld.core.common import Observation, ActionModel
|
||||
from aworld.logs.util import logger
|
||||
from aworld.utils.import_package import import_package
|
||||
import_package("odps") # noqa
|
||||
from odps import ODPS # noqa
|
||||
from odps.models.record import Record # noqa
|
||||
|
||||
|
||||
class OdpsSQLBuilder:
|
||||
''' Example:
|
||||
query_condition = QueryBuilder().eq("field1", "value1").and_().eq("field2", "value2")
|
||||
sql_builder = OdpsSQLBuilder(query_condition)
|
||||
sql = sql_builder.build_sql()
|
||||
print(sql) # 输出: "field1 = 'value1' AND field2 = 'value2'"
|
||||
'''
|
||||
|
||||
def __init__(self, query_condition: QueryCondition):
|
||||
self.query_condition = query_condition
|
||||
|
||||
def _build_condition(self, condition: QueryCondition) -> str:
|
||||
if condition is None:
|
||||
return ""
|
||||
|
||||
if "field" in condition and "op" in condition:
|
||||
field = condition["field"].split('.')[-1]
|
||||
op = condition["op"]
|
||||
value = condition.get("value")
|
||||
|
||||
if op == "eq":
|
||||
return f"{field} = {self._format_value(value)}"
|
||||
elif op == "ne":
|
||||
return f"{field} != {self._format_value(value)}"
|
||||
elif op == "gt":
|
||||
return f"{field} > {self._format_value(value)}"
|
||||
elif op == "gte":
|
||||
return f"{field} >= {self._format_value(value)}"
|
||||
elif op == "lt":
|
||||
return f"{field} < {self._format_value(value)}"
|
||||
elif op == "lte":
|
||||
return f"{field} <= {self._format_value(value)}"
|
||||
elif op == "in":
|
||||
return f"{field} IN ({self._format_value(value)})"
|
||||
elif op == "not_in":
|
||||
return f"{field} NOT IN ({self._format_value(value)})"
|
||||
elif op == "like":
|
||||
return f"{field} LIKE '{value}'"
|
||||
elif op == "not_like":
|
||||
return f"{field} NOT LIKE '{value}'"
|
||||
elif op == "is_null":
|
||||
return f"{field} IS NULL"
|
||||
elif op == "is_not_null":
|
||||
return f"{field} IS NOT NULL"
|
||||
|
||||
elif "and_" in condition:
|
||||
return f"({' AND '.join(self._build_condition(c) for c in condition['and_'])})"
|
||||
elif "or_" in condition:
|
||||
return f"({' OR '.join(self._build_condition(c) for c in condition['or_'])})"
|
||||
|
||||
return ""
|
||||
|
||||
def _format_value(self, value: Any) -> str:
|
||||
if isinstance(value, str):
|
||||
return f"'{value}'"
|
||||
elif isinstance(value, (list, tuple)):
|
||||
return ", ".join(self._format_value(v) for v in value)
|
||||
return str(value)
|
||||
|
||||
def build_sql(self) -> str:
|
||||
if not self.query_condition:
|
||||
return ""
|
||||
return self._build_condition(self.query_condition)
|
||||
|
||||
|
||||
class OdpsStorage(Storage):
|
||||
'''
|
||||
Aliyun ODPS storage.
|
||||
Table schema:
|
||||
id: int
|
||||
task_id: string
|
||||
task_name: string
|
||||
agent_id: string
|
||||
step: int
|
||||
execute_time: string
|
||||
state: string
|
||||
actions: string
|
||||
reward_t: string
|
||||
adv_t: string
|
||||
v_t: string
|
||||
'''
|
||||
|
||||
def __init__(self, table_name: str, project: str, endpoint: str, access_id: str, access_key: str, **kwargs):
|
||||
self.table_name = table_name
|
||||
self.project = project
|
||||
self.endpoint = endpoint
|
||||
self.access_id = access_id
|
||||
self.access_key = access_key
|
||||
self.kwargs = kwargs
|
||||
self._init_odps()
|
||||
|
||||
def _init_odps(self):
|
||||
self.odps = ODPS(self.access_id, self.access_key,
|
||||
self.project, self.endpoint)
|
||||
|
||||
def _get_table(self):
|
||||
return self.odps.get_table(self.table_name)
|
||||
|
||||
def _convert_row_to_record(self, row: DataRow) -> Record:
|
||||
table = self._get_table()
|
||||
record = table.new_record()
|
||||
record["id"] = row.id
|
||||
record["task_id"] = row.exp_meta.task_id
|
||||
record["task_name"] = row.exp_meta.task_name
|
||||
record["agent_id"] = row.exp_meta.agent_id
|
||||
record["step"] = row.exp_meta.step
|
||||
record["execute_time"] = row.exp_meta.execute_time
|
||||
if row.exp_data.state:
|
||||
record["state"] = row.exp_data.state.model_dump_json()
|
||||
if row.exp_data.actions:
|
||||
record["actions"] = "[" + ", ".join(action.model_dump_json()
|
||||
for action in row.exp_data.actions) + "]"
|
||||
if row.exp_data.reward_t:
|
||||
record["reward_t"] = row.exp_data.reward_t
|
||||
if row.exp_data.adv_t:
|
||||
record["adv_t"] = row.exp_data.adv_t
|
||||
if row.exp_data.v_t:
|
||||
record["v_t"] = row.exp_data.v_t
|
||||
return record
|
||||
|
||||
def _convert_record_to_row(self, record: Record) -> DataRow:
|
||||
return DataRow(
|
||||
id=record.id,
|
||||
exp_meta=ExpMeta(
|
||||
task_id=record['task_id'],
|
||||
task_name=record['task_name'],
|
||||
agent_id=record['agent_id'],
|
||||
step=record['step'],
|
||||
execute_time=record['execute_time'],
|
||||
pre_agent=record['pre_agent'] if 'pre_agent' in record else None
|
||||
),
|
||||
exp_data=Experience(
|
||||
state=parse_obj_as(Observation, json.loads(record['state'])),
|
||||
actions=[parse_obj_as(ActionModel, item)
|
||||
for item in json.loads(record['actions'])],
|
||||
reward_t=record['reward_t'] if 'reward_t' in record else None,
|
||||
adv_t=record['adv_t'] if 'adv_t' in record else None,
|
||||
v_t=record['v_t'] if 'v_t' in record else None,
|
||||
)
|
||||
)
|
||||
|
||||
def _build_paginated_sql(self, page: int = None, page_size: int = None):
|
||||
if page and page_size:
|
||||
offset = (page - 1) * page_size
|
||||
limit = page_size
|
||||
return f" LIMIT {offset}, {limit}"
|
||||
return ""
|
||||
|
||||
def _build_sql(self, query_condition: QueryCondition, page: int = None, page_size: int = None):
|
||||
if not query_condition:
|
||||
return f"SELECT * FROM {self.table_name}" + self._build_paginated_sql(page, page_size)
|
||||
where_builder = OdpsSQLBuilder(query_condition)
|
||||
sql = f"SELECT * FROM {self.table_name} WHERE {where_builder.build_sql()}" + self._build_paginated_sql(page,
|
||||
page_size)
|
||||
return sql
|
||||
|
||||
def _build_count_sql(self, query_condition: QueryCondition):
|
||||
if not query_condition:
|
||||
return f"SELECT count(1) as count FROM {self.table_name}"
|
||||
where_builder = OdpsSQLBuilder(query_condition)
|
||||
sql = f"SELECT count(1) as count FROM {self.table_name} WHERE {where_builder.build_sql()}"
|
||||
return sql
|
||||
|
||||
def add(self, row: DataRow):
|
||||
record = self._convert_row_to_record(row)
|
||||
self.odps.write_table(self.table_name, [record])
|
||||
|
||||
def add_batch(self, rows: list[DataRow]):
|
||||
records = [self._convert_row_to_record(row) for row in rows]
|
||||
self.odps.write_table(self.table_name, records)
|
||||
|
||||
def size(self, query_condition: QueryCondition = None) -> int:
|
||||
sql = self._build_count_sql(query_condition)
|
||||
with self.odps.execute_sql(sql).open_reader() as reader:
|
||||
return reader[0]["count"]
|
||||
|
||||
def get_all(self, query_condition: QueryCondition = None) -> list[DataRow]:
|
||||
sql = self._build_sql(query_condition)
|
||||
logger.info(f"get_all sql: {sql}")
|
||||
with self.odps.execute_sql(sql).open_reader(tunnel=True) as reader:
|
||||
rows = []
|
||||
for record in reader:
|
||||
rows.append(self._convert_record_to_row(record))
|
||||
return rows
|
||||
|
||||
def get_paginated(self, page: int, page_size: int, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
sql = self._build_sql(query_condition, page, page_size)
|
||||
logger.info(f"get_paginated sql: {sql}")
|
||||
with self.odps.execute_sql(sql).open_reader(tunnel=True) as reader:
|
||||
rows = []
|
||||
for record in reader:
|
||||
rows.append(self._convert_record_to_row(record))
|
||||
return rows
|
||||
|
||||
def get_by_task_id(self, task_id: str) -> List[DataRow]:
|
||||
query_condition = QueryBuilder().eq("task_id", task_id).build()
|
||||
return self.get_all(query_condition)
|
||||
|
||||
def get_bacth_by_task_ids(self, task_ids: List[str]) -> Dict[str, List[DataRow]]:
|
||||
query_condition = QueryBuilder().in_("task_id", task_ids).build()
|
||||
sql = self._build_sql(query_condition)
|
||||
logger.info(f"get_bacth_by_task_ids sql: {sql}")
|
||||
result = {}
|
||||
with self.odps.execute_sql(sql).open_reader(tunnel=True) as reader:
|
||||
for record in reader:
|
||||
row = self._convert_record_to_row(record)
|
||||
if row.exp_meta.task_id not in result:
|
||||
result[row.exp_meta.task_id] = []
|
||||
result[row.exp_meta.task_id].append(row)
|
||||
return result
|
||||
@@ -0,0 +1,262 @@
|
||||
import json
|
||||
from typing import Dict, List
|
||||
from aworld.replay_buffer.base import Storage, DataRow, ExpMeta, Experience
|
||||
from aworld.logs.util import logger
|
||||
from aworld.utils.import_package import import_package
|
||||
from aworld.replay_buffer.query_filter import QueryCondition, QueryBuilder
|
||||
from aworld.core.common import Observation, ActionModel
|
||||
import_package("redis") # noqa
|
||||
from redis import Redis # noqa
|
||||
from redis.commands.json.path import Path # noqa
|
||||
import redis.commands.search.aggregation as aggregations # noqa
|
||||
import redis.commands.search.reducers as reducers # noqa
|
||||
from redis.commands.search.field import TextField, NumericField, TagField # noqa
|
||||
from redis.commands.search.index_definition import IndexDefinition, IndexType # noqa
|
||||
from redis.commands.search.query import Query # noqa
|
||||
import redis.exceptions # noqa
|
||||
|
||||
|
||||
class RedisSearchQueryBuilder:
|
||||
"""
|
||||
Build redis search query from query condition
|
||||
"""
|
||||
|
||||
def __init__(self, query_condition: QueryCondition):
|
||||
self.query_condition = query_condition
|
||||
|
||||
def _build_condition(self, condition: QueryCondition) -> str:
|
||||
if condition is None:
|
||||
return ""
|
||||
|
||||
if "field" in condition and "op" in condition:
|
||||
field = condition["field"].split('.')[-1]
|
||||
op = condition["op"]
|
||||
value = condition.get("value")
|
||||
|
||||
if op == "eq":
|
||||
return f"@{field}:{{{value}}}"
|
||||
elif op == "ne":
|
||||
return f"-@{field}:{{{value}}}"
|
||||
elif op == "gt":
|
||||
return f"@{field}:[{value} +inf]"
|
||||
elif op == "gte":
|
||||
return f"@{field}:[{value} +inf]"
|
||||
elif op == "lt":
|
||||
return f"@{field}:[-inf {value}]"
|
||||
elif op == "lte":
|
||||
return f"@{field}:[-inf {value}]"
|
||||
elif op == "in":
|
||||
return f"@{field}:{{{'|'.join(str(v) for v in value)}}}"
|
||||
elif op == "not_in":
|
||||
return f"-@{field}:{{{'|'.join(str(v) for v in value)}}}"
|
||||
elif op == "like":
|
||||
return f"@{field}:*{value}*"
|
||||
elif op == "not_like":
|
||||
return f"-@{field}:*{value}*"
|
||||
elif op == "is_null":
|
||||
return f"-@{field}:*"
|
||||
elif op == "is_not_null":
|
||||
return f"@{field}:*"
|
||||
|
||||
elif "and_" in condition:
|
||||
conditions = [self._build_condition(c) for c in condition["and_"]]
|
||||
return " ".join(conditions)
|
||||
elif "or_" in condition:
|
||||
conditions = [self._build_condition(c) for c in condition["or_"]]
|
||||
return f"({'|'.join(conditions)})"
|
||||
|
||||
return ""
|
||||
|
||||
def build(self) -> Query:
|
||||
query_str = self._build_condition(self.query_condition)
|
||||
logger.info(f"redis search query: {query_str}")
|
||||
return Query(query_str)
|
||||
|
||||
|
||||
class RedisStorage(Storage):
|
||||
def __init__(self,
|
||||
host: str = 'localhost',
|
||||
port: int = 6379,
|
||||
db: int = 0,
|
||||
password: str = None,
|
||||
key_prefix: str = 'AWORLD:RB:',
|
||||
index_name: str = 'idx:AWORLD:RB',
|
||||
recreate_idx_if_exists=False):
|
||||
self._redis = Redis(host=host, port=port, db=db, password=password)
|
||||
self._key_prefix = key_prefix
|
||||
self._index_name = index_name
|
||||
self._recreate_idx_if_exists = recreate_idx_if_exists
|
||||
self._create_index()
|
||||
|
||||
def _create_index(self):
|
||||
try:
|
||||
existing_indices = self._redis.execute_command('FT._LIST')
|
||||
if self._index_name.encode('utf-8') in existing_indices:
|
||||
logger.info(f"Index {self._index_name} already exists")
|
||||
if self._recreate_idx_if_exists:
|
||||
self._redis.ft(self._index_name).dropindex()
|
||||
logger.info(f"Index {self._index_name} dropped")
|
||||
else:
|
||||
return
|
||||
self._redis.ft(self._index_name).create_index(
|
||||
(
|
||||
TagField("id"),
|
||||
TagField("task_id"),
|
||||
TextField("task_name"),
|
||||
TagField("agent_id"),
|
||||
NumericField("step"),
|
||||
NumericField("execute_time"),
|
||||
TagField("pre_agent")
|
||||
),
|
||||
definition=IndexDefinition(
|
||||
prefix=[self._key_prefix], index_type=IndexType.HASH)
|
||||
)
|
||||
except redis.exceptions.ResponseError as e:
|
||||
logger.error(f"Create index {self._index_name} failed. {e}")
|
||||
|
||||
def _get_object_key(self, key: str) -> str:
|
||||
return f"{self._key_prefix}{key}"
|
||||
|
||||
def _serialize_to_str(self, value) -> str:
|
||||
if str is None:
|
||||
return ""
|
||||
if isinstance(value, (int, float)):
|
||||
return str(value)
|
||||
return str(value) if value is not None else ""
|
||||
|
||||
def _serialize(self, data: DataRow) -> Dict[str, str]:
|
||||
dict_data = {
|
||||
'id': data.id,
|
||||
'task_id': data.exp_meta.task_id,
|
||||
'task_name': data.exp_meta.task_name,
|
||||
'agent_id': data.exp_meta.agent_id,
|
||||
'step': data.exp_meta.step,
|
||||
'execute_time': data.exp_meta.execute_time,
|
||||
'pre_agent': data.exp_meta.pre_agent,
|
||||
'state': data.exp_data.state.model_dump_json(),
|
||||
'actions': "[" + ", ".join(action.model_dump_json()
|
||||
for action in data.exp_data.actions) + "]",
|
||||
'reward_t': data.exp_data.reward_t,
|
||||
'adv_t': data.exp_data.adv_t,
|
||||
'v_t': data.exp_data.v_t
|
||||
}
|
||||
return {k: self._serialize_to_str(v) for k, v in dict_data.items()}
|
||||
|
||||
def _deserialize(self, data: Dict) -> DataRow:
|
||||
if not data:
|
||||
return None
|
||||
return DataRow(
|
||||
id=data.get('id'),
|
||||
exp_meta=ExpMeta(
|
||||
task_id=data.get('task_id'),
|
||||
task_name=data.get('task_name'),
|
||||
agent_id=data.get('agent_id'),
|
||||
step=int(data.get('step', 0)),
|
||||
execute_time=float(data.get('execute_time', 0)),
|
||||
pre_agent=data.get('pre_agent')
|
||||
),
|
||||
exp_data=Experience(
|
||||
state=Observation.model_validate_json(data.get('state', '{}')),
|
||||
actions=[ActionModel.model_validate_json(json.dumps(action))
|
||||
for action in json.loads(data.get('actions', '[]'))],
|
||||
reward_t=float(data.get('reward_t', 0)) if data.get(
|
||||
'reward_t') is not '' else None,
|
||||
adv_t=float(data.get('adv_t', 0)) if data.get(
|
||||
'adv_t') is not '' else None,
|
||||
v_t=float(data.get('v_t', 0)) if data.get(
|
||||
'v_t') is not '' else None
|
||||
)
|
||||
)
|
||||
|
||||
def add(self, data: DataRow):
|
||||
key = self._get_object_key(data.id)
|
||||
self._redis.hset(key, mapping=self._serialize(data))
|
||||
|
||||
def add_batch(self, data_batch: List[DataRow]):
|
||||
pipeline = self._redis.pipeline()
|
||||
for data in data_batch:
|
||||
if not data or not data.exp_meta:
|
||||
continue
|
||||
key = self._get_object_key(data.id)
|
||||
pipeline.hset(key, mapping=self._serialize(data))
|
||||
pipeline.execute()
|
||||
|
||||
def search(self, key: str, value: str) -> DataRow:
|
||||
result = self._redis.ft(self._index_name).search(
|
||||
Query(f"@{key}:{{{value}}}"))
|
||||
logger.info(f"Search result: {result}")
|
||||
|
||||
def size(self, query_condition: QueryCondition = None) -> int:
|
||||
'''
|
||||
Get the size of the storage.
|
||||
Returns:
|
||||
int: Size of the storage.
|
||||
'''
|
||||
if not query_condition:
|
||||
return self._redis.ft(self._index_name).info()['num_docs']
|
||||
query_builder = RedisSearchQueryBuilder(query_condition)
|
||||
query = query_builder.build()
|
||||
return self._redis.ft(self._index_name).search(query).total
|
||||
|
||||
def get_paginated(self, page: int, page_size: int, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
'''
|
||||
Get paginated data from the storage.
|
||||
Args:
|
||||
page (int): Page number.
|
||||
page_size (int): Number of data per page.
|
||||
Returns:
|
||||
List[DataRow]: List of data.
|
||||
'''
|
||||
if not query_condition:
|
||||
result = self._redis.ft(self._index_name).search(
|
||||
Query("*").paging(page, page_size))
|
||||
else:
|
||||
query_builder = RedisSearchQueryBuilder(query_condition)
|
||||
query = query_builder.build().paging(page, page_size)
|
||||
result = self._redis.ft(self._index_name).search(query)
|
||||
return [self._deserialize(doc.__dict__) for doc in result.docs]
|
||||
|
||||
def get_all(self, query_condition: QueryCondition = None) -> List[DataRow]:
|
||||
'''
|
||||
Get all data from the storage.
|
||||
Returns:
|
||||
List[DataRow]: List of data.
|
||||
'''
|
||||
if not query_condition:
|
||||
result = self._redis.ft(self._index_name).search(Query("*"))
|
||||
else:
|
||||
query_builder = RedisSearchQueryBuilder(query_condition)
|
||||
query = query_builder.build()
|
||||
result = self._redis.ft(self._index_name).search(query)
|
||||
return [self._deserialize(doc.__dict__) for doc in result.docs]
|
||||
|
||||
def get_by_task_id(self, task_id: str) -> List[DataRow]:
|
||||
'''
|
||||
Get data by task_id from the storage.
|
||||
Args:
|
||||
task_id (str): Task id.
|
||||
Returns:
|
||||
List[DataRow]: List of data.
|
||||
'''
|
||||
query_condition = QueryBuilder().eq("task_id", task_id).build()
|
||||
return self.get_all(query_condition)
|
||||
|
||||
def get_bacth_by_task_ids(self, task_ids: List[str]) -> Dict[str, List[DataRow]]:
|
||||
'''
|
||||
Get data by task_ids from the storage.
|
||||
Args:
|
||||
task_ids (List[str]): List of task ids.
|
||||
Returns:
|
||||
Dict[str, List[DataRow]]: Dict of task id and list of data.
|
||||
'''
|
||||
query_condition = QueryBuilder().in_("task_id", task_ids).build()
|
||||
result = self.get_all(query_condition)
|
||||
return {task_id: [data for data in result if data.exp_meta.task_id == task_id] for task_id in task_ids}
|
||||
|
||||
def clear(self):
|
||||
'''
|
||||
Clear the storage.
|
||||
'''
|
||||
keys = self._redis.keys(f"{self._key_prefix}*")
|
||||
if keys:
|
||||
self._redis.delete(*keys)
|
||||
Reference in New Issue
Block a user