Files
2026-07-26 18:50:51 +08:00

142 lines
5.6 KiB
Python

from __future__ import annotations
import uuid
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import JSON, DateTime, Integer, String, Text, UniqueConstraint, delete, select
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class RunEvent(Base):
__tablename__ = "agent_run_events"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
run_id: Mapped[str] = mapped_column(String(64), index=True)
user_id: Mapped[str] = mapped_column(String(128), index=True)
chat_id: Mapped[str] = mapped_column(String(256), index=True)
sequence: Mapped[int] = mapped_column(Integer)
event_type: Mapped[str] = mapped_column(String(80), index=True)
payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
class Memory(Base):
__tablename__ = "agent_memories"
id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id: Mapped[str] = mapped_column(String(128), index=True)
content: Mapped[str] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
class Plan(Base):
__tablename__ = "agent_plans"
__table_args__ = (UniqueConstraint("user_id", "chat_id"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[str] = mapped_column(String(128), index=True)
chat_id: Mapped[str] = mapped_column(String(256), index=True)
items: Mapped[list[dict[str, Any]]] = mapped_column(JSON, default=list)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(UTC))
class RuntimeStore:
def __init__(self, url: str) -> None:
self.engine: AsyncEngine = create_async_engine(url, pool_pre_ping=True)
self.sessions = async_sessionmaker(self.engine, expire_on_commit=False)
async def initialize(self) -> None:
async with self.engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
async def close(self) -> None:
await self.engine.dispose()
@asynccontextmanager
async def session(self) -> AsyncIterator[AsyncSession]:
async with self.sessions() as session:
yield session
async def append_event(
self,
run_id: str,
user_id: str,
chat_id: str,
sequence: int,
event_type: str,
payload: dict[str, Any] | None = None,
) -> None:
async with self.session() as session:
session.add(
RunEvent(
run_id=run_id,
user_id=user_id,
chat_id=chat_id,
sequence=sequence,
event_type=event_type,
payload=payload or {},
)
)
await session.commit()
async def events_for_chat(self, user_id: str, chat_id: str, limit: int = 500) -> list[dict[str, Any]]:
async with self.session() as session:
rows = (
await session.scalars(
select(RunEvent)
.where(RunEvent.user_id == user_id, RunEvent.chat_id == chat_id)
.order_by(RunEvent.id.desc())
.limit(max(1, min(limit, 2000)))
)
).all()
return [
{
"run_id": row.run_id,
"sequence": row.sequence,
"type": row.event_type,
"payload": row.payload,
"created_at": row.created_at.isoformat(),
}
for row in reversed(rows)
]
async def remember(self, user_id: str, content: str) -> str:
item = Memory(user_id=user_id, content=content)
async with self.session() as session:
session.add(item)
await session.commit()
return item.id
async def recall(self, user_id: str, query: str = "", limit: int = 8) -> list[dict[str, str]]:
statement = select(Memory).where(Memory.user_id == user_id)
if query.strip():
statement = statement.where(Memory.content.ilike(f"%{query.strip()}%"))
statement = statement.order_by(Memory.created_at.desc()).limit(max(1, min(limit, 50)))
async with self.session() as session:
rows = (await session.scalars(statement)).all()
return [{"id": row.id, "content": row.content, "created_at": row.created_at.isoformat()} for row in rows]
async def forget(self, user_id: str, memory_id: str) -> bool:
async with self.session() as session:
result = await session.execute(delete(Memory).where(Memory.id == memory_id, Memory.user_id == user_id))
await session.commit()
return bool(result.rowcount)
async def update_plan(self, user_id: str, chat_id: str, items: list[dict[str, Any]]) -> None:
async with self.session() as session:
row = await session.scalar(select(Plan).where(Plan.user_id == user_id, Plan.chat_id == chat_id))
if row is None:
session.add(Plan(user_id=user_id, chat_id=chat_id, items=items))
else:
row.items = items
row.updated_at = datetime.now(UTC)
await session.commit()