NeerajCodz Copilot commited on
Commit
b4b210e
·
1 Parent(s): 24f0bf0

fix: add OpenEnv root reset and step aliases

Browse files

Expose OpenEnv-compatible root endpoints (/reset, /step, /state/{episode_id}) and API aliases (/api/reset, /api/step, /api/state/{episode_id}) to prevent 405 on evaluator probes.

Add endpoint coercion for relaxed action payloads and tests for alias behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

backend/app/api/routes/__init__.py CHANGED
@@ -1,5 +1,5 @@
1
  """API routes package."""
2
 
3
- from app.api.routes import agents, episode, health, memory, sites, tasks, tools
4
 
5
- __all__ = ["agents", "episode", "health", "memory", "sites", "tasks", "tools"]
 
1
  """API routes package."""
2
 
3
+ from app.api.routes import agents, episode, health, memory, openenv, sites, tasks, tools
4
 
5
+ __all__ = ["agents", "episode", "health", "memory", "openenv", "sites", "tasks", "tools"]
backend/app/api/routes/openenv.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenEnv compatibility endpoints exposed at root-level paths."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import APIRouter, Body, HTTPException, status
8
+ from pydantic import BaseModel, Field
9
+
10
+ from app.api.deps import SettingsDep
11
+ from app.api.routes.episode import (
12
+ EpisodeState,
13
+ ResetRequest,
14
+ ResetResponse,
15
+ StepRequest,
16
+ get_episode_state,
17
+ reset_episode,
18
+ step_episode,
19
+ )
20
+ from app.core.action import Action, ActionType
21
+
22
+ router = APIRouter(tags=["OpenEnv"])
23
+
24
+
25
+ class OpenEnvResetRequest(BaseModel):
26
+ """Lenient reset request supporting common OpenEnv field aliases."""
27
+
28
+ task_id: str | None = Field(default=None)
29
+ task: str | None = Field(default=None)
30
+ task_name: str | None = Field(default=None)
31
+ seed: int | None = Field(default=None)
32
+ config: dict[str, Any] | None = Field(default=None)
33
+
34
+
35
+ class OpenEnvStepRequest(BaseModel):
36
+ """Lenient step request supporting common OpenEnv field aliases."""
37
+
38
+ episode_id: str | None = Field(default=None)
39
+ episode: str | None = Field(default=None)
40
+ session_id: str | None = Field(default=None)
41
+ action: Any = Field(default_factory=dict)
42
+
43
+
44
+ def _coerce_action(action_payload: Any) -> Action:
45
+ """Coerce OpenEnv-style actions into internal Action model."""
46
+ if isinstance(action_payload, Action):
47
+ return action_payload
48
+
49
+ if isinstance(action_payload, str):
50
+ action_type = action_payload.strip().lower()
51
+ try:
52
+ return Action(action_type=ActionType(action_type), parameters={})
53
+ except ValueError:
54
+ return Action.wait()
55
+
56
+ if isinstance(action_payload, dict):
57
+ payload = dict(action_payload)
58
+
59
+ if "action_type" not in payload:
60
+ for alias in ("action", "type", "name"):
61
+ alias_value = payload.get(alias)
62
+ if isinstance(alias_value, str) and alias_value.strip():
63
+ payload["action_type"] = alias_value.strip().lower()
64
+ break
65
+
66
+ if "parameters" not in payload:
67
+ params = payload.get("params")
68
+ payload["parameters"] = params if isinstance(params, dict) else {}
69
+
70
+ if "reasoning" not in payload and isinstance(payload.get("thought"), str):
71
+ payload["reasoning"] = payload["thought"]
72
+
73
+ action_type = payload.get("action_type")
74
+ if not isinstance(action_type, str):
75
+ payload["action_type"] = ActionType.WAIT.value
76
+ payload["parameters"] = {}
77
+ else:
78
+ normalized = action_type.strip().lower()
79
+ try:
80
+ ActionType(normalized)
81
+ payload["action_type"] = normalized
82
+ except ValueError:
83
+ payload["action_type"] = ActionType.WAIT.value
84
+ payload["parameters"] = {}
85
+
86
+ try:
87
+ return Action.model_validate(payload)
88
+ except Exception:
89
+ return Action.wait()
90
+
91
+ return Action.wait()
92
+
93
+
94
+ @router.post(
95
+ "/reset",
96
+ response_model=ResetResponse,
97
+ status_code=status.HTTP_200_OK,
98
+ summary="OpenEnv-compatible reset endpoint",
99
+ )
100
+ @router.post(
101
+ "/api/reset",
102
+ response_model=ResetResponse,
103
+ status_code=status.HTTP_200_OK,
104
+ include_in_schema=False,
105
+ )
106
+ async def openenv_reset(
107
+ settings: SettingsDep,
108
+ request: OpenEnvResetRequest | None = Body(default=None),
109
+ ) -> ResetResponse:
110
+ """
111
+ Root-level reset alias used by OpenEnv evaluators.
112
+
113
+ Defaults to `task_001` when no explicit task identifier is provided.
114
+ """
115
+ payload = request or OpenEnvResetRequest()
116
+ task_id = payload.task_id or payload.task or payload.task_name or "task_001"
117
+ normalized_request = ResetRequest(task_id=task_id, seed=payload.seed, config=payload.config)
118
+ return await reset_episode(normalized_request, settings)
119
+
120
+
121
+ @router.post(
122
+ "/step",
123
+ status_code=status.HTTP_200_OK,
124
+ summary="OpenEnv-compatible step endpoint",
125
+ )
126
+ @router.post(
127
+ "/api/step",
128
+ status_code=status.HTTP_200_OK,
129
+ include_in_schema=False,
130
+ )
131
+ async def openenv_step(
132
+ request: OpenEnvStepRequest = Body(default_factory=OpenEnvStepRequest),
133
+ ) -> dict[str, Any]:
134
+ """Root-level step alias used by OpenEnv evaluators."""
135
+ episode_id = request.episode_id or request.episode or request.session_id
136
+ if not episode_id:
137
+ raise HTTPException(
138
+ status_code=status.HTTP_400_BAD_REQUEST,
139
+ detail="Missing episode_id",
140
+ )
141
+
142
+ result = await step_episode(
143
+ StepRequest(
144
+ episode_id=episode_id,
145
+ action=_coerce_action(request.action),
146
+ )
147
+ )
148
+ payload = result.model_dump()
149
+ payload["done"] = bool(result.terminated or result.truncated)
150
+ return payload
151
+
152
+
153
+ @router.get(
154
+ "/state/{episode_id}",
155
+ response_model=EpisodeState,
156
+ status_code=status.HTTP_200_OK,
157
+ summary="OpenEnv-compatible state endpoint",
158
+ )
159
+ @router.get(
160
+ "/api/state/{episode_id}",
161
+ response_model=EpisodeState,
162
+ status_code=status.HTTP_200_OK,
163
+ include_in_schema=False,
164
+ )
165
+ async def openenv_state(episode_id: str) -> EpisodeState:
166
+ """Root-level state alias used by OpenEnv evaluators."""
167
+ return await get_episode_state(episode_id)
168
+
backend/app/main.py CHANGED
@@ -11,7 +11,7 @@ from fastapi.middleware.cors import CORSMiddleware
11
  from fastapi.responses import FileResponse, HTMLResponse
12
  from fastapi.staticfiles import StaticFiles
13
 
14
- from app.api.routes import agents, episode, health, memory, plugins, scrape, sites, tasks, tools
15
  from app.api.routes import settings as settings_routes
16
  from app.config import get_settings
17
  from app.memory.manager import MemoryManager
@@ -135,6 +135,7 @@ def create_app() -> FastAPI:
135
  app.include_router(plugins.router, prefix=api_prefix, tags=["Plugins"])
136
  app.include_router(sites.router, prefix=api_prefix, tags=["Sites"])
137
  app.include_router(scrape.router, prefix=api_prefix, tags=["Scraping"])
 
138
 
139
  # Import and include providers router
140
  from app.api.routes import providers
 
11
  from fastapi.responses import FileResponse, HTMLResponse
12
  from fastapi.staticfiles import StaticFiles
13
 
14
+ from app.api.routes import agents, episode, health, memory, openenv, plugins, scrape, sites, tasks, tools
15
  from app.api.routes import settings as settings_routes
16
  from app.config import get_settings
17
  from app.memory.manager import MemoryManager
 
135
  app.include_router(plugins.router, prefix=api_prefix, tags=["Plugins"])
136
  app.include_router(sites.router, prefix=api_prefix, tags=["Sites"])
137
  app.include_router(scrape.router, prefix=api_prefix, tags=["Scraping"])
138
+ app.include_router(openenv.router, tags=["OpenEnv"])
139
 
140
  # Import and include providers router
141
  from app.api.routes import providers
backend/tests/test_api/test_episode.py CHANGED
@@ -46,3 +46,30 @@ def test_get_state(client: TestClient, sample_task: dict) -> None:
46
  assert response.status_code == 200
47
  data = response.json()
48
  assert data["episode_id"] == episode_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  assert response.status_code == 200
47
  data = response.json()
48
  assert data["episode_id"] == episode_id
49
+
50
+
51
+ def test_openenv_reset_alias(client: TestClient, sample_task: dict) -> None:
52
+ """Test OpenEnv-compatible reset alias at root path."""
53
+ response = client.post("/reset", json={"task": sample_task["task_id"]})
54
+ assert response.status_code == 200
55
+ data = response.json()
56
+ assert "episode_id" in data
57
+ assert data["task_id"] == sample_task["task_id"]
58
+
59
+
60
+ def test_openenv_step_alias_with_string_action(client: TestClient, sample_task: dict) -> None:
61
+ """Test OpenEnv-compatible step alias accepts string action payloads."""
62
+ reset_response = client.post("/reset", json={"task_id": sample_task["task_id"]})
63
+ assert reset_response.status_code == 200
64
+ episode_id = reset_response.json()["episode_id"]
65
+
66
+ step_response = client.post(
67
+ "/step",
68
+ json={
69
+ "episode_id": episode_id,
70
+ "action": "done",
71
+ },
72
+ )
73
+ assert step_response.status_code == 200
74
+ data = step_response.json()
75
+ assert "done" in data