Source code for trinity.explorer.proxy.recorder
from typing import Dict, List, Set
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from trinity.buffer.schema.sql_schema import init_async_engine
from trinity.buffer.utils import async_run_with_retry_session
from trinity.common.experience import Experience
from trinity.utils.log import get_logger
[docs]
class HistoryRecorder:
"""Record chat history into the database using async SQL."""
[docs]
def __init__(self, db_url: str, table_name: str):
self.logger = get_logger()
self._db_url = db_url
self._table_name = table_name
self._initialized = False
[docs]
async def prepare(self) -> None:
if self._initialized:
return
engine, self.meta_cls, self.blob_cls = await init_async_engine(
db_url=self._db_url,
table_name=self._table_name,
schema_type="experience",
)
self.session = async_sessionmaker(engine, expire_on_commit=False)
self._initialized = True
self.logger.info(f"Init async SQL storage at {self._db_url}")
[docs]
async def record_history(self, experiences: List[Experience]) -> None:
"""Save experiences to the database."""
await self.prepare()
async def operation(session: AsyncSession):
for exp in experiences:
meta_row = self.meta_cls.from_experience(exp)
session.add(meta_row)
await session.flush()
blob_row = self.blob_cls(id=meta_row.id, experience_bytes=exp.serialize())
session.add(blob_row)
await async_run_with_retry_session(self.session, operation)
[docs]
async def update_reward(
self, reward: float, msg_ids: list, run_id: int, task_id: str
) -> List[Experience]:
"""Update reward for given response IDs and return the updated experiences.
Only experiences that have not been consumed (consumed == 0) will be returned.
"""
await self.prepare()
meta_cls = self.meta_cls
blob_cls = self.blob_cls
async def operation(session: AsyncSession):
stmt = (
select(meta_cls)
.where(meta_cls.msg_id.in_(msg_ids), meta_cls.consumed == 0)
.with_for_update()
)
result = await session.execute(stmt)
records = result.scalars().all()
if not records:
return []
ids = [record.id for record in records]
update_stmt = (
update(meta_cls)
.where(meta_cls.id.in_(ids))
.values(
reward=reward,
run_id=run_id,
task_id=task_id,
consumed=meta_cls.consumed + 1,
)
)
await session.execute(update_stmt)
blob_stmt = select(blob_cls).where(blob_cls.id.in_(ids))
blob_result = await session.execute(blob_stmt)
blobs = blob_result.scalars().all()
blob_map = {b.id: b.experience_bytes for b in blobs}
# Re-fetch meta rows to get updated values
refresh_stmt = select(meta_cls).where(meta_cls.id.in_(ids))
refresh_result = await session.execute(refresh_stmt)
updated_records = refresh_result.scalars().all()
updated_experiences = []
for record in updated_records:
blob_bytes = blob_map.get(record.id)
if blob_bytes is not None:
updated_experiences.append(record.to_experience(blob_bytes))
return updated_experiences
return await async_run_with_retry_session(self.session, operation)
[docs]
class MemoryHistoryRecorder:
"""
In-memory version of HistoryRecorder for high-performance reward update and history recording.
All data is stored in memory, and can be flushed to persistent storage as needed.
"""
[docs]
def __init__(self):
self.logger = get_logger()
# msg_id -> Experience
self._exp_map: Dict[str, Experience] = {}
# Set of msg_id that are not consumed
self._unconsumed: Set[str] = set()
[docs]
async def record_history(self, experiences: List[Experience]) -> None:
"""Save experiences in memory."""
for exp in experiences:
self._exp_map[exp.eid.suffix] = exp
if getattr(exp, "consumed", 0) == 0:
self._unconsumed.add(exp.eid.suffix)
[docs]
async def update_reward(
self, reward: float, msg_ids: list, run_id: int, task_id: str
) -> List[Experience]:
"""Update reward for given response IDs and return the updated experiences."""
updated = []
for msg_id in msg_ids:
if msg_id in self._unconsumed and msg_id in self._exp_map:
exp = self._exp_map.pop(msg_id)
exp.reward = reward
exp.eid.run = run_id
exp.eid.task = task_id
self._unconsumed.remove(msg_id)
updated.append(exp)
return updated