22
33import asyncio
44import socket
5+ from typing import Annotated , Any
56
67import httpx
78import yaml
89from fastapi import FastAPI
9- from pydantic import BaseModel , Field , PrivateAttr
10+ from fastapi .responses import JSONResponse
11+ from pydantic import BaseModel , Field , PrivateAttr , TypeAdapter
1012from typing_extensions import Self
1113from uvicorn import Config , Server
1214from uvicorn .config import LOGGING_CONFIG
2224 EvaluationResponse ,
2325 GroupedRolloutRequest ,
2426 GroupRolloutParams ,
25- RolloutGenerator ,
27+ Rollout ,
2628 RolloutRequest ,
2729 TokenRollout ,
2830)
2931from ...agent .registry import get_agent_class
32+ from ...agent .reward_only_agent import RewardOnlyAgent
3033from ...inference import InferenceInterface
3134from ...inference .chat_interface import MegatronChatInterface
32- from ..api import EnvironmentServer , RemoteEvaluationRequest , RemoteRolloutRequest
35+ from ..api import (
36+ EnvironmentServer ,
37+ RemoteEpisodeRequest ,
38+ RemoteEvaluationRequest ,
39+ RemotePrompt ,
40+ RemotePromptRequest ,
41+ RemoteRolloutRequest ,
42+ )
43+
44+ # A finished rollout on the wire; an empty placeholder validates as the first member.
45+ _ROLLOUT = TypeAdapter (Annotated [TokenRollout | Rollout , Field (union_mode = "left_to_right" )])
3346
3447
3548def _bind (wire : BaseModel , request_cls : type , interface : InferenceInterface ):
@@ -45,30 +58,49 @@ class FastAPIEnvServer(EnvironmentServer):
4558
4659 @classmethod
4760 def build_app (cls , env_cls : type [Agent ], cls_args : dict , policy : InferenceInterface ) -> FastAPI :
48- """Env server app; every route generates through `policy`."""
61+ """Env server app: one environment instance generating through `policy`."""
4962 app = FastAPI ()
50-
51- if issubclass (env_cls , ContrastiveRolloutGenerator ):
63+ env = env_cls (** cls_args )
64+
65+ @app .exception_handler (Exception )
66+ async def env_error (_ , exc : Exception ) -> JSONResponse :
67+ # The trainer sees the environment's failure the way a local agent would raise it.
68+ return JSONResponse (status_code = 500 , content = {"detail" : repr (exc )})
69+
70+ if isinstance (env , RewardOnlyAgent ):
71+
72+ @app .post ("/prompts/" )
73+ async def sample_prompt (request : RemotePromptRequest ) -> RemotePrompt :
74+ prompt , golden = await env .get_prompt (validation = request .validation )
75+ return RemotePrompt (prompt = prompt , golden = golden )
76+
77+ @app .post ("/episodes/" )
78+ async def run_episode (request : RemoteEpisodeRequest ) -> dict [str , Any ]:
79+ group_request = GroupedRolloutRequest (
80+ num_groups = 1 ,
81+ rollouts_per_group = 1 ,
82+ inference_interface = policy ,
83+ generation_args = request .generation_args ,
84+ validation = request .validation ,
85+ )
86+ params = env .group_rollout_params (
87+ group_request , prompt = request .prompt , golden = request .golden
88+ )
89+ rollout = await params .build_rollout (await params .run_episode ())
90+ return rollout .model_dump ()
91+
92+ if isinstance (env , ContrastiveRolloutGenerator ):
5293
5394 @app .post ("/contrastive_rollouts/" )
5495 async def contrastive_rollouts (
5596 request : RemoteRolloutRequest ,
5697 ) -> list [ContrastiveRollout ]:
57- env = env_cls (** cls_args )
5898 return await env .get_contrastive_rollouts (_bind (request , RolloutRequest , policy ))
5999
60- if issubclass (env_cls , RolloutGenerator ):
61-
62- @app .post ("/rollouts/" )
63- async def rollouts (request : RemoteRolloutRequest ) -> list [TokenRollout ]:
64- env = env_cls (** cls_args )
65- return await env .get_reward_rollouts (_bind (request , RolloutRequest , policy ))
66-
67- if issubclass (env_cls , EvaluationAgent ):
100+ if isinstance (env , EvaluationAgent ):
68101
69102 @app .post ("/evaluation/" )
70103 async def run_evaluation (request : RemoteEvaluationRequest ):
71- env = env_cls (** cls_args )
72104 return await env .run_evaluation (_bind (request , EvaluationRequest , policy ))
73105
74106 return app
@@ -115,23 +147,39 @@ async def get_contrastive_rollouts(self, request: RolloutRequest) -> list[Contra
115147 result = await self ._post ("/contrastive_rollouts/" , wire )
116148 return [ContrastiveRollout .model_validate (r ) for r in result ]
117149
118- async def prepare_group_rollout (self , request : GroupedRolloutRequest ) -> GroupRolloutParams :
119- raise NotImplementedError (
120- "FastAPIEnvServer overrides get_reward_rollouts; prepare_group_rollout is not used."
150+ async def prepare_group_rollout (
151+ self , request : GroupedRolloutRequest | RolloutRequest
152+ ) -> GroupRolloutParams :
153+ sampled = RemotePrompt .model_validate (
154+ await self ._post ("/prompts/" , RemotePromptRequest (validation = request .validation ))
155+ )
156+ episode_request = RemoteEpisodeRequest (
157+ prompt = sampled .prompt ,
158+ golden = sampled .golden ,
159+ generation_args = request .generation_args ,
160+ validation = request .validation ,
121161 )
122162
163+ async def run_episode () -> Rollout | TokenRollout :
164+ # The env server scores the episode itself, so the episode is already the rollout.
165+ return _ROLLOUT .validate_python (await self ._post ("/episodes/" , episode_request ))
166+
167+ async def build_rollout (rollout : Rollout | TokenRollout ) -> Rollout | TokenRollout :
168+ return rollout
169+
170+ return GroupRolloutParams (run_episode = run_episode , build_rollout = build_rollout )
171+
123172 async def get_rollout_response (self , request , inference_request ):
124173 raise NotImplementedError (
125- "FastAPIEnvServer overrides get_reward_rollouts/run_evaluation; "
126- "get_rollout_response is not used."
174+ "Episodes run on the env server; FastAPIEnvServer never generates itself."
127175 )
128176
129- async def get_reward_rollouts (self , request : RolloutRequest ) -> list [TokenRollout ]:
130- wire = RemoteRolloutRequest . model_validate (
131- request . model_dump ( exclude = { "inference_interface" } )
132- )
133- result = await self . _post ( "/rollouts/" , wire )
134- return [ TokenRollout . model_validate ( r ) for r in result ]
177+ async def get_reward_rollouts (self , request : RolloutRequest ) -> list [Rollout | TokenRollout ]:
178+ async def single_rollout () -> Rollout | TokenRollout :
179+ params = await self . prepare_group_rollout ( request )
180+ return await params . build_rollout ( await params . run_episode () )
181+
182+ return list ( await asyncio . gather ( * [ single_rollout ( ) for _ in range ( request . num_rollouts )]))
135183
136184 async def run_evaluation (self , request : EvaluationRequest ) -> EvaluationResponse :
137185 wire = RemoteEvaluationRequest .model_validate (
0 commit comments