Source code for oumi.environments.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.

"""Abstract base for envs backed by user-supplied dotted-path executors."""

from __future__ import annotations

from abc import abstractmethod
from collections.abc import Callable
from contextlib import AbstractContextManager
from typing import Any

from oumi.core.configs.params.environment_params import EnvironmentParams
from oumi.core.configs.params.tool_params import ToolLookupError, ToolParams
from oumi.core.types.tool_call import ToolResult
from oumi.environments.base_environment import BaseEnvironment
from oumi.environments.executable_tool import ExecutableTool
from oumi.environments.utils import import_executor, validate_executor_result


[docs] class ExecutableEnvironment(BaseEnvironment): """Abstract base for envs that dispatch tool calls to Python executors. Each tool declares its executor as a dotted import path; the base resolves them into ``_executors`` at construction. Subclasses supply the per-call execution context (DB connection, HTTP client, FS root, ...) by implementing ``_build_execution_context``. The base owns tool lookup, argument and result validation, the ``_absorb_result`` post-hook, and the ``close`` lifecycle. Executors are invoked as ``executor(arguments=<dict>, context=<ctx>)`` and must return a ``ToolResult``. Result validation runs inside the execution context so a transactional context manager sees a validation failure and can roll back; ``_absorb_result`` runs only after the context exits cleanly. """ tool_params_cls: type[ToolParams] = ExecutableTool def __init__(self, params: EnvironmentParams) -> None: """Resolve each tool's dotted-path executor into ``_executors``.""" self._params = params self._executors: dict[str, Callable[..., Any]] = { tool.id: import_executor(tool.executor, tool.id) for tool in params.tools } @abstractmethod def _build_execution_context( self, tool: ExecutableTool, arguments: dict[str, Any] ) -> AbstractContextManager[Any]: """Yield the per-call execution context (DB conn, HTTP client, ...).""" def _absorb_result(self, tool: ExecutableTool, result: ToolResult) -> None: """Post-hook called after a successful executor call. Default no-op."""
[docs] def step(self, calls: list[tuple[str, dict[str, Any]]]) -> list[ToolResult]: """Execute a batch of tool calls; results are returned in input order.""" return [self._step_one(tool_id, arguments) for tool_id, arguments in calls]
def _lookup_tool(self, tool_id: str) -> ExecutableTool: for tool in self._params.tools: if tool.id == tool_id: return tool raise ToolLookupError( f"Tool '{tool_id}' not found in environment '{self._params.id}'. " f"Available tools: {[t.id for t in self._params.tools]}" ) def _step_one(self, tool_id: str, arguments: dict[str, Any]) -> ToolResult: tool = self._lookup_tool(tool_id) tool.validate_arguments(arguments) with self._build_execution_context(tool, arguments) as ctx: result = self._executors[tool_id](arguments=arguments, context=ctx) validated = validate_executor_result(tool, result) self._absorb_result(tool, validated) return validated