Source code for oumi.environments.database_executable_environment
# Copyright 2025 - Oumi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Executable environment backed by a Database-isolated SQLite session."""
from __future__ import annotations
import sqlite3
import uuid
from collections.abc import Iterator
from contextlib import contextmanager, suppress
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from oumi.core.configs.params.base_params import BaseParams
from oumi.core.configs.params.environment_params import EnvironmentParams
from oumi.core.registry import register_environment
from oumi.core.types.tool_call import ToolResult
from oumi.environments.database_session import (
DatabaseSession,
materialize_sqlite_snapshot,
)
from oumi.environments.executable_environment import ExecutableEnvironment
from oumi.environments.executable_tool import ExecutableTool
from oumi.environments.utils import parse_env_kwargs
@dataclass
class DatabaseExecutableEnvironmentKwargs(BaseParams):
"""Type-specific kwargs for :class:`DatabaseExecutableEnvironment`."""
db_path: Path | str | None = None
schema_sql: str | None = None
seed_sql: str | None = None
def __finalize_and_validate__(self) -> None:
"""Validate the database source configuration."""
if self.seed_sql and not self.schema_sql:
raise ValueError("seed_sql requires schema_sql.")
if bool(self.db_path) == bool(self.schema_sql):
raise ValueError("Provide exactly one of db_path or schema_sql.")
@contextmanager
def _savepoint(connection: sqlite3.Connection, name: str) -> Iterator[None]:
# Randomized so model SQL can't RELEASE/ROLLBACK TO our savepoint and break cleanup.
sp = f"{name}_{uuid.uuid4().hex}"
connection.execute(f"SAVEPOINT {sp}")
try:
yield
except BaseException:
# Best-effort rollback: a cleanup failure must not mask the real error.
with suppress(sqlite3.Error):
connection.execute(f"ROLLBACK TO SAVEPOINT {sp}")
connection.execute(f"RELEASE SAVEPOINT {sp}")
raise
else:
connection.execute(f"RELEASE SAVEPOINT {sp}")
[docs]
@register_environment("database")
class DatabaseExecutableEnvironment(ExecutableEnvironment):
"""Runs SQL-executing tools against an isolated database session."""
def __init__(self, params: EnvironmentParams, session: DatabaseSession) -> None:
"""Bind the env to its params and an already-open Database session."""
super().__init__(params)
self._session = session
[docs]
@classmethod
def from_params(cls, params: EnvironmentParams) -> DatabaseExecutableEnvironment:
"""Build the env, opening a session over its configured DB.
``db_path`` shares one snapshot file across rollouts (scales to large DBs)
and is safe for concurrent *readers*, but SQLite serializes concurrent
*writers* on one file, so write-heavy concurrent rollouts should use
``schema_sql`` (a fresh per-rollout file) instead.
"""
kwargs = parse_env_kwargs(
DatabaseExecutableEnvironmentKwargs,
params,
env_label="DatabaseExecutableEnvironment",
)
if kwargs.db_path:
path = Path(kwargs.db_path)
if not path.is_file():
raise ValueError(
f"DatabaseExecutableEnvironment '{params.id}': db_path "
f"must reference an existing file: {path}"
)
session = DatabaseSession(path)
else:
assert kwargs.schema_sql is not None
snapshot = materialize_sqlite_snapshot(
schema_sql=kwargs.schema_sql, seed_sql=kwargs.seed_sql
)
session = DatabaseSession(snapshot, owns_file=True)
try:
return cls(params, session)
except BaseException:
session.close()
raise
[docs]
def requires_isolation(self) -> bool:
"""Each rollout needs its own session; never share across samples."""
return True
[docs]
def step(self, calls: list[tuple[str, dict[str, Any]]]) -> list[ToolResult]:
"""Execute a batch atomically within the rollout transaction."""
with _savepoint(self._session.connection, "oumi_batch"):
return super().step(calls)
@contextmanager
def _build_execution_context(
self, tool: ExecutableTool, arguments: dict[str, Any]
) -> Iterator[sqlite3.Connection]:
"""Bind a savepoint-scoped connection and enforce read-only tools."""
connection = self._session.connection
if tool.read_only:
connection.execute("PRAGMA query_only = ON")
try:
with _savepoint(connection, "oumi_tool_call"):
yield connection
finally:
if tool.read_only:
# Best-effort reset: don't let it mask an in-flight executor error.
with suppress(sqlite3.Error):
connection.execute("PRAGMA query_only = OFF")
[docs]
def close(self) -> None:
"""Roll back the episode's writes and tear down the session."""
self._session.close()