Upload folder using huggingface_hub
Browse files- Dockerfile +1 -2
- README.md +2 -2
- envs/repl_env/pyproject.toml +1 -1
- envs/repl_env/server/Dockerfile +1 -0
- envs/repl_env/server/__init__.py +0 -19
- envs/repl_env/server/app.py +0 -18
- pyproject.toml +1 -1
- server/Dockerfile +1 -0
- server/__init__.py +0 -19
- server/app.py +0 -18
- src/core/env_server/interfaces.py +3 -1
- src/core/env_server/mcp_environment.py +64 -91
- src/core/env_server/web_interface.py +4 -2
- src/core/openenv/__init__.py +12 -4
- src/core/openenv/cli/templates/openenv_env/pyproject.toml +1 -1
- src/core/openenv/core/env_server/interfaces.py +3 -1
- src/core/openenv/core/env_server/mcp_environment.py +64 -91
- src/core/openenv/core/env_server/web_interface.py +4 -2
- src/openenv/__init__.py +12 -4
- src/openenv/cli/templates/openenv_env/pyproject.toml +1 -1
- src/openenv/core/env_server/interfaces.py +3 -1
- src/openenv/core/env_server/mcp_environment.py +64 -91
- src/openenv/core/env_server/web_interface.py +4 -2
- src/openenv_core.egg-info/PKG-INFO +1 -1
- src/openenv_core.egg-info/SOURCES.txt +1 -2
Dockerfile
CHANGED
|
@@ -74,6 +74,7 @@ ENV PATH="/app/.venv/bin:$PATH"
|
|
| 74 |
|
| 75 |
# Set PYTHONPATH so imports work correctly
|
| 76 |
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
|
|
|
| 77 |
|
| 78 |
# Health check using Python (more portable than curl/wget)
|
| 79 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
@@ -82,5 +83,3 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
| 82 |
# Run the FastAPI server
|
| 83 |
# The module path is constructed to work with the /app/env structure
|
| 84 |
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
| 85 |
-
|
| 86 |
-
ENV ENABLE_WEB_INTERFACE=true
|
|
|
|
| 74 |
|
| 75 |
# Set PYTHONPATH so imports work correctly
|
| 76 |
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 77 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 78 |
|
| 79 |
# Health check using Python (more portable than curl/wget)
|
| 80 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
|
|
| 83 |
# Run the FastAPI server
|
| 84 |
# The module path is constructed to work with the /app/env structure
|
| 85 |
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
|
|
|
|
|
README.md
CHANGED
|
@@ -8,7 +8,7 @@ pinned: false
|
|
| 8 |
app_port: 8000
|
| 9 |
base_path: /web
|
| 10 |
tags:
|
| 11 |
-
- openenv-0.2.
|
| 12 |
- openenv
|
| 13 |
---
|
| 14 |
|
|
@@ -17,7 +17,7 @@ tags:
|
|
| 17 |
This Space is built from OpenEnv environment `repl_env`.
|
| 18 |
|
| 19 |
- Space URL: `https://huggingface.co/spaces/openenv/repl`
|
| 20 |
-
- OpenEnv pinned ref: `0.2.
|
| 21 |
- Hub tag: `openenv`
|
| 22 |
|
| 23 |
### Connecting from Code
|
|
|
|
| 8 |
app_port: 8000
|
| 9 |
base_path: /web
|
| 10 |
tags:
|
| 11 |
+
- openenv-0.2.3
|
| 12 |
- openenv
|
| 13 |
---
|
| 14 |
|
|
|
|
| 17 |
This Space is built from OpenEnv environment `repl_env`.
|
| 18 |
|
| 19 |
- Space URL: `https://huggingface.co/spaces/openenv/repl`
|
| 20 |
+
- OpenEnv pinned ref: `0.2.3`
|
| 21 |
- Hub tag: `openenv`
|
| 22 |
|
| 23 |
### Connecting from Code
|
envs/repl_env/pyproject.toml
CHANGED
|
@@ -15,7 +15,7 @@ description = "Recursive Language Model REPL Environment for OpenEnv"
|
|
| 15 |
requires-python = ">=3.10"
|
| 16 |
dependencies = [
|
| 17 |
# Core OpenEnv dependencies (required for server functionality)
|
| 18 |
-
"openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git@v0.2.
|
| 19 |
"fastapi>=0.115.0",
|
| 20 |
"pydantic>=2.0.0",
|
| 21 |
"uvicorn>=0.24.0",
|
|
|
|
| 15 |
requires-python = ">=3.10"
|
| 16 |
dependencies = [
|
| 17 |
# Core OpenEnv dependencies (required for server functionality)
|
| 18 |
+
"openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git@v0.2.3",
|
| 19 |
"fastapi>=0.115.0",
|
| 20 |
"pydantic>=2.0.0",
|
| 21 |
"uvicorn>=0.24.0",
|
envs/repl_env/server/Dockerfile
CHANGED
|
@@ -70,6 +70,7 @@ ENV PATH="/app/.venv/bin:$PATH"
|
|
| 70 |
|
| 71 |
# Set PYTHONPATH so imports work correctly
|
| 72 |
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
|
|
|
| 73 |
|
| 74 |
# Health check using Python (more portable than curl/wget)
|
| 75 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
|
|
| 70 |
|
| 71 |
# Set PYTHONPATH so imports work correctly
|
| 72 |
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 74 |
|
| 75 |
# Health check using Python (more portable than curl/wget)
|
| 76 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
envs/repl_env/server/__init__.py
CHANGED
|
@@ -10,25 +10,6 @@ REPL Environment Server Components.
|
|
| 10 |
This module contains the server-side implementation of the REPL environment.
|
| 11 |
"""
|
| 12 |
|
| 13 |
-
import sys
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def _prefer_bundled_openenv_src() -> None:
|
| 18 |
-
"""Ensure bundled src/openenv wins over installed openenv-core wheels."""
|
| 19 |
-
for parent in Path(__file__).resolve().parents:
|
| 20 |
-
src_dir = parent / "src"
|
| 21 |
-
if not (src_dir / "openenv").is_dir():
|
| 22 |
-
continue
|
| 23 |
-
src_path = str(src_dir)
|
| 24 |
-
if src_path in sys.path:
|
| 25 |
-
sys.path.remove(src_path)
|
| 26 |
-
sys.path.insert(0, src_path)
|
| 27 |
-
return
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
_prefer_bundled_openenv_src()
|
| 31 |
-
|
| 32 |
from .python_executor import PythonExecutor
|
| 33 |
from .repl_environment import REPLEnvironment
|
| 34 |
|
|
|
|
| 10 |
This module contains the server-side implementation of the REPL environment.
|
| 11 |
"""
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from .python_executor import PythonExecutor
|
| 14 |
from .repl_environment import REPLEnvironment
|
| 15 |
|
envs/repl_env/server/app.py
CHANGED
|
@@ -38,24 +38,6 @@ Environment Variables:
|
|
| 38 |
import inspect
|
| 39 |
import logging
|
| 40 |
import os
|
| 41 |
-
import sys
|
| 42 |
-
from pathlib import Path
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def _prefer_bundled_openenv_src() -> None:
|
| 46 |
-
"""Ensure the bundled repo src/ tree wins over installed openenv-core wheels."""
|
| 47 |
-
for parent in Path(__file__).resolve().parents:
|
| 48 |
-
src_dir = parent / "src"
|
| 49 |
-
if not (src_dir / "openenv").is_dir():
|
| 50 |
-
continue
|
| 51 |
-
src_path = str(src_dir)
|
| 52 |
-
if src_path in sys.path:
|
| 53 |
-
sys.path.remove(src_path)
|
| 54 |
-
sys.path.insert(0, src_path)
|
| 55 |
-
return
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
_prefer_bundled_openenv_src()
|
| 59 |
|
| 60 |
try:
|
| 61 |
from openenv.core.env_server.http_server import create_app
|
|
|
|
| 38 |
import inspect
|
| 39 |
import logging
|
| 40 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
try:
|
| 43 |
from openenv.core.env_server.http_server import create_app
|
pyproject.toml
CHANGED
|
@@ -15,7 +15,7 @@ description = "Recursive Language Model REPL Environment for OpenEnv"
|
|
| 15 |
requires-python = ">=3.10"
|
| 16 |
dependencies = [
|
| 17 |
# Core OpenEnv dependencies (required for server functionality)
|
| 18 |
-
"openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git@v0.2.
|
| 19 |
"fastapi>=0.115.0",
|
| 20 |
"pydantic>=2.0.0",
|
| 21 |
"uvicorn>=0.24.0",
|
|
|
|
| 15 |
requires-python = ">=3.10"
|
| 16 |
dependencies = [
|
| 17 |
# Core OpenEnv dependencies (required for server functionality)
|
| 18 |
+
"openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git@v0.2.3",
|
| 19 |
"fastapi>=0.115.0",
|
| 20 |
"pydantic>=2.0.0",
|
| 21 |
"uvicorn>=0.24.0",
|
server/Dockerfile
CHANGED
|
@@ -70,6 +70,7 @@ ENV PATH="/app/.venv/bin:$PATH"
|
|
| 70 |
|
| 71 |
# Set PYTHONPATH so imports work correctly
|
| 72 |
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
|
|
|
| 73 |
|
| 74 |
# Health check using Python (more portable than curl/wget)
|
| 75 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
|
|
| 70 |
|
| 71 |
# Set PYTHONPATH so imports work correctly
|
| 72 |
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
ENV ENABLE_WEB_INTERFACE=true
|
| 74 |
|
| 75 |
# Health check using Python (more portable than curl/wget)
|
| 76 |
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
server/__init__.py
CHANGED
|
@@ -10,25 +10,6 @@ REPL Environment Server Components.
|
|
| 10 |
This module contains the server-side implementation of the REPL environment.
|
| 11 |
"""
|
| 12 |
|
| 13 |
-
import sys
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def _prefer_bundled_openenv_src() -> None:
|
| 18 |
-
"""Ensure bundled src/openenv wins over installed openenv-core wheels."""
|
| 19 |
-
for parent in Path(__file__).resolve().parents:
|
| 20 |
-
src_dir = parent / "src"
|
| 21 |
-
if not (src_dir / "openenv").is_dir():
|
| 22 |
-
continue
|
| 23 |
-
src_path = str(src_dir)
|
| 24 |
-
if src_path in sys.path:
|
| 25 |
-
sys.path.remove(src_path)
|
| 26 |
-
sys.path.insert(0, src_path)
|
| 27 |
-
return
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
_prefer_bundled_openenv_src()
|
| 31 |
-
|
| 32 |
from .python_executor import PythonExecutor
|
| 33 |
from .repl_environment import REPLEnvironment
|
| 34 |
|
|
|
|
| 10 |
This module contains the server-side implementation of the REPL environment.
|
| 11 |
"""
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from .python_executor import PythonExecutor
|
| 14 |
from .repl_environment import REPLEnvironment
|
| 15 |
|
server/app.py
CHANGED
|
@@ -38,24 +38,6 @@ Environment Variables:
|
|
| 38 |
import inspect
|
| 39 |
import logging
|
| 40 |
import os
|
| 41 |
-
import sys
|
| 42 |
-
from pathlib import Path
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def _prefer_bundled_openenv_src() -> None:
|
| 46 |
-
"""Ensure the bundled repo src/ tree wins over installed openenv-core wheels."""
|
| 47 |
-
for parent in Path(__file__).resolve().parents:
|
| 48 |
-
src_dir = parent / "src"
|
| 49 |
-
if not (src_dir / "openenv").is_dir():
|
| 50 |
-
continue
|
| 51 |
-
src_path = str(src_dir)
|
| 52 |
-
if src_path in sys.path:
|
| 53 |
-
sys.path.remove(src_path)
|
| 54 |
-
sys.path.insert(0, src_path)
|
| 55 |
-
return
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
_prefer_bundled_openenv_src()
|
| 59 |
|
| 60 |
try:
|
| 61 |
from openenv.core.env_server.http_server import create_app
|
|
|
|
| 38 |
import inspect
|
| 39 |
import logging
|
| 40 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
try:
|
| 43 |
from openenv.core.env_server.http_server import create_app
|
src/core/env_server/interfaces.py
CHANGED
|
@@ -6,7 +6,9 @@
|
|
| 6 |
|
| 7 |
import inspect
|
| 8 |
from abc import ABC, abstractmethod
|
| 9 |
-
from typing import Any, Generic, Optional, Protocol, TYPE_CHECKING,
|
|
|
|
|
|
|
| 10 |
|
| 11 |
from .types import Action, EnvironmentMetadata, Observation, State
|
| 12 |
|
|
|
|
| 6 |
|
| 7 |
import inspect
|
| 8 |
from abc import ABC, abstractmethod
|
| 9 |
+
from typing import Any, Generic, Optional, Protocol, TYPE_CHECKING, TypeVar
|
| 10 |
+
|
| 11 |
+
from typing_extensions import TypedDict
|
| 12 |
|
| 13 |
from .types import Action, EnvironmentMetadata, Observation, State
|
| 14 |
|
src/core/env_server/mcp_environment.py
CHANGED
|
@@ -420,24 +420,49 @@ class MCPEnvironment(Environment):
|
|
| 420 |
return self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 421 |
|
| 422 |
def _handle_list_tools(self) -> ListToolsObservation:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
"""
|
| 424 |
-
|
| 425 |
|
| 426 |
Returns:
|
| 427 |
-
|
| 428 |
-
names, descriptions, and input schemas, filtered by current mode.
|
| 429 |
"""
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
current_mode = getattr(self, "_mode", None)
|
| 433 |
|
| 434 |
-
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
-
|
| 438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
for tool in tools_result:
|
| 442 |
if tool.name not in self._mode_tool_schemas:
|
| 443 |
tools.append(
|
|
@@ -449,11 +474,8 @@ class MCPEnvironment(Environment):
|
|
| 449 |
else {},
|
| 450 |
)
|
| 451 |
)
|
| 452 |
-
|
| 453 |
-
# Add mode-specific tools available in current mode
|
| 454 |
for tool_name, mode_schemas in self._mode_tool_schemas.items():
|
| 455 |
if None in mode_schemas:
|
| 456 |
-
# Tool available in all modes
|
| 457 |
schema = mode_schemas[None]
|
| 458 |
tools.append(
|
| 459 |
Tool(
|
|
@@ -463,7 +485,6 @@ class MCPEnvironment(Environment):
|
|
| 463 |
)
|
| 464 |
)
|
| 465 |
elif current_mode in mode_schemas:
|
| 466 |
-
# Tool available in current mode
|
| 467 |
schema = mode_schemas[current_mode]
|
| 468 |
tools.append(
|
| 469 |
Tool(
|
|
@@ -472,65 +493,30 @@ class MCPEnvironment(Environment):
|
|
| 472 |
input_schema=schema["input_schema"],
|
| 473 |
)
|
| 474 |
)
|
| 475 |
-
|
| 476 |
return ListToolsObservation(tools=tools)
|
| 477 |
-
|
| 478 |
except Exception as e:
|
| 479 |
-
# Return an observation with error in metadata
|
| 480 |
return ListToolsObservation(
|
| 481 |
tools=[],
|
| 482 |
-
metadata={
|
| 483 |
-
"error": str(e),
|
| 484 |
-
"error_type": "list_tools_failed",
|
| 485 |
-
},
|
| 486 |
)
|
| 487 |
|
| 488 |
-
async def
|
| 489 |
-
"""
|
| 490 |
-
Async helper to list tools from the MCP client.
|
| 491 |
-
|
| 492 |
-
Returns:
|
| 493 |
-
List of tool objects from the MCP server.
|
| 494 |
-
"""
|
| 495 |
-
async with self.mcp_session() as client:
|
| 496 |
-
return await client.list_tools()
|
| 497 |
-
|
| 498 |
-
def _handle_call_tool(
|
| 499 |
self,
|
| 500 |
action: CallToolAction,
|
| 501 |
timeout_s: Optional[float] = None,
|
| 502 |
) -> CallToolObservation:
|
| 503 |
-
"""
|
| 504 |
-
Handle a CallToolAction by invoking the specified tool.
|
| 505 |
-
|
| 506 |
-
Args:
|
| 507 |
-
action: The CallToolAction containing tool_name and arguments.
|
| 508 |
-
timeout_s: Timeout in seconds. Defaults to MCP_TOOL_CALL_TIMEOUT (30s).
|
| 509 |
-
|
| 510 |
-
Returns:
|
| 511 |
-
CallToolObservation with the tool's result or an error.
|
| 512 |
-
"""
|
| 513 |
timeout = timeout_s if timeout_s is not None else MCP_TOOL_CALL_TIMEOUT
|
| 514 |
-
|
| 515 |
-
# Check if this is a mode-specific tool
|
| 516 |
tool_name = action.tool_name
|
| 517 |
current_mode = getattr(self, "_mode", None)
|
| 518 |
|
| 519 |
if tool_name in self._mode_tools:
|
| 520 |
mode_info = self._mode_tools[tool_name]
|
| 521 |
-
|
| 522 |
-
# Check if tool is available in current mode
|
| 523 |
-
# Tool is available if:
|
| 524 |
-
# 1. It has a None mode (available in all modes), OR
|
| 525 |
-
# 2. It has an implementation for the current mode
|
| 526 |
if None in mode_info:
|
| 527 |
-
# Use the mode-agnostic version
|
| 528 |
func = mode_info[None]
|
| 529 |
elif current_mode in mode_info:
|
| 530 |
-
# Use the mode-specific version
|
| 531 |
func = mode_info[current_mode]
|
| 532 |
else:
|
| 533 |
-
# Tool not available in current mode
|
| 534 |
return CallToolObservation(
|
| 535 |
tool_name=tool_name,
|
| 536 |
result=None,
|
|
@@ -539,16 +525,11 @@ class MCPEnvironment(Environment):
|
|
| 539 |
message=f"Tool '{tool_name}' not available in {current_mode} mode",
|
| 540 |
),
|
| 541 |
)
|
| 542 |
-
|
| 543 |
-
# Call the mode-specific function directly
|
| 544 |
try:
|
| 545 |
-
# Check if function is async and await if necessary
|
| 546 |
if inspect.iscoroutinefunction(func):
|
| 547 |
-
result =
|
| 548 |
else:
|
| 549 |
result = func(**action.arguments)
|
| 550 |
-
|
| 551 |
-
# Wrap result in CallToolResult format to match FastMCP behavior
|
| 552 |
return CallToolObservation(
|
| 553 |
tool_name=tool_name,
|
| 554 |
result=CallToolResult(
|
|
@@ -569,22 +550,12 @@ class MCPEnvironment(Environment):
|
|
| 569 |
),
|
| 570 |
)
|
| 571 |
|
| 572 |
-
# Not a mode-specific tool, use FastMCP
|
| 573 |
try:
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
asyncio.wait_for(
|
| 578 |
-
self._async_call_tool(action.tool_name, action.arguments),
|
| 579 |
-
timeout=timeout,
|
| 580 |
-
)
|
| 581 |
-
)
|
| 582 |
-
|
| 583 |
-
return CallToolObservation(
|
| 584 |
-
tool_name=action.tool_name,
|
| 585 |
-
result=result,
|
| 586 |
)
|
| 587 |
-
|
| 588 |
except asyncio.TimeoutError:
|
| 589 |
return CallToolObservation(
|
| 590 |
tool_name=action.tool_name,
|
|
@@ -594,11 +565,8 @@ class MCPEnvironment(Environment):
|
|
| 594 |
message=f"Tool '{action.tool_name}' timed out after {timeout} seconds",
|
| 595 |
),
|
| 596 |
)
|
| 597 |
-
|
| 598 |
except Exception as e:
|
| 599 |
error_message = str(e)
|
| 600 |
-
|
| 601 |
-
# Determine error type based on the exception
|
| 602 |
if (
|
| 603 |
"not found" in error_message.lower()
|
| 604 |
or "unknown tool" in error_message.lower()
|
|
@@ -611,29 +579,34 @@ class MCPEnvironment(Environment):
|
|
| 611 |
error_type = ToolErrorType.INVALID_ARGS
|
| 612 |
else:
|
| 613 |
error_type = ToolErrorType.EXECUTION_ERROR
|
| 614 |
-
|
| 615 |
return CallToolObservation(
|
| 616 |
tool_name=action.tool_name,
|
| 617 |
result=None,
|
| 618 |
-
error=ToolError(
|
| 619 |
-
error_type=error_type,
|
| 620 |
-
message=error_message,
|
| 621 |
-
),
|
| 622 |
)
|
| 623 |
|
| 624 |
-
async def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
"""
|
| 626 |
-
Async
|
| 627 |
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
Returns:
|
| 633 |
-
The result from the tool execution.
|
| 634 |
"""
|
| 635 |
-
|
| 636 |
-
return await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
|
| 638 |
@abstractmethod
|
| 639 |
def _step_impl(
|
|
|
|
| 420 |
return self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 421 |
|
| 422 |
def _handle_list_tools(self) -> ListToolsObservation:
|
| 423 |
+
"""Sync wrapper — delegates to the canonical async implementation."""
|
| 424 |
+
return run_async_safely(self._async_handle_list_tools())
|
| 425 |
+
|
| 426 |
+
async def _async_list_tools(self) -> list:
|
| 427 |
"""
|
| 428 |
+
Async helper to list tools from the MCP client.
|
| 429 |
|
| 430 |
Returns:
|
| 431 |
+
List of tool objects from the MCP server.
|
|
|
|
| 432 |
"""
|
| 433 |
+
async with self.mcp_session() as client:
|
| 434 |
+
return await client.list_tools()
|
|
|
|
| 435 |
|
| 436 |
+
def _handle_call_tool(
|
| 437 |
+
self,
|
| 438 |
+
action: CallToolAction,
|
| 439 |
+
timeout_s: Optional[float] = None,
|
| 440 |
+
) -> CallToolObservation:
|
| 441 |
+
"""Sync wrapper — delegates to the canonical async implementation."""
|
| 442 |
+
return run_async_safely(
|
| 443 |
+
self._async_handle_call_tool(action, timeout_s=timeout_s)
|
| 444 |
+
)
|
| 445 |
|
| 446 |
+
async def _async_call_tool(self, tool_name: str, arguments: dict) -> Any:
|
| 447 |
+
"""
|
| 448 |
+
Async helper to call a tool on the MCP server.
|
| 449 |
+
|
| 450 |
+
Args:
|
| 451 |
+
tool_name: Name of the tool to invoke.
|
| 452 |
+
arguments: Dictionary of arguments to pass to the tool.
|
| 453 |
|
| 454 |
+
Returns:
|
| 455 |
+
The result from the tool execution.
|
| 456 |
+
"""
|
| 457 |
+
async with self.mcp_session() as client:
|
| 458 |
+
return await client.call_tool(tool_name, arguments)
|
| 459 |
+
|
| 460 |
+
async def _async_handle_list_tools(self) -> ListToolsObservation:
|
| 461 |
+
"""Async version of _handle_list_tools — avoids run_async_safely."""
|
| 462 |
+
try:
|
| 463 |
+
current_mode = getattr(self, "_mode", None)
|
| 464 |
+
tools_result = await self._async_list_tools()
|
| 465 |
+
tools = []
|
| 466 |
for tool in tools_result:
|
| 467 |
if tool.name not in self._mode_tool_schemas:
|
| 468 |
tools.append(
|
|
|
|
| 474 |
else {},
|
| 475 |
)
|
| 476 |
)
|
|
|
|
|
|
|
| 477 |
for tool_name, mode_schemas in self._mode_tool_schemas.items():
|
| 478 |
if None in mode_schemas:
|
|
|
|
| 479 |
schema = mode_schemas[None]
|
| 480 |
tools.append(
|
| 481 |
Tool(
|
|
|
|
| 485 |
)
|
| 486 |
)
|
| 487 |
elif current_mode in mode_schemas:
|
|
|
|
| 488 |
schema = mode_schemas[current_mode]
|
| 489 |
tools.append(
|
| 490 |
Tool(
|
|
|
|
| 493 |
input_schema=schema["input_schema"],
|
| 494 |
)
|
| 495 |
)
|
|
|
|
| 496 |
return ListToolsObservation(tools=tools)
|
|
|
|
| 497 |
except Exception as e:
|
|
|
|
| 498 |
return ListToolsObservation(
|
| 499 |
tools=[],
|
| 500 |
+
metadata={"error": str(e), "error_type": "list_tools_failed"},
|
|
|
|
|
|
|
|
|
|
| 501 |
)
|
| 502 |
|
| 503 |
+
async def _async_handle_call_tool(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
self,
|
| 505 |
action: CallToolAction,
|
| 506 |
timeout_s: Optional[float] = None,
|
| 507 |
) -> CallToolObservation:
|
| 508 |
+
"""Async version of _handle_call_tool — avoids run_async_safely."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
timeout = timeout_s if timeout_s is not None else MCP_TOOL_CALL_TIMEOUT
|
|
|
|
|
|
|
| 510 |
tool_name = action.tool_name
|
| 511 |
current_mode = getattr(self, "_mode", None)
|
| 512 |
|
| 513 |
if tool_name in self._mode_tools:
|
| 514 |
mode_info = self._mode_tools[tool_name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
if None in mode_info:
|
|
|
|
| 516 |
func = mode_info[None]
|
| 517 |
elif current_mode in mode_info:
|
|
|
|
| 518 |
func = mode_info[current_mode]
|
| 519 |
else:
|
|
|
|
| 520 |
return CallToolObservation(
|
| 521 |
tool_name=tool_name,
|
| 522 |
result=None,
|
|
|
|
| 525 |
message=f"Tool '{tool_name}' not available in {current_mode} mode",
|
| 526 |
),
|
| 527 |
)
|
|
|
|
|
|
|
| 528 |
try:
|
|
|
|
| 529 |
if inspect.iscoroutinefunction(func):
|
| 530 |
+
result = await func(**action.arguments)
|
| 531 |
else:
|
| 532 |
result = func(**action.arguments)
|
|
|
|
|
|
|
| 533 |
return CallToolObservation(
|
| 534 |
tool_name=tool_name,
|
| 535 |
result=CallToolResult(
|
|
|
|
| 550 |
),
|
| 551 |
)
|
| 552 |
|
|
|
|
| 553 |
try:
|
| 554 |
+
result = await asyncio.wait_for(
|
| 555 |
+
self._async_call_tool(action.tool_name, action.arguments),
|
| 556 |
+
timeout=timeout,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
)
|
| 558 |
+
return CallToolObservation(tool_name=action.tool_name, result=result)
|
| 559 |
except asyncio.TimeoutError:
|
| 560 |
return CallToolObservation(
|
| 561 |
tool_name=action.tool_name,
|
|
|
|
| 565 |
message=f"Tool '{action.tool_name}' timed out after {timeout} seconds",
|
| 566 |
),
|
| 567 |
)
|
|
|
|
| 568 |
except Exception as e:
|
| 569 |
error_message = str(e)
|
|
|
|
|
|
|
| 570 |
if (
|
| 571 |
"not found" in error_message.lower()
|
| 572 |
or "unknown tool" in error_message.lower()
|
|
|
|
| 579 |
error_type = ToolErrorType.INVALID_ARGS
|
| 580 |
else:
|
| 581 |
error_type = ToolErrorType.EXECUTION_ERROR
|
|
|
|
| 582 |
return CallToolObservation(
|
| 583 |
tool_name=action.tool_name,
|
| 584 |
result=None,
|
| 585 |
+
error=ToolError(error_type=error_type, message=error_message),
|
|
|
|
|
|
|
|
|
|
| 586 |
)
|
| 587 |
|
| 588 |
+
async def step_async(
|
| 589 |
+
self,
|
| 590 |
+
action: Action,
|
| 591 |
+
timeout_s: Optional[float] = None,
|
| 592 |
+
**kwargs: Any,
|
| 593 |
+
) -> Observation:
|
| 594 |
"""
|
| 595 |
+
Async step that routes MCP actions without going through run_async_safely.
|
| 596 |
|
| 597 |
+
The WebSocket handler calls this directly on the outer event loop, where
|
| 598 |
+
the MCP session is already open, avoiding the thread/event-loop deadlock
|
| 599 |
+
that occurs when the sync step() path is used via run_in_executor.
|
|
|
|
|
|
|
|
|
|
| 600 |
"""
|
| 601 |
+
if isinstance(action, ListToolsAction):
|
| 602 |
+
return await self._async_handle_list_tools()
|
| 603 |
+
elif isinstance(action, CallToolAction):
|
| 604 |
+
return await self._async_handle_call_tool(action, timeout_s=timeout_s)
|
| 605 |
+
else:
|
| 606 |
+
loop = asyncio.get_event_loop()
|
| 607 |
+
return await loop.run_in_executor(
|
| 608 |
+
None, lambda: self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 609 |
+
)
|
| 610 |
|
| 611 |
@abstractmethod
|
| 612 |
def _step_impl(
|
src/core/env_server/web_interface.py
CHANGED
|
@@ -22,7 +22,7 @@ from datetime import datetime
|
|
| 22 |
from typing import Any, Callable, Dict, List, Optional, Type
|
| 23 |
|
| 24 |
import gradio as gr
|
| 25 |
-
from fastapi import Body, FastAPI, HTTPException,
|
| 26 |
from fastapi.responses import RedirectResponse
|
| 27 |
from pydantic import BaseModel, ConfigDict, Field
|
| 28 |
|
|
@@ -356,7 +356,9 @@ class WebInterfaceManager:
|
|
| 356 |
else:
|
| 357 |
# Run sync reset in thread pool to avoid blocking event loop
|
| 358 |
# and to support environments using sync libraries (e.g., Playwright)
|
| 359 |
-
observation = await self._run_sync_in_thread_pool(
|
|
|
|
|
|
|
| 360 |
state: State = self.env.state
|
| 361 |
|
| 362 |
# Serialize observation once using shared utility
|
|
|
|
| 22 |
from typing import Any, Callable, Dict, List, Optional, Type
|
| 23 |
|
| 24 |
import gradio as gr
|
| 25 |
+
from fastapi import Body, FastAPI, HTTPException, status, WebSocket, WebSocketDisconnect
|
| 26 |
from fastapi.responses import RedirectResponse
|
| 27 |
from pydantic import BaseModel, ConfigDict, Field
|
| 28 |
|
|
|
|
| 356 |
else:
|
| 357 |
# Run sync reset in thread pool to avoid blocking event loop
|
| 358 |
# and to support environments using sync libraries (e.g., Playwright)
|
| 359 |
+
observation = await self._run_sync_in_thread_pool(
|
| 360 |
+
self.env.reset, **valid_kwargs
|
| 361 |
+
)
|
| 362 |
state: State = self.env.state
|
| 363 |
|
| 364 |
# Serialize observation once using shared utility
|
src/core/openenv/__init__.py
CHANGED
|
@@ -14,10 +14,18 @@ __all__ = [
|
|
| 14 |
"SyncEnvClient",
|
| 15 |
]
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
_LAZY_MODULES = {
|
|
|
|
| 14 |
"SyncEnvClient",
|
| 15 |
]
|
| 16 |
|
| 17 |
+
|
| 18 |
+
def _load_package_version() -> str:
|
| 19 |
+
"""Resolve the installed distribution version for the OpenEnv package."""
|
| 20 |
+
for distribution_name in ("openenv-core", "openenv"):
|
| 21 |
+
try:
|
| 22 |
+
return metadata.version(distribution_name)
|
| 23 |
+
except metadata.PackageNotFoundError:
|
| 24 |
+
continue
|
| 25 |
+
return "0.0.0"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
__version__ = _load_package_version()
|
| 29 |
|
| 30 |
|
| 31 |
_LAZY_MODULES = {
|
src/core/openenv/cli/templates/openenv_env/pyproject.toml
CHANGED
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
| 17 |
# Core OpenEnv runtime (provides FastAPI server + HTTP client types)
|
| 18 |
# install from github
|
| 19 |
# "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
|
| 20 |
-
"openenv-core[core]>=0.2.
|
| 21 |
# Environment-specific dependencies
|
| 22 |
# Add all dependencies needed for your environment here
|
| 23 |
# Examples:
|
|
|
|
| 17 |
# Core OpenEnv runtime (provides FastAPI server + HTTP client types)
|
| 18 |
# install from github
|
| 19 |
# "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
|
| 20 |
+
"openenv-core[core]>=0.2.2",
|
| 21 |
# Environment-specific dependencies
|
| 22 |
# Add all dependencies needed for your environment here
|
| 23 |
# Examples:
|
src/core/openenv/core/env_server/interfaces.py
CHANGED
|
@@ -6,7 +6,9 @@
|
|
| 6 |
|
| 7 |
import inspect
|
| 8 |
from abc import ABC, abstractmethod
|
| 9 |
-
from typing import Any, Generic, Optional, Protocol, TYPE_CHECKING,
|
|
|
|
|
|
|
| 10 |
|
| 11 |
from .types import Action, EnvironmentMetadata, Observation, State
|
| 12 |
|
|
|
|
| 6 |
|
| 7 |
import inspect
|
| 8 |
from abc import ABC, abstractmethod
|
| 9 |
+
from typing import Any, Generic, Optional, Protocol, TYPE_CHECKING, TypeVar
|
| 10 |
+
|
| 11 |
+
from typing_extensions import TypedDict
|
| 12 |
|
| 13 |
from .types import Action, EnvironmentMetadata, Observation, State
|
| 14 |
|
src/core/openenv/core/env_server/mcp_environment.py
CHANGED
|
@@ -420,24 +420,49 @@ class MCPEnvironment(Environment):
|
|
| 420 |
return self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 421 |
|
| 422 |
def _handle_list_tools(self) -> ListToolsObservation:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
"""
|
| 424 |
-
|
| 425 |
|
| 426 |
Returns:
|
| 427 |
-
|
| 428 |
-
names, descriptions, and input schemas, filtered by current mode.
|
| 429 |
"""
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
current_mode = getattr(self, "_mode", None)
|
| 433 |
|
| 434 |
-
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
-
|
| 438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
for tool in tools_result:
|
| 442 |
if tool.name not in self._mode_tool_schemas:
|
| 443 |
tools.append(
|
|
@@ -449,11 +474,8 @@ class MCPEnvironment(Environment):
|
|
| 449 |
else {},
|
| 450 |
)
|
| 451 |
)
|
| 452 |
-
|
| 453 |
-
# Add mode-specific tools available in current mode
|
| 454 |
for tool_name, mode_schemas in self._mode_tool_schemas.items():
|
| 455 |
if None in mode_schemas:
|
| 456 |
-
# Tool available in all modes
|
| 457 |
schema = mode_schemas[None]
|
| 458 |
tools.append(
|
| 459 |
Tool(
|
|
@@ -463,7 +485,6 @@ class MCPEnvironment(Environment):
|
|
| 463 |
)
|
| 464 |
)
|
| 465 |
elif current_mode in mode_schemas:
|
| 466 |
-
# Tool available in current mode
|
| 467 |
schema = mode_schemas[current_mode]
|
| 468 |
tools.append(
|
| 469 |
Tool(
|
|
@@ -472,65 +493,30 @@ class MCPEnvironment(Environment):
|
|
| 472 |
input_schema=schema["input_schema"],
|
| 473 |
)
|
| 474 |
)
|
| 475 |
-
|
| 476 |
return ListToolsObservation(tools=tools)
|
| 477 |
-
|
| 478 |
except Exception as e:
|
| 479 |
-
# Return an observation with error in metadata
|
| 480 |
return ListToolsObservation(
|
| 481 |
tools=[],
|
| 482 |
-
metadata={
|
| 483 |
-
"error": str(e),
|
| 484 |
-
"error_type": "list_tools_failed",
|
| 485 |
-
},
|
| 486 |
)
|
| 487 |
|
| 488 |
-
async def
|
| 489 |
-
"""
|
| 490 |
-
Async helper to list tools from the MCP client.
|
| 491 |
-
|
| 492 |
-
Returns:
|
| 493 |
-
List of tool objects from the MCP server.
|
| 494 |
-
"""
|
| 495 |
-
async with self.mcp_session() as client:
|
| 496 |
-
return await client.list_tools()
|
| 497 |
-
|
| 498 |
-
def _handle_call_tool(
|
| 499 |
self,
|
| 500 |
action: CallToolAction,
|
| 501 |
timeout_s: Optional[float] = None,
|
| 502 |
) -> CallToolObservation:
|
| 503 |
-
"""
|
| 504 |
-
Handle a CallToolAction by invoking the specified tool.
|
| 505 |
-
|
| 506 |
-
Args:
|
| 507 |
-
action: The CallToolAction containing tool_name and arguments.
|
| 508 |
-
timeout_s: Timeout in seconds. Defaults to MCP_TOOL_CALL_TIMEOUT (30s).
|
| 509 |
-
|
| 510 |
-
Returns:
|
| 511 |
-
CallToolObservation with the tool's result or an error.
|
| 512 |
-
"""
|
| 513 |
timeout = timeout_s if timeout_s is not None else MCP_TOOL_CALL_TIMEOUT
|
| 514 |
-
|
| 515 |
-
# Check if this is a mode-specific tool
|
| 516 |
tool_name = action.tool_name
|
| 517 |
current_mode = getattr(self, "_mode", None)
|
| 518 |
|
| 519 |
if tool_name in self._mode_tools:
|
| 520 |
mode_info = self._mode_tools[tool_name]
|
| 521 |
-
|
| 522 |
-
# Check if tool is available in current mode
|
| 523 |
-
# Tool is available if:
|
| 524 |
-
# 1. It has a None mode (available in all modes), OR
|
| 525 |
-
# 2. It has an implementation for the current mode
|
| 526 |
if None in mode_info:
|
| 527 |
-
# Use the mode-agnostic version
|
| 528 |
func = mode_info[None]
|
| 529 |
elif current_mode in mode_info:
|
| 530 |
-
# Use the mode-specific version
|
| 531 |
func = mode_info[current_mode]
|
| 532 |
else:
|
| 533 |
-
# Tool not available in current mode
|
| 534 |
return CallToolObservation(
|
| 535 |
tool_name=tool_name,
|
| 536 |
result=None,
|
|
@@ -539,16 +525,11 @@ class MCPEnvironment(Environment):
|
|
| 539 |
message=f"Tool '{tool_name}' not available in {current_mode} mode",
|
| 540 |
),
|
| 541 |
)
|
| 542 |
-
|
| 543 |
-
# Call the mode-specific function directly
|
| 544 |
try:
|
| 545 |
-
# Check if function is async and await if necessary
|
| 546 |
if inspect.iscoroutinefunction(func):
|
| 547 |
-
result =
|
| 548 |
else:
|
| 549 |
result = func(**action.arguments)
|
| 550 |
-
|
| 551 |
-
# Wrap result in CallToolResult format to match FastMCP behavior
|
| 552 |
return CallToolObservation(
|
| 553 |
tool_name=tool_name,
|
| 554 |
result=CallToolResult(
|
|
@@ -569,22 +550,12 @@ class MCPEnvironment(Environment):
|
|
| 569 |
),
|
| 570 |
)
|
| 571 |
|
| 572 |
-
# Not a mode-specific tool, use FastMCP
|
| 573 |
try:
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
asyncio.wait_for(
|
| 578 |
-
self._async_call_tool(action.tool_name, action.arguments),
|
| 579 |
-
timeout=timeout,
|
| 580 |
-
)
|
| 581 |
-
)
|
| 582 |
-
|
| 583 |
-
return CallToolObservation(
|
| 584 |
-
tool_name=action.tool_name,
|
| 585 |
-
result=result,
|
| 586 |
)
|
| 587 |
-
|
| 588 |
except asyncio.TimeoutError:
|
| 589 |
return CallToolObservation(
|
| 590 |
tool_name=action.tool_name,
|
|
@@ -594,11 +565,8 @@ class MCPEnvironment(Environment):
|
|
| 594 |
message=f"Tool '{action.tool_name}' timed out after {timeout} seconds",
|
| 595 |
),
|
| 596 |
)
|
| 597 |
-
|
| 598 |
except Exception as e:
|
| 599 |
error_message = str(e)
|
| 600 |
-
|
| 601 |
-
# Determine error type based on the exception
|
| 602 |
if (
|
| 603 |
"not found" in error_message.lower()
|
| 604 |
or "unknown tool" in error_message.lower()
|
|
@@ -611,29 +579,34 @@ class MCPEnvironment(Environment):
|
|
| 611 |
error_type = ToolErrorType.INVALID_ARGS
|
| 612 |
else:
|
| 613 |
error_type = ToolErrorType.EXECUTION_ERROR
|
| 614 |
-
|
| 615 |
return CallToolObservation(
|
| 616 |
tool_name=action.tool_name,
|
| 617 |
result=None,
|
| 618 |
-
error=ToolError(
|
| 619 |
-
error_type=error_type,
|
| 620 |
-
message=error_message,
|
| 621 |
-
),
|
| 622 |
)
|
| 623 |
|
| 624 |
-
async def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
"""
|
| 626 |
-
Async
|
| 627 |
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
Returns:
|
| 633 |
-
The result from the tool execution.
|
| 634 |
"""
|
| 635 |
-
|
| 636 |
-
return await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
|
| 638 |
@abstractmethod
|
| 639 |
def _step_impl(
|
|
|
|
| 420 |
return self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 421 |
|
| 422 |
def _handle_list_tools(self) -> ListToolsObservation:
|
| 423 |
+
"""Sync wrapper — delegates to the canonical async implementation."""
|
| 424 |
+
return run_async_safely(self._async_handle_list_tools())
|
| 425 |
+
|
| 426 |
+
async def _async_list_tools(self) -> list:
|
| 427 |
"""
|
| 428 |
+
Async helper to list tools from the MCP client.
|
| 429 |
|
| 430 |
Returns:
|
| 431 |
+
List of tool objects from the MCP server.
|
|
|
|
| 432 |
"""
|
| 433 |
+
async with self.mcp_session() as client:
|
| 434 |
+
return await client.list_tools()
|
|
|
|
| 435 |
|
| 436 |
+
def _handle_call_tool(
|
| 437 |
+
self,
|
| 438 |
+
action: CallToolAction,
|
| 439 |
+
timeout_s: Optional[float] = None,
|
| 440 |
+
) -> CallToolObservation:
|
| 441 |
+
"""Sync wrapper — delegates to the canonical async implementation."""
|
| 442 |
+
return run_async_safely(
|
| 443 |
+
self._async_handle_call_tool(action, timeout_s=timeout_s)
|
| 444 |
+
)
|
| 445 |
|
| 446 |
+
async def _async_call_tool(self, tool_name: str, arguments: dict) -> Any:
|
| 447 |
+
"""
|
| 448 |
+
Async helper to call a tool on the MCP server.
|
| 449 |
+
|
| 450 |
+
Args:
|
| 451 |
+
tool_name: Name of the tool to invoke.
|
| 452 |
+
arguments: Dictionary of arguments to pass to the tool.
|
| 453 |
|
| 454 |
+
Returns:
|
| 455 |
+
The result from the tool execution.
|
| 456 |
+
"""
|
| 457 |
+
async with self.mcp_session() as client:
|
| 458 |
+
return await client.call_tool(tool_name, arguments)
|
| 459 |
+
|
| 460 |
+
async def _async_handle_list_tools(self) -> ListToolsObservation:
|
| 461 |
+
"""Async version of _handle_list_tools — avoids run_async_safely."""
|
| 462 |
+
try:
|
| 463 |
+
current_mode = getattr(self, "_mode", None)
|
| 464 |
+
tools_result = await self._async_list_tools()
|
| 465 |
+
tools = []
|
| 466 |
for tool in tools_result:
|
| 467 |
if tool.name not in self._mode_tool_schemas:
|
| 468 |
tools.append(
|
|
|
|
| 474 |
else {},
|
| 475 |
)
|
| 476 |
)
|
|
|
|
|
|
|
| 477 |
for tool_name, mode_schemas in self._mode_tool_schemas.items():
|
| 478 |
if None in mode_schemas:
|
|
|
|
| 479 |
schema = mode_schemas[None]
|
| 480 |
tools.append(
|
| 481 |
Tool(
|
|
|
|
| 485 |
)
|
| 486 |
)
|
| 487 |
elif current_mode in mode_schemas:
|
|
|
|
| 488 |
schema = mode_schemas[current_mode]
|
| 489 |
tools.append(
|
| 490 |
Tool(
|
|
|
|
| 493 |
input_schema=schema["input_schema"],
|
| 494 |
)
|
| 495 |
)
|
|
|
|
| 496 |
return ListToolsObservation(tools=tools)
|
|
|
|
| 497 |
except Exception as e:
|
|
|
|
| 498 |
return ListToolsObservation(
|
| 499 |
tools=[],
|
| 500 |
+
metadata={"error": str(e), "error_type": "list_tools_failed"},
|
|
|
|
|
|
|
|
|
|
| 501 |
)
|
| 502 |
|
| 503 |
+
async def _async_handle_call_tool(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
self,
|
| 505 |
action: CallToolAction,
|
| 506 |
timeout_s: Optional[float] = None,
|
| 507 |
) -> CallToolObservation:
|
| 508 |
+
"""Async version of _handle_call_tool — avoids run_async_safely."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
timeout = timeout_s if timeout_s is not None else MCP_TOOL_CALL_TIMEOUT
|
|
|
|
|
|
|
| 510 |
tool_name = action.tool_name
|
| 511 |
current_mode = getattr(self, "_mode", None)
|
| 512 |
|
| 513 |
if tool_name in self._mode_tools:
|
| 514 |
mode_info = self._mode_tools[tool_name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
if None in mode_info:
|
|
|
|
| 516 |
func = mode_info[None]
|
| 517 |
elif current_mode in mode_info:
|
|
|
|
| 518 |
func = mode_info[current_mode]
|
| 519 |
else:
|
|
|
|
| 520 |
return CallToolObservation(
|
| 521 |
tool_name=tool_name,
|
| 522 |
result=None,
|
|
|
|
| 525 |
message=f"Tool '{tool_name}' not available in {current_mode} mode",
|
| 526 |
),
|
| 527 |
)
|
|
|
|
|
|
|
| 528 |
try:
|
|
|
|
| 529 |
if inspect.iscoroutinefunction(func):
|
| 530 |
+
result = await func(**action.arguments)
|
| 531 |
else:
|
| 532 |
result = func(**action.arguments)
|
|
|
|
|
|
|
| 533 |
return CallToolObservation(
|
| 534 |
tool_name=tool_name,
|
| 535 |
result=CallToolResult(
|
|
|
|
| 550 |
),
|
| 551 |
)
|
| 552 |
|
|
|
|
| 553 |
try:
|
| 554 |
+
result = await asyncio.wait_for(
|
| 555 |
+
self._async_call_tool(action.tool_name, action.arguments),
|
| 556 |
+
timeout=timeout,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
)
|
| 558 |
+
return CallToolObservation(tool_name=action.tool_name, result=result)
|
| 559 |
except asyncio.TimeoutError:
|
| 560 |
return CallToolObservation(
|
| 561 |
tool_name=action.tool_name,
|
|
|
|
| 565 |
message=f"Tool '{action.tool_name}' timed out after {timeout} seconds",
|
| 566 |
),
|
| 567 |
)
|
|
|
|
| 568 |
except Exception as e:
|
| 569 |
error_message = str(e)
|
|
|
|
|
|
|
| 570 |
if (
|
| 571 |
"not found" in error_message.lower()
|
| 572 |
or "unknown tool" in error_message.lower()
|
|
|
|
| 579 |
error_type = ToolErrorType.INVALID_ARGS
|
| 580 |
else:
|
| 581 |
error_type = ToolErrorType.EXECUTION_ERROR
|
|
|
|
| 582 |
return CallToolObservation(
|
| 583 |
tool_name=action.tool_name,
|
| 584 |
result=None,
|
| 585 |
+
error=ToolError(error_type=error_type, message=error_message),
|
|
|
|
|
|
|
|
|
|
| 586 |
)
|
| 587 |
|
| 588 |
+
async def step_async(
|
| 589 |
+
self,
|
| 590 |
+
action: Action,
|
| 591 |
+
timeout_s: Optional[float] = None,
|
| 592 |
+
**kwargs: Any,
|
| 593 |
+
) -> Observation:
|
| 594 |
"""
|
| 595 |
+
Async step that routes MCP actions without going through run_async_safely.
|
| 596 |
|
| 597 |
+
The WebSocket handler calls this directly on the outer event loop, where
|
| 598 |
+
the MCP session is already open, avoiding the thread/event-loop deadlock
|
| 599 |
+
that occurs when the sync step() path is used via run_in_executor.
|
|
|
|
|
|
|
|
|
|
| 600 |
"""
|
| 601 |
+
if isinstance(action, ListToolsAction):
|
| 602 |
+
return await self._async_handle_list_tools()
|
| 603 |
+
elif isinstance(action, CallToolAction):
|
| 604 |
+
return await self._async_handle_call_tool(action, timeout_s=timeout_s)
|
| 605 |
+
else:
|
| 606 |
+
loop = asyncio.get_event_loop()
|
| 607 |
+
return await loop.run_in_executor(
|
| 608 |
+
None, lambda: self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 609 |
+
)
|
| 610 |
|
| 611 |
@abstractmethod
|
| 612 |
def _step_impl(
|
src/core/openenv/core/env_server/web_interface.py
CHANGED
|
@@ -22,7 +22,7 @@ from datetime import datetime
|
|
| 22 |
from typing import Any, Callable, Dict, List, Optional, Type
|
| 23 |
|
| 24 |
import gradio as gr
|
| 25 |
-
from fastapi import Body, FastAPI, HTTPException,
|
| 26 |
from fastapi.responses import RedirectResponse
|
| 27 |
from pydantic import BaseModel, ConfigDict, Field
|
| 28 |
|
|
@@ -356,7 +356,9 @@ class WebInterfaceManager:
|
|
| 356 |
else:
|
| 357 |
# Run sync reset in thread pool to avoid blocking event loop
|
| 358 |
# and to support environments using sync libraries (e.g., Playwright)
|
| 359 |
-
observation = await self._run_sync_in_thread_pool(
|
|
|
|
|
|
|
| 360 |
state: State = self.env.state
|
| 361 |
|
| 362 |
# Serialize observation once using shared utility
|
|
|
|
| 22 |
from typing import Any, Callable, Dict, List, Optional, Type
|
| 23 |
|
| 24 |
import gradio as gr
|
| 25 |
+
from fastapi import Body, FastAPI, HTTPException, status, WebSocket, WebSocketDisconnect
|
| 26 |
from fastapi.responses import RedirectResponse
|
| 27 |
from pydantic import BaseModel, ConfigDict, Field
|
| 28 |
|
|
|
|
| 356 |
else:
|
| 357 |
# Run sync reset in thread pool to avoid blocking event loop
|
| 358 |
# and to support environments using sync libraries (e.g., Playwright)
|
| 359 |
+
observation = await self._run_sync_in_thread_pool(
|
| 360 |
+
self.env.reset, **valid_kwargs
|
| 361 |
+
)
|
| 362 |
state: State = self.env.state
|
| 363 |
|
| 364 |
# Serialize observation once using shared utility
|
src/openenv/__init__.py
CHANGED
|
@@ -14,10 +14,18 @@ __all__ = [
|
|
| 14 |
"SyncEnvClient",
|
| 15 |
]
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
_LAZY_MODULES = {
|
|
|
|
| 14 |
"SyncEnvClient",
|
| 15 |
]
|
| 16 |
|
| 17 |
+
|
| 18 |
+
def _load_package_version() -> str:
|
| 19 |
+
"""Resolve the installed distribution version for the OpenEnv package."""
|
| 20 |
+
for distribution_name in ("openenv-core", "openenv"):
|
| 21 |
+
try:
|
| 22 |
+
return metadata.version(distribution_name)
|
| 23 |
+
except metadata.PackageNotFoundError:
|
| 24 |
+
continue
|
| 25 |
+
return "0.0.0"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
__version__ = _load_package_version()
|
| 29 |
|
| 30 |
|
| 31 |
_LAZY_MODULES = {
|
src/openenv/cli/templates/openenv_env/pyproject.toml
CHANGED
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
| 17 |
# Core OpenEnv runtime (provides FastAPI server + HTTP client types)
|
| 18 |
# install from github
|
| 19 |
# "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
|
| 20 |
-
"openenv-core[core]>=0.2.
|
| 21 |
# Environment-specific dependencies
|
| 22 |
# Add all dependencies needed for your environment here
|
| 23 |
# Examples:
|
|
|
|
| 17 |
# Core OpenEnv runtime (provides FastAPI server + HTTP client types)
|
| 18 |
# install from github
|
| 19 |
# "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
|
| 20 |
+
"openenv-core[core]>=0.2.2",
|
| 21 |
# Environment-specific dependencies
|
| 22 |
# Add all dependencies needed for your environment here
|
| 23 |
# Examples:
|
src/openenv/core/env_server/interfaces.py
CHANGED
|
@@ -6,7 +6,9 @@
|
|
| 6 |
|
| 7 |
import inspect
|
| 8 |
from abc import ABC, abstractmethod
|
| 9 |
-
from typing import Any, Generic, Optional, Protocol, TYPE_CHECKING,
|
|
|
|
|
|
|
| 10 |
|
| 11 |
from .types import Action, EnvironmentMetadata, Observation, State
|
| 12 |
|
|
|
|
| 6 |
|
| 7 |
import inspect
|
| 8 |
from abc import ABC, abstractmethod
|
| 9 |
+
from typing import Any, Generic, Optional, Protocol, TYPE_CHECKING, TypeVar
|
| 10 |
+
|
| 11 |
+
from typing_extensions import TypedDict
|
| 12 |
|
| 13 |
from .types import Action, EnvironmentMetadata, Observation, State
|
| 14 |
|
src/openenv/core/env_server/mcp_environment.py
CHANGED
|
@@ -420,24 +420,49 @@ class MCPEnvironment(Environment):
|
|
| 420 |
return self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 421 |
|
| 422 |
def _handle_list_tools(self) -> ListToolsObservation:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
"""
|
| 424 |
-
|
| 425 |
|
| 426 |
Returns:
|
| 427 |
-
|
| 428 |
-
names, descriptions, and input schemas, filtered by current mode.
|
| 429 |
"""
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
current_mode = getattr(self, "_mode", None)
|
| 433 |
|
| 434 |
-
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
-
|
| 438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
for tool in tools_result:
|
| 442 |
if tool.name not in self._mode_tool_schemas:
|
| 443 |
tools.append(
|
|
@@ -449,11 +474,8 @@ class MCPEnvironment(Environment):
|
|
| 449 |
else {},
|
| 450 |
)
|
| 451 |
)
|
| 452 |
-
|
| 453 |
-
# Add mode-specific tools available in current mode
|
| 454 |
for tool_name, mode_schemas in self._mode_tool_schemas.items():
|
| 455 |
if None in mode_schemas:
|
| 456 |
-
# Tool available in all modes
|
| 457 |
schema = mode_schemas[None]
|
| 458 |
tools.append(
|
| 459 |
Tool(
|
|
@@ -463,7 +485,6 @@ class MCPEnvironment(Environment):
|
|
| 463 |
)
|
| 464 |
)
|
| 465 |
elif current_mode in mode_schemas:
|
| 466 |
-
# Tool available in current mode
|
| 467 |
schema = mode_schemas[current_mode]
|
| 468 |
tools.append(
|
| 469 |
Tool(
|
|
@@ -472,65 +493,30 @@ class MCPEnvironment(Environment):
|
|
| 472 |
input_schema=schema["input_schema"],
|
| 473 |
)
|
| 474 |
)
|
| 475 |
-
|
| 476 |
return ListToolsObservation(tools=tools)
|
| 477 |
-
|
| 478 |
except Exception as e:
|
| 479 |
-
# Return an observation with error in metadata
|
| 480 |
return ListToolsObservation(
|
| 481 |
tools=[],
|
| 482 |
-
metadata={
|
| 483 |
-
"error": str(e),
|
| 484 |
-
"error_type": "list_tools_failed",
|
| 485 |
-
},
|
| 486 |
)
|
| 487 |
|
| 488 |
-
async def
|
| 489 |
-
"""
|
| 490 |
-
Async helper to list tools from the MCP client.
|
| 491 |
-
|
| 492 |
-
Returns:
|
| 493 |
-
List of tool objects from the MCP server.
|
| 494 |
-
"""
|
| 495 |
-
async with self.mcp_session() as client:
|
| 496 |
-
return await client.list_tools()
|
| 497 |
-
|
| 498 |
-
def _handle_call_tool(
|
| 499 |
self,
|
| 500 |
action: CallToolAction,
|
| 501 |
timeout_s: Optional[float] = None,
|
| 502 |
) -> CallToolObservation:
|
| 503 |
-
"""
|
| 504 |
-
Handle a CallToolAction by invoking the specified tool.
|
| 505 |
-
|
| 506 |
-
Args:
|
| 507 |
-
action: The CallToolAction containing tool_name and arguments.
|
| 508 |
-
timeout_s: Timeout in seconds. Defaults to MCP_TOOL_CALL_TIMEOUT (30s).
|
| 509 |
-
|
| 510 |
-
Returns:
|
| 511 |
-
CallToolObservation with the tool's result or an error.
|
| 512 |
-
"""
|
| 513 |
timeout = timeout_s if timeout_s is not None else MCP_TOOL_CALL_TIMEOUT
|
| 514 |
-
|
| 515 |
-
# Check if this is a mode-specific tool
|
| 516 |
tool_name = action.tool_name
|
| 517 |
current_mode = getattr(self, "_mode", None)
|
| 518 |
|
| 519 |
if tool_name in self._mode_tools:
|
| 520 |
mode_info = self._mode_tools[tool_name]
|
| 521 |
-
|
| 522 |
-
# Check if tool is available in current mode
|
| 523 |
-
# Tool is available if:
|
| 524 |
-
# 1. It has a None mode (available in all modes), OR
|
| 525 |
-
# 2. It has an implementation for the current mode
|
| 526 |
if None in mode_info:
|
| 527 |
-
# Use the mode-agnostic version
|
| 528 |
func = mode_info[None]
|
| 529 |
elif current_mode in mode_info:
|
| 530 |
-
# Use the mode-specific version
|
| 531 |
func = mode_info[current_mode]
|
| 532 |
else:
|
| 533 |
-
# Tool not available in current mode
|
| 534 |
return CallToolObservation(
|
| 535 |
tool_name=tool_name,
|
| 536 |
result=None,
|
|
@@ -539,16 +525,11 @@ class MCPEnvironment(Environment):
|
|
| 539 |
message=f"Tool '{tool_name}' not available in {current_mode} mode",
|
| 540 |
),
|
| 541 |
)
|
| 542 |
-
|
| 543 |
-
# Call the mode-specific function directly
|
| 544 |
try:
|
| 545 |
-
# Check if function is async and await if necessary
|
| 546 |
if inspect.iscoroutinefunction(func):
|
| 547 |
-
result =
|
| 548 |
else:
|
| 549 |
result = func(**action.arguments)
|
| 550 |
-
|
| 551 |
-
# Wrap result in CallToolResult format to match FastMCP behavior
|
| 552 |
return CallToolObservation(
|
| 553 |
tool_name=tool_name,
|
| 554 |
result=CallToolResult(
|
|
@@ -569,22 +550,12 @@ class MCPEnvironment(Environment):
|
|
| 569 |
),
|
| 570 |
)
|
| 571 |
|
| 572 |
-
# Not a mode-specific tool, use FastMCP
|
| 573 |
try:
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
asyncio.wait_for(
|
| 578 |
-
self._async_call_tool(action.tool_name, action.arguments),
|
| 579 |
-
timeout=timeout,
|
| 580 |
-
)
|
| 581 |
-
)
|
| 582 |
-
|
| 583 |
-
return CallToolObservation(
|
| 584 |
-
tool_name=action.tool_name,
|
| 585 |
-
result=result,
|
| 586 |
)
|
| 587 |
-
|
| 588 |
except asyncio.TimeoutError:
|
| 589 |
return CallToolObservation(
|
| 590 |
tool_name=action.tool_name,
|
|
@@ -594,11 +565,8 @@ class MCPEnvironment(Environment):
|
|
| 594 |
message=f"Tool '{action.tool_name}' timed out after {timeout} seconds",
|
| 595 |
),
|
| 596 |
)
|
| 597 |
-
|
| 598 |
except Exception as e:
|
| 599 |
error_message = str(e)
|
| 600 |
-
|
| 601 |
-
# Determine error type based on the exception
|
| 602 |
if (
|
| 603 |
"not found" in error_message.lower()
|
| 604 |
or "unknown tool" in error_message.lower()
|
|
@@ -611,29 +579,34 @@ class MCPEnvironment(Environment):
|
|
| 611 |
error_type = ToolErrorType.INVALID_ARGS
|
| 612 |
else:
|
| 613 |
error_type = ToolErrorType.EXECUTION_ERROR
|
| 614 |
-
|
| 615 |
return CallToolObservation(
|
| 616 |
tool_name=action.tool_name,
|
| 617 |
result=None,
|
| 618 |
-
error=ToolError(
|
| 619 |
-
error_type=error_type,
|
| 620 |
-
message=error_message,
|
| 621 |
-
),
|
| 622 |
)
|
| 623 |
|
| 624 |
-
async def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
"""
|
| 626 |
-
Async
|
| 627 |
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
Returns:
|
| 633 |
-
The result from the tool execution.
|
| 634 |
"""
|
| 635 |
-
|
| 636 |
-
return await
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
|
| 638 |
@abstractmethod
|
| 639 |
def _step_impl(
|
|
|
|
| 420 |
return self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 421 |
|
| 422 |
def _handle_list_tools(self) -> ListToolsObservation:
|
| 423 |
+
"""Sync wrapper — delegates to the canonical async implementation."""
|
| 424 |
+
return run_async_safely(self._async_handle_list_tools())
|
| 425 |
+
|
| 426 |
+
async def _async_list_tools(self) -> list:
|
| 427 |
"""
|
| 428 |
+
Async helper to list tools from the MCP client.
|
| 429 |
|
| 430 |
Returns:
|
| 431 |
+
List of tool objects from the MCP server.
|
|
|
|
| 432 |
"""
|
| 433 |
+
async with self.mcp_session() as client:
|
| 434 |
+
return await client.list_tools()
|
|
|
|
| 435 |
|
| 436 |
+
def _handle_call_tool(
|
| 437 |
+
self,
|
| 438 |
+
action: CallToolAction,
|
| 439 |
+
timeout_s: Optional[float] = None,
|
| 440 |
+
) -> CallToolObservation:
|
| 441 |
+
"""Sync wrapper — delegates to the canonical async implementation."""
|
| 442 |
+
return run_async_safely(
|
| 443 |
+
self._async_handle_call_tool(action, timeout_s=timeout_s)
|
| 444 |
+
)
|
| 445 |
|
| 446 |
+
async def _async_call_tool(self, tool_name: str, arguments: dict) -> Any:
|
| 447 |
+
"""
|
| 448 |
+
Async helper to call a tool on the MCP server.
|
| 449 |
+
|
| 450 |
+
Args:
|
| 451 |
+
tool_name: Name of the tool to invoke.
|
| 452 |
+
arguments: Dictionary of arguments to pass to the tool.
|
| 453 |
|
| 454 |
+
Returns:
|
| 455 |
+
The result from the tool execution.
|
| 456 |
+
"""
|
| 457 |
+
async with self.mcp_session() as client:
|
| 458 |
+
return await client.call_tool(tool_name, arguments)
|
| 459 |
+
|
| 460 |
+
async def _async_handle_list_tools(self) -> ListToolsObservation:
|
| 461 |
+
"""Async version of _handle_list_tools — avoids run_async_safely."""
|
| 462 |
+
try:
|
| 463 |
+
current_mode = getattr(self, "_mode", None)
|
| 464 |
+
tools_result = await self._async_list_tools()
|
| 465 |
+
tools = []
|
| 466 |
for tool in tools_result:
|
| 467 |
if tool.name not in self._mode_tool_schemas:
|
| 468 |
tools.append(
|
|
|
|
| 474 |
else {},
|
| 475 |
)
|
| 476 |
)
|
|
|
|
|
|
|
| 477 |
for tool_name, mode_schemas in self._mode_tool_schemas.items():
|
| 478 |
if None in mode_schemas:
|
|
|
|
| 479 |
schema = mode_schemas[None]
|
| 480 |
tools.append(
|
| 481 |
Tool(
|
|
|
|
| 485 |
)
|
| 486 |
)
|
| 487 |
elif current_mode in mode_schemas:
|
|
|
|
| 488 |
schema = mode_schemas[current_mode]
|
| 489 |
tools.append(
|
| 490 |
Tool(
|
|
|
|
| 493 |
input_schema=schema["input_schema"],
|
| 494 |
)
|
| 495 |
)
|
|
|
|
| 496 |
return ListToolsObservation(tools=tools)
|
|
|
|
| 497 |
except Exception as e:
|
|
|
|
| 498 |
return ListToolsObservation(
|
| 499 |
tools=[],
|
| 500 |
+
metadata={"error": str(e), "error_type": "list_tools_failed"},
|
|
|
|
|
|
|
|
|
|
| 501 |
)
|
| 502 |
|
| 503 |
+
async def _async_handle_call_tool(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
self,
|
| 505 |
action: CallToolAction,
|
| 506 |
timeout_s: Optional[float] = None,
|
| 507 |
) -> CallToolObservation:
|
| 508 |
+
"""Async version of _handle_call_tool — avoids run_async_safely."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
timeout = timeout_s if timeout_s is not None else MCP_TOOL_CALL_TIMEOUT
|
|
|
|
|
|
|
| 510 |
tool_name = action.tool_name
|
| 511 |
current_mode = getattr(self, "_mode", None)
|
| 512 |
|
| 513 |
if tool_name in self._mode_tools:
|
| 514 |
mode_info = self._mode_tools[tool_name]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
if None in mode_info:
|
|
|
|
| 516 |
func = mode_info[None]
|
| 517 |
elif current_mode in mode_info:
|
|
|
|
| 518 |
func = mode_info[current_mode]
|
| 519 |
else:
|
|
|
|
| 520 |
return CallToolObservation(
|
| 521 |
tool_name=tool_name,
|
| 522 |
result=None,
|
|
|
|
| 525 |
message=f"Tool '{tool_name}' not available in {current_mode} mode",
|
| 526 |
),
|
| 527 |
)
|
|
|
|
|
|
|
| 528 |
try:
|
|
|
|
| 529 |
if inspect.iscoroutinefunction(func):
|
| 530 |
+
result = await func(**action.arguments)
|
| 531 |
else:
|
| 532 |
result = func(**action.arguments)
|
|
|
|
|
|
|
| 533 |
return CallToolObservation(
|
| 534 |
tool_name=tool_name,
|
| 535 |
result=CallToolResult(
|
|
|
|
| 550 |
),
|
| 551 |
)
|
| 552 |
|
|
|
|
| 553 |
try:
|
| 554 |
+
result = await asyncio.wait_for(
|
| 555 |
+
self._async_call_tool(action.tool_name, action.arguments),
|
| 556 |
+
timeout=timeout,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
)
|
| 558 |
+
return CallToolObservation(tool_name=action.tool_name, result=result)
|
| 559 |
except asyncio.TimeoutError:
|
| 560 |
return CallToolObservation(
|
| 561 |
tool_name=action.tool_name,
|
|
|
|
| 565 |
message=f"Tool '{action.tool_name}' timed out after {timeout} seconds",
|
| 566 |
),
|
| 567 |
)
|
|
|
|
| 568 |
except Exception as e:
|
| 569 |
error_message = str(e)
|
|
|
|
|
|
|
| 570 |
if (
|
| 571 |
"not found" in error_message.lower()
|
| 572 |
or "unknown tool" in error_message.lower()
|
|
|
|
| 579 |
error_type = ToolErrorType.INVALID_ARGS
|
| 580 |
else:
|
| 581 |
error_type = ToolErrorType.EXECUTION_ERROR
|
|
|
|
| 582 |
return CallToolObservation(
|
| 583 |
tool_name=action.tool_name,
|
| 584 |
result=None,
|
| 585 |
+
error=ToolError(error_type=error_type, message=error_message),
|
|
|
|
|
|
|
|
|
|
| 586 |
)
|
| 587 |
|
| 588 |
+
async def step_async(
|
| 589 |
+
self,
|
| 590 |
+
action: Action,
|
| 591 |
+
timeout_s: Optional[float] = None,
|
| 592 |
+
**kwargs: Any,
|
| 593 |
+
) -> Observation:
|
| 594 |
"""
|
| 595 |
+
Async step that routes MCP actions without going through run_async_safely.
|
| 596 |
|
| 597 |
+
The WebSocket handler calls this directly on the outer event loop, where
|
| 598 |
+
the MCP session is already open, avoiding the thread/event-loop deadlock
|
| 599 |
+
that occurs when the sync step() path is used via run_in_executor.
|
|
|
|
|
|
|
|
|
|
| 600 |
"""
|
| 601 |
+
if isinstance(action, ListToolsAction):
|
| 602 |
+
return await self._async_handle_list_tools()
|
| 603 |
+
elif isinstance(action, CallToolAction):
|
| 604 |
+
return await self._async_handle_call_tool(action, timeout_s=timeout_s)
|
| 605 |
+
else:
|
| 606 |
+
loop = asyncio.get_event_loop()
|
| 607 |
+
return await loop.run_in_executor(
|
| 608 |
+
None, lambda: self._step_impl(action, timeout_s=timeout_s, **kwargs)
|
| 609 |
+
)
|
| 610 |
|
| 611 |
@abstractmethod
|
| 612 |
def _step_impl(
|
src/openenv/core/env_server/web_interface.py
CHANGED
|
@@ -22,7 +22,7 @@ from datetime import datetime
|
|
| 22 |
from typing import Any, Callable, Dict, List, Optional, Type
|
| 23 |
|
| 24 |
import gradio as gr
|
| 25 |
-
from fastapi import Body, FastAPI, HTTPException,
|
| 26 |
from fastapi.responses import RedirectResponse
|
| 27 |
from pydantic import BaseModel, ConfigDict, Field
|
| 28 |
|
|
@@ -356,7 +356,9 @@ class WebInterfaceManager:
|
|
| 356 |
else:
|
| 357 |
# Run sync reset in thread pool to avoid blocking event loop
|
| 358 |
# and to support environments using sync libraries (e.g., Playwright)
|
| 359 |
-
observation = await self._run_sync_in_thread_pool(
|
|
|
|
|
|
|
| 360 |
state: State = self.env.state
|
| 361 |
|
| 362 |
# Serialize observation once using shared utility
|
|
|
|
| 22 |
from typing import Any, Callable, Dict, List, Optional, Type
|
| 23 |
|
| 24 |
import gradio as gr
|
| 25 |
+
from fastapi import Body, FastAPI, HTTPException, status, WebSocket, WebSocketDisconnect
|
| 26 |
from fastapi.responses import RedirectResponse
|
| 27 |
from pydantic import BaseModel, ConfigDict, Field
|
| 28 |
|
|
|
|
| 356 |
else:
|
| 357 |
# Run sync reset in thread pool to avoid blocking event loop
|
| 358 |
# and to support environments using sync libraries (e.g., Playwright)
|
| 359 |
+
observation = await self._run_sync_in_thread_pool(
|
| 360 |
+
self.env.reset, **valid_kwargs
|
| 361 |
+
)
|
| 362 |
state: State = self.env.state
|
| 363 |
|
| 364 |
# Serialize observation once using shared utility
|
src/openenv_core.egg-info/PKG-INFO
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
Metadata-Version: 2.4
|
| 2 |
Name: openenv-core
|
| 3 |
-
Version: 0.2.3
|
| 4 |
Summary: A unified framework for reinforcement learning environments
|
| 5 |
Requires-Python: >=3.10
|
| 6 |
Description-Content-Type: text/markdown
|
|
|
|
| 1 |
Metadata-Version: 2.4
|
| 2 |
Name: openenv-core
|
| 3 |
+
Version: 0.2.3
|
| 4 |
Summary: A unified framework for reinforcement learning environments
|
| 5 |
Requires-Python: >=3.10
|
| 6 |
Description-Content-Type: text/markdown
|
src/openenv_core.egg-info/SOURCES.txt
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
LICENSE
|
|
|
|
| 2 |
README.md
|
| 3 |
pyproject.toml
|
| 4 |
src/openenv/__init__.py
|
|
@@ -19,8 +20,6 @@ src/openenv/cli/commands/serve.py
|
|
| 19 |
src/openenv/cli/commands/skills.py
|
| 20 |
src/openenv/cli/commands/validate.py
|
| 21 |
src/openenv/cli/templates/__init__.py
|
| 22 |
-
src/openenv/cli/templates/__pycache__/__init__.cpython-311.pyc
|
| 23 |
-
src/openenv/cli/templates/__pycache__/__init__.cpython-313.pyc
|
| 24 |
src/openenv/cli/templates/openenv_env/README.md
|
| 25 |
src/openenv/cli/templates/openenv_env/__init__.py
|
| 26 |
src/openenv/cli/templates/openenv_env/client.py
|
|
|
|
| 1 |
LICENSE
|
| 2 |
+
MANIFEST.in
|
| 3 |
README.md
|
| 4 |
pyproject.toml
|
| 5 |
src/openenv/__init__.py
|
|
|
|
| 20 |
src/openenv/cli/commands/skills.py
|
| 21 |
src/openenv/cli/commands/validate.py
|
| 22 |
src/openenv/cli/templates/__init__.py
|
|
|
|
|
|
|
| 23 |
src/openenv/cli/templates/openenv_env/README.md
|
| 24 |
src/openenv/cli/templates/openenv_env/__init__.py
|
| 25 |
src/openenv/cli/templates/openenv_env/client.py
|