Skip to content
Open
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
87 changes: 87 additions & 0 deletions tests/test_agent.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for the Agent resource."""

import json
import pytest
from datetime import datetime
from pydantic import BaseModel
Expand All @@ -12,6 +13,7 @@
CreditUsage,
)
from vlmrun.client.agent import Agent
from vlmrun.types import MessageContent


class SampleInputModel(BaseModel):
Expand Down Expand Up @@ -296,3 +298,88 @@ class MockClient:
agent = Agent(MockClient())
result = agent._process_inputs(None)
assert result is None

def test_process_inputs_dict_with_nested_basemodel(self):
"""Test that dict inputs with nested BaseModel values are JSON-serializable."""

class MockClient:
api_key = "test-key"
base_url = "https://api.vlm.run/v1"
timeout = 120.0
max_retries = 1

Comment on lines +305 to +310

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve maintainability and reduce code duplication, consider defining the MockClient class once at the TestAgentProcessInputs class level or as a pytest fixture. It is repeated in every test method within this test class (e.g., test_process_inputs_with_dict, test_process_inputs_with_basemodel, etc.), including the newly added tests.

agent = Agent(MockClient())
inputs = {
"file": MessageContent(
type="input_file", file_id="d9f74779-5e5f-4bec-a901-97c55598c56e"
),
}
result = agent._process_inputs(inputs)

assert isinstance(result, dict)
assert isinstance(result["file"], dict)
assert result["file"]["type"] == "input_file"
assert result["file"]["file_id"] == "d9f74779-5e5f-4bec-a901-97c55598c56e"
json.dumps(result)

def test_process_inputs_dict_with_list_of_basemodels(self):
"""Test that dict inputs with lists of BaseModel values are serialized."""

class MockClient:
api_key = "test-key"
base_url = "https://api.vlm.run/v1"
timeout = 120.0
max_retries = 1

agent = Agent(MockClient())
inputs = {
"files": [
MessageContent(type="input_file", file_id="aaa"),
MessageContent(type="input_file", file_id="bbb"),
],
}
result = agent._process_inputs(inputs)

assert isinstance(result["files"], list)
assert all(isinstance(item, dict) for item in result["files"])
assert result["files"][0]["file_id"] == "aaa"
assert result["files"][1]["file_id"] == "bbb"
json.dumps(result)

def test_process_inputs_dict_plain_values_unchanged(self):
"""Test that dict inputs with plain string/int values pass through unchanged."""

class MockClient:
api_key = "test-key"
base_url = "https://api.vlm.run/v1"
timeout = 120.0
max_retries = 1

agent = Agent(MockClient())
inputs = {"url": "https://example.com/image.jpg", "count": 3, "flag": True}
result = agent._process_inputs(inputs)

assert result == {"url": "https://example.com/image.jpg", "count": 3, "flag": True}
json.dumps(result)

def test_process_inputs_dict_with_nested_dict_containing_basemodel(self):
"""Test that deeply nested BaseModel values inside dicts are serialized."""

class MockClient:
api_key = "test-key"
base_url = "https://api.vlm.run/v1"
timeout = 120.0
max_retries = 1

agent = Agent(MockClient())
inputs = {
"metadata": {
"content": MessageContent(type="text", text="hello world"),
},
}
result = agent._process_inputs(inputs)

assert isinstance(result["metadata"]["content"], dict)
assert result["metadata"]["content"]["type"] == "text"
assert result["metadata"]["content"]["text"] == "hello world"
json.dumps(result)
12 changes: 12 additions & 0 deletions vlmrun/client/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ def __init__(self, client: "VLMRunProtocol") -> None:
self._client = client
self._requestor = APIRequestor(client)

@staticmethod
def _serialize_value(value: Any) -> Any:
"""Recursively serialize a value, converting BaseModel instances to dicts."""
if isinstance(value, BaseModel):
return value.model_dump(exclude_none=True)
elif isinstance(value, dict):
return {k: Agent._serialize_value(v) for k, v in value.items()}
elif isinstance(value, list):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation only handles lists. It would be more robust to also handle other sequence types like tuples, which might also contain BaseModel instances. If a tuple of BaseModels is passed, json.dumps will fail.

Suggested change
elif isinstance(value, list):
elif isinstance(value, (list, tuple)):

return [Agent._serialize_value(item) for item in value]
return value

def _process_inputs(
self, inputs: Union[dict[str, Any], BaseModel, None]
) -> Optional[dict[str, Any]]:
Expand All @@ -53,6 +64,7 @@ def _process_inputs(
DeprecationWarning,
stacklevel=3,
)
return {k: self._serialize_value(v) for k, v in inputs.items()}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with how _serialize_value is called within itself recursively, it would be clearer to call it as a static method using Agent._serialize_value(...) here as well, instead of self._serialize_value(...). This makes it more explicit that it's a static method and doesn't depend on the instance state.

Suggested change
return {k: self._serialize_value(v) for k, v in inputs.items()}
return {k: Agent._serialize_value(v) for k, v in inputs.items()}

return inputs

def get(
Expand Down
2 changes: 1 addition & 1 deletion vlmrun/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.5.6"
__version__ = "0.5.7"
Loading