This is an automated email from the ASF dual-hosted git repository.

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new bd3c62a0 [python][test] Add unit tests for tool schema conversion 
utilities (#815)
bd3c62a0 is described below

commit bd3c62a0fbe812dedb6fe980083daf90209e12fe
Author: vishnu prakash <[email protected]>
AuthorDate: Tue Jun 9 12:28:52 2026 +0530

    [python][test] Add unit tests for tool schema conversion utilities (#815)
---
 python/flink_agents/api/tools/tests/__init__.py   |  17 ++
 python/flink_agents/api/tools/tests/test_utils.py | 221 ++++++++++++++++++++++
 2 files changed, 238 insertions(+)

diff --git a/python/flink_agents/api/tools/tests/__init__.py 
b/python/flink_agents/api/tools/tests/__init__.py
new file mode 100644
index 00000000..65b48d4d
--- /dev/null
+++ b/python/flink_agents/api/tools/tests/__init__.py
@@ -0,0 +1,17 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  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.
+################################################################################
diff --git a/python/flink_agents/api/tools/tests/test_utils.py 
b/python/flink_agents/api/tools/tests/test_utils.py
new file mode 100644
index 00000000..b1d01f09
--- /dev/null
+++ b/python/flink_agents/api/tools/tests/test_utils.py
@@ -0,0 +1,221 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  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.
+################################################################################
+import json
+from typing import Annotated, Any, List
+
+import pytest
+from pydantic import BaseModel, Field, ValidationError
+
+from flink_agents.api.tools.utils import (
+    create_java_tool_schema_str_from_model,
+    create_model_from_java_tool_schema_str,
+    create_model_from_schema,
+    create_schema_from_function,
+)
+
+# ---- create_schema_from_function 
---------------------------------------------
+
+
+def _sample_function(
+    a: int,
+    b: str = "x",
+    c=5,
+    d: Annotated[int, "annotated desc"] = 0,
+) -> None:
+    """Sample function for schema extraction.
+
+    Parameters
+    ----------
+    a : int
+        the a param
+    b : str
+        the b param
+    """
+    raise NotImplementedError
+
+
+def test_schema_from_function_required_param() -> None:
+    field = create_schema_from_function("sample", 
_sample_function).model_fields["a"]
+    assert field.annotation is int
+    assert field.is_required()
+    assert field.description == "the a param"
+
+
+def test_schema_from_function_default_makes_param_optional() -> None:
+    field = create_schema_from_function("sample", 
_sample_function).model_fields["b"]
+    assert field.annotation is str
+    assert not field.is_required()
+    assert field.default == "x"
+    assert field.description == "the b param"
+
+
+def test_schema_from_function_unannotated_param_is_any() -> None:
+    field = create_schema_from_function("sample", 
_sample_function).model_fields["c"]
+    # No annotation falls back to Any, and the missing docstring entry to a
+    # placeholder description.
+    assert field.annotation is Any
+    assert field.default == 5
+    assert field.description == "Parameter: c"
+
+
+def test_schema_from_function_annotated_metadata_is_description() -> None:
+    field = create_schema_from_function("sample", 
_sample_function).model_fields["d"]
+    # Annotated[int, "annotated desc"] keeps the int type and uses the metadata
+    # string as the field description.
+    assert field.annotation is int
+    assert field.description == "annotated desc"
+    assert field.default == 0
+
+
+# ---- create_model_from_schema 
------------------------------------------------
+
+
+class _Inner(BaseModel):
+    x: int
+
+
+class _Outer(BaseModel):
+    name: str = Field(description="the name")
+    count: int = Field(ge=0, le=10, default=1)
+    tags: List[str] = []
+    maybe: int | None = None
+    inner: _Inner
+
+
[email protected](scope="module")
+def rebuilt_model() -> type[BaseModel]:
+    return create_model_from_schema("Rebuilt", _Outer.model_json_schema())
+
+
+def test_model_from_schema_scalar_field(rebuilt_model: type[BaseModel]) -> 
None:
+    field = rebuilt_model.model_fields["name"]
+    assert field.annotation is str
+    assert field.is_required()
+    assert field.description == "the name"
+
+
+def test_model_from_schema_typed_array(rebuilt_model: type[BaseModel]) -> None:
+    assert rebuilt_model.model_fields["tags"].annotation == list[str]
+
+
+def test_model_from_schema_optional_field(rebuilt_model: type[BaseModel]) -> 
None:
+    assert not rebuilt_model.model_fields["maybe"].is_required()
+    assert rebuilt_model(name="a", inner={"x": 1}).maybe is None
+
+
+def test_model_from_schema_nested_model(rebuilt_model: type[BaseModel]) -> 
None:
+    inner_field = rebuilt_model.model_fields["inner"]
+    assert isinstance(inner_field.annotation, type)
+    assert issubclass(inner_field.annotation, BaseModel)
+    with pytest.raises(ValidationError):
+        rebuilt_model(name="a", inner={})  # nested 'x' is required
+
+
+def test_model_from_schema_enforces_constraints(rebuilt_model: 
type[BaseModel]) -> None:
+    # ge=0, le=10 from the source schema are kept on the rebuilt model.
+    assert rebuilt_model(name="a", count=5, inner={"x": 1}).count == 5
+    with pytest.raises(ValidationError):
+        rebuilt_model(name="a", count=99, inner={"x": 1})
+
+
+# ---- create_model_from_java_tool_schema_str 
----------------------------------
+
+
+def test_model_from_java_schema_type_mapping() -> None:
+    schema_str = json.dumps(
+        {
+            "properties": {
+                "s": {"type": "string", "description": "d"},
+                "i": {"type": "integer", "description": "d"},
+                "f": {"type": "number", "description": "d"},
+                "b": {"type": "boolean", "description": "d"},
+                "arr": {"type": "array", "description": "d"},
+                "obj": {"type": "object", "description": "d"},
+            }
+        }
+    )
+    fields = create_model_from_java_tool_schema_str("J", 
schema_str).model_fields
+    assert fields["s"].annotation is str
+    assert fields["i"].annotation is int
+    assert fields["f"].annotation is float
+    assert fields["b"].annotation is bool
+    assert fields["arr"].annotation is list
+    assert fields["obj"].annotation is dict
+
+
+def test_model_from_java_schema_preserves_description() -> None:
+    schema_str = json.dumps(
+        {"properties": {"id": {"type": "string", "description": "the id"}}}
+    )
+    field = create_model_from_java_tool_schema_str("J", 
schema_str).model_fields["id"]
+    assert field.description == "the id"
+
+
+def test_model_from_java_schema_fields_are_required() -> None:
+    schema_str = json.dumps(
+        {"properties": {"a": {"type": "string", "description": "d"}}}
+    )
+    field = create_model_from_java_tool_schema_str("J", 
schema_str).model_fields["a"]
+    assert field.is_required()
+
+
+# ---- create_java_tool_schema_str_from_model 
----------------------------------
+
+
+class _JavaSourceModel(BaseModel):
+    id: str = Field(description="the id")
+    n: int = Field(description="a number")
+    opt: str | None = Field(default=None, description="optional one")
+
+
+def test_java_schema_from_model_types_and_descriptions() -> None:
+    props = 
json.loads(create_java_tool_schema_str_from_model(_JavaSourceModel))[
+        "properties"
+    ]
+    assert props["id"] == {"type": "string", "description": "the id"}
+    assert props["n"] == {"type": "integer", "description": "a number"}
+    # `str | None` is unwrapped to its inner "string" type.
+    assert props["opt"]["type"] == "string"
+
+
+def test_java_schema_from_model_required_list() -> None:
+    schema = 
json.loads(create_java_tool_schema_str_from_model(_JavaSourceModel))
+    assert set(schema["required"]) == {"id", "n"}
+    assert "opt" not in schema["required"]
+
+
+# ---- round trip 
--------------------------------------------------------------
+
+
+class _RoundTripModel(BaseModel):
+    id: str = Field(description="the id")
+    n: int = Field(description="a number")
+
+
+def test_java_schema_round_trip_preserves_fields() -> None:
+    # create_model_from_java_tool_schema_str is the documented inverse of
+    # create_java_tool_schema_str_from_model for names, types and descriptions.
+    schema_str = create_java_tool_schema_str_from_model(_RoundTripModel)
+    rebuilt = create_model_from_java_tool_schema_str("RoundTrip", schema_str)
+
+    original = _RoundTripModel.model_fields
+    result = rebuilt.model_fields
+    assert set(result) == set(original)
+    for name, field in original.items():
+        assert result[name].annotation is field.annotation
+        assert result[name].description == field.description

Reply via email to