"""Engine, session plumbing, and shared column helpers. DATA_DIR is resolved relative to this file; the db package lives one level deeper than the old single-file module, hence the extra ``parent``. """ import json import uuid from datetime import datetime, timezone from pathlib import Path from typing import Any import sqlalchemy as sa from sqlalchemy.pool import StaticPool from sqlmodel import Session, SQLModel, create_engine DATA_DIR = Path(__file__).resolve().parent.parent.parent.parent.parent / "data" DATA_DIR.mkdir(parents=True, exist_ok=True) DATABASE_URL = f"sqlite:///{DATA_DIR / 'agenteval.db'}" FILES_DIR = DATA_DIR / "files" FILES_DIR.mkdir(parents=True, exist_ok=True) engine = create_engine( DATABASE_URL, echo=False, connect_args={"check_same_thread": False}, poolclass=StaticPool, ) # WAL mode allows concurrent reads during writes — critical for the evaluation # engine's high-frequency turn/result writes while the frontend polls. with engine.connect() as _conn: _conn.execute(sa.text("PRAGMA journal_mode=WAL")) _conn.commit() def utc_now() -> datetime: return datetime.now(timezone.utc) def as_utc(dt: datetime) -> datetime: """Attach UTC tzinfo to a naive datetime. SQLite round-trips drop tzinfo; stored times are always UTC, so a naive value read back is restored as UTC before any comparison with utc_now(). """ return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc) def iso_utc(dt: datetime | None) -> str | None: """Serialize a datetime to ISO 8601 with UTC timezone suffix. Guarantees the output always ends with 'Z' or '+00:00' so JavaScript's Date.parse() interprets it correctly as UTC (no 8-hour local-time offset). """ if dt is None: return None if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.isoformat(timespec="seconds").replace("+00:00", "Z") def new_uuid() -> str: return str(uuid.uuid4()) def _json_dumps(value: Any) -> str: """Serialize a JSON column value. ensure_ascii=False keeps CJK readable in the stored text — the single serialization口径 for all JSON columns.""" return json.dumps(value, ensure_ascii=False) def _json_loads(raw: str) -> Any: return json.loads(raw) def init_db() -> None: SQLModel.metadata.create_all(engine) def get_session() -> Session: return Session(engine) def get_session_context(): """Context manager that creates and properly closes a database session.""" session = Session(engine) try: yield session finally: session.close()