Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion resources_servers/workbench/configs/workbench.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ workbench_simple_agent:
- name: example
type: example
jsonl_fpath: resources_servers/workbench/data/example.jsonl
max_turns: 6
max_steps: 6
16 changes: 10 additions & 6 deletions responses_api_agents/simple_agent/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@
NeMoGymResponse,
NeMoGymResponseCreateParamsNonStreaming,
NeMoGymResponseFunctionToolCall,
NeMoGymResponseOutputMessage,
)


class SimpleAgentConfig(BaseResponsesAPIAgentConfig):
resources_server: ResourcesServerRef
model_server: ModelServerRef
max_turns: int = None
max_steps: int = None


class SimpleAgentRunRequest(BaseRunRequest):
Expand All @@ -64,10 +65,10 @@ async def responses(self, body: NeMoGymResponseCreateParamsNonStreaming = Body()
body.input = [NeMoGymEasyInputMessage(role="user", content=body.input)]

new_outputs = []
turn = 0
step = 0

while True:
turn += 1
step += 1
new_body = body.model_copy(update={"input": body.input + new_outputs})

model_response = await self.server_client.post(
Expand All @@ -87,7 +88,10 @@ async def responses(self, body: NeMoGymResponseCreateParamsNonStreaming = Body()
new_outputs.extend(output)

all_fn_calls: List[NeMoGymResponseFunctionToolCall] = [o for o in output if o.type == "function_call"]
if not all_fn_calls:
all_output_messages: List[NeMoGymResponseOutputMessage] = [
o for o in output if o.type == "message" and o.role == "assistant"
]
if not all_fn_calls and all_output_messages:
break

for output_function_call in all_fn_calls:
Expand All @@ -104,8 +108,8 @@ async def responses(self, body: NeMoGymResponseCreateParamsNonStreaming = Body()
)
new_outputs.append(tool_response)

# Check if max turns is not None and if we have exhausted it.
if self.config.max_turns and turn >= self.config.max_turns:
# Check if max steps is not None and if we have exhausted it.
if self.config.max_steps and step >= self.config.max_steps:
break

model_response.output = new_outputs
Expand Down
166 changes: 165 additions & 1 deletion responses_api_agents/simple_agent/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@
# 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.
from unittest.mock import MagicMock
from unittest.mock import MagicMock, call

from fastapi.testclient import TestClient
from pytest import MonkeyPatch

from nemo_gym.openai_utils import (
NeMoGymEasyInputMessage,
NeMoGymResponseCreateParamsNonStreaming,
NeMoGymResponseReasoningItem,
NeMoGymSummary,
)
from nemo_gym.server_utils import ServerClient
from responses_api_agents.simple_agent.app import (
Expand Down Expand Up @@ -150,3 +152,165 @@ async def test_responses(self, monkeypatch: MonkeyPatch) -> None:
"user": None,
}
assert expected_responses_dict == actual_responses_dict

async def test_responses_continues_on_reasoning_only(self, monkeypatch: MonkeyPatch) -> None:
config = SimpleAgentConfig(
host="0.0.0.0",
port=8080,
entrypoint="",
model_server=ModelServerRef(
type="responses_api_models",
name="my server name",
),
resources_server=ResourcesServerRef(
type="resources_servers",
name="",
),
)
server = SimpleAgent(config=config, server_client=MagicMock(spec=ServerClient))
app = server.setup_webserver()
client = TestClient(app)

mock_response_reasoning_data = {
"id": "resp_688babb004988199b26c5250ba69c1e80abdf302bcd600d3",
"created_at": 1753983920.0,
"model": "dummy_model",
"object": "response",
"output": [
{
"id": "msg_688babb17a7881998cc7a42d53c8e5790abdf302bcd600d3",
"summary": [
{
"text": "I'm thinking how to respond",
"type": "summary_text",
}
],
"status": "completed",
"type": "reasoning",
}
],
"parallel_tool_calls": True,
"tool_choice": "auto",
"tools": [],
}

mock_response_chat_data = {
"id": "resp_688babb004988199b26c5250ba69c1e80abdf302bcd600d3",
"created_at": 1753983920.0,
"model": "dummy_model",
"object": "response",
"output": [
{
"id": "msg_688babb17a7881998cc7a42d53c8e5790abdf302bcd600d3",
"content": [
{
"annotations": [],
"text": "Hello! How can I help you today?",
"type": "output_text",
}
],
"role": "assistant",
"status": "completed",
"type": "message",
}
],
"parallel_tool_calls": True,
"tool_choice": "auto",
"tools": [],
}

dotjson_mock = MagicMock()
dotjson_mock.json.side_effect = [mock_response_reasoning_data, mock_response_chat_data]
server.server_client.post.return_value = dotjson_mock

# No model provided should use the one from the config
res_no_model = client.post("/v1/responses", json={"input": [{"role": "user", "content": "hello"}]})
assert res_no_model.status_code == 200

expected_calls = [
call(
server_name="my server name",
url_path="/v1/responses",
json=NeMoGymResponseCreateParamsNonStreaming(
input=[NeMoGymEasyInputMessage(content="hello", role="user", type="message")]
),
),
call().json(),
call(
server_name="my server name",
url_path="/v1/responses",
json=NeMoGymResponseCreateParamsNonStreaming(
input=[
NeMoGymEasyInputMessage(content="hello", role="user", type="message"),
NeMoGymResponseReasoningItem(
id="msg_688babb17a7881998cc7a42d53c8e5790abdf302bcd600d3",
summary=[NeMoGymSummary(text="I'm thinking how to respond", type="summary_text")],
type="reasoning",
encrypted_content=None,
status="completed",
),
]
),
),
call().json(),
]
server.server_client.post.assert_has_calls(expected_calls)

actual_responses_dict = res_no_model.json()
expected_responses_dict = {
"id": "resp_688babb004988199b26c5250ba69c1e80abdf302bcd600d3",
"created_at": 1753983920.0,
"error": None,
"incomplete_details": None,
"instructions": None,
"metadata": None,
"model": "dummy_model",
"object": "response",
"output": [
{
"id": "msg_688babb17a7881998cc7a42d53c8e5790abdf302bcd600d3",
"encrypted_content": None,
"summary": [
{
"text": "I'm thinking how to respond",
"type": "summary_text",
}
],
"status": "completed",
"type": "reasoning",
},
{
"id": "msg_688babb17a7881998cc7a42d53c8e5790abdf302bcd600d3",
"content": [
{
"annotations": [],
"text": "Hello! How can I help you today?",
"type": "output_text",
"logprobs": None,
}
],
"role": "assistant",
"status": "completed",
"type": "message",
},
],
"parallel_tool_calls": True,
"temperature": None,
"tool_choice": "auto",
"tools": [],
"top_p": None,
"background": None,
"max_output_tokens": None,
"max_tool_calls": None,
"previous_response_id": None,
"prompt": None,
"reasoning": None,
"service_tier": None,
"status": None,
"text": None,
"top_logprobs": None,
"truncation": None,
"usage": None,
"user": None,
}
assert expected_responses_dict == actual_responses_dict
7 changes: 6 additions & 1 deletion responses_api_agents/simple_agent_stateful/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
NeMoGymResponse,
NeMoGymResponseCreateParamsNonStreaming,
NeMoGymResponseFunctionToolCall,
NeMoGymResponseOutputMessage,
)


Expand All @@ -55,6 +56,7 @@ class SimpleAgentStatefulVerifyResponse(BaseVerifyResponse):
model_config = ConfigDict(extra="allow")


# TODO we should find a way to merge this with the regular simple agent.
class SimpleAgentStateful(SimpleResponsesAPIAgent):
config: SimpleAgentStatefulConfig

Expand Down Expand Up @@ -87,7 +89,10 @@ async def responses(self, body: NeMoGymResponseCreateParamsNonStreaming = Body()
new_outputs.extend(output)

all_fn_calls: List[NeMoGymResponseFunctionToolCall] = [o for o in output if o.type == "function_call"]
if not all_fn_calls:
all_output_messages: List[NeMoGymResponseOutputMessage] = [
o for o in output if o.type == "message" and o.role == "assistant"
]
if not all_fn_calls and all_output_messages:
break

for output_function_call in all_fn_calls:
Expand Down