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

wenjin 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 be6783a4 [Tech Debt]Add cross-language resource consistency checking 
tool (#579)
be6783a4 is described below

commit be6783a4f1565905182a7289336063cba488e7cd
Author: bigken <[email protected]>
AuthorDate: Mon Apr 13 13:56:49 2026 +0800

    [Tech Debt]Add cross-language resource consistency checking tool (#579)
    
    * [Tech Debt]Add cross-language resource consistency checking tool
    
    * [test]ResourceCheckTest add Apache licence
    
    * [test] Fix Ruff violations in test_resource_name
    
    * [test] Fix code style
    
    * [ci] Remove check resourceName to cross_language_tests
    
    * [ci] Remove check resourceName to e2e.sh
    
    * [ci] Fix consistency between Python resourceName and Java resourceName
    
    * [doc] fix resource.py doc
    
    * [doc] fix resourceName in docs
    
    ---------
    
    Co-authored-by: yan.xu <[email protected]>
---
 .../flink/agents/api/resource/ResourceName.java    |   2 +-
 docs/content/docs/development/chat_models.md       |   8 +-
 .../agents/integration/test/ResourceCheckTest.java |  98 ++++++
 .../test-scripts/check_resource_consistency.py     | 328 +++++++++++++++++++++
 python/flink_agents/api/resource.py                |   6 +-
 .../flink_agents/api/tests/test_resource_name.py   |  83 ++++++
 .../chat_model_integration_agent.py                |   6 +-
 tools/e2e.sh                                       |  10 +
 8 files changed, 530 insertions(+), 11 deletions(-)

diff --git 
a/api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java 
b/api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java
index 5ec16fcd..e4002be7 100644
--- a/api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java
+++ b/api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java
@@ -34,7 +34,7 @@ package org.apache.flink.agents.api.resource;
  * <pre>{@code
  * // Java implementation
  * ResourceName.ChatModel.OLLAMA_CONNECTION
- * ResourceName.ChatModel.OPENAI_SETUP
+ * ResourceName.ChatModel.OPENAI_COMPLETIONS_SETUP
  *
  * // Python implementation
  * ResourceName.ChatModel.Python.OLLAMA_CONNECTION
diff --git a/docs/content/docs/development/chat_models.md 
b/docs/content/docs/development/chat_models.md
index 4a43c8d3..82ea82f8 100644
--- a/docs/content/docs/development/chat_models.md
+++ b/docs/content/docs/development/chat_models.md
@@ -734,7 +734,7 @@ class MyAgent(Agent):
     @staticmethod
     def openai_connection() -> ResourceDescriptor:
         return ResourceDescriptor(
-            clazz=ResourceName.ChatModel.OPENAI_CONNECTION,
+            clazz=ResourceName.ChatModel.OPENAI_COMPLETIONS_CONNECTION,
             api_key="<your-api-key>",
             api_base_url="https://api.openai.com/v1";,
             max_retries=3,
@@ -745,7 +745,7 @@ class MyAgent(Agent):
     @staticmethod
     def openai_chat_model() -> ResourceDescriptor:
         return ResourceDescriptor(
-            clazz=ResourceName.ChatModel.OPENAI_SETUP,
+            clazz=ResourceName.ChatModel.OPENAI_COMPLETIONS_SETUP,
             connection="openai_connection",
             model="gpt-4",
             temperature=0.7,
@@ -761,7 +761,7 @@ class MyAgent(Agent):
 public class MyAgent extends Agent {
     @ChatModelConnection
     public static ResourceDescriptor openaiConnection() {
-        return 
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OPENAI_CONNECTION)
+        return 
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OPENAI_COMPLETIONS_CONNECTION)
                 .addInitialArgument("api_key", "<your-api-key>")
                 .addInitialArgument("api_base_url", 
"https://api.openai.com/v1";)
                 .addInitialArgument("timeout", 60)
@@ -771,7 +771,7 @@ public class MyAgent extends Agent {
 
     @ChatModelSetup
     public static ResourceDescriptor openaiChatModel() {
-        return 
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OPENAI_SETUP)
+        return 
ResourceDescriptor.Builder.newBuilder(ResourceName.ChatModel.OPENAI_COMPLETIONS_SETUP)
                 .addInitialArgument("connection", "openaiConnection")
                 .addInitialArgument("model", "gpt-4")
                 .addInitialArgument("temperature", 0.7d)
diff --git 
a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/ResourceCheckTest.java
 
b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/ResourceCheckTest.java
new file mode 100644
index 00000000..2bcf753b
--- /dev/null
+++ 
b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/ResourceCheckTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+package org.apache.flink.agents.integration.test;
+
+import org.apache.flink.agents.api.resource.ResourceName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Verify that the Java class name defined in {@link ResourceName} corresponds 
to a class that
+ * exists.
+ */
+public class ResourceCheckTest {
+
+    private static final String JAVA_CLASS_PREFIX = "org.apache.flink.agents.";
+
+    @Test
+    public void checkResourceNameJavaClassesExist() throws Exception {
+        List<String> missing = new ArrayList<>();
+
+        collectAndCheckJavaClasses(ResourceName.class, "", false, missing);
+
+        if (missing.isEmpty()) {
+            System.out.println(
+                    "Success: The Java class referenced by ResourceName has 
passed validation without any missing or conflicting elements.");
+        }
+
+        assertThat(missing)
+                .as(
+                        "The following Java class does not exist in 
ResourceName, please check the class name or module dependencies: %s",
+                        missing)
+                .isEmpty();
+    }
+
+    private void collectAndCheckJavaClasses(
+            Class<?> clazz, String prefix, boolean underPythonClass, 
List<String> missing)
+            throws Exception {
+        for (Field field : clazz.getDeclaredFields()) {
+            if (!isPublicStaticFinalString(field)) {
+                continue;
+            }
+            field.setAccessible(true);
+            String value = (String) field.get(null);
+            if (value == null || value.isEmpty()) {
+                continue;
+            }
+            String fieldPath = prefix + clazz.getSimpleName() + "." + 
field.getName();
+
+            // Skip Python path && unconventional path
+            if (!underPythonClass && value.startsWith(JAVA_CLASS_PREFIX)) {
+                try {
+                    ClassLoader.getSystemClassLoader().loadClass(value);
+                } catch (ClassNotFoundException e) {
+                    missing.add(value + " (from " + fieldPath + ")");
+                }
+            }
+        }
+
+        for (Class<?> inner : clazz.getDeclaredClasses()) {
+            if (!Modifier.isStatic(inner.getModifiers())) {
+                continue;
+            }
+            boolean nextUnderPythonClass =
+                    underPythonClass || "Python".equals(inner.getSimpleName());
+            collectAndCheckJavaClasses(
+                    inner, prefix + clazz.getSimpleName() + ".", 
nextUnderPythonClass, missing);
+        }
+    }
+
+    private static boolean isPublicStaticFinalString(Field field) {
+        int m = field.getModifiers();
+        return Modifier.isPublic(m)
+                && Modifier.isStatic(m)
+                && Modifier.isFinal(m)
+                && field.getType() == String.class;
+    }
+}
diff --git a/e2e-test/test-scripts/check_resource_consistency.py 
b/e2e-test/test-scripts/check_resource_consistency.py
new file mode 100644
index 00000000..6734c9bd
--- /dev/null
+++ b/e2e-test/test-scripts/check_resource_consistency.py
@@ -0,0 +1,328 @@
+#!/usr/bin/env python3
+################################################################################
+#  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.
+################################################################################
+"""
+Check the consistency of resource class name constants between 
ResourceName.java and resource.py
+"""
+
+from __future__ import annotations
+
+import re
+import sys
+from pathlib import Path
+
+
+def parse_java_resource_name(java_path: Path) -> dict:
+    content = java_path.read_text(encoding="utf-8")
+
+    string_const_re = re.compile(
+        r'public\s+static\s+final\s+String\s+([A-Za-z0-9_]+)\s*=\s*"([^"]+)";',
+        re.DOTALL,
+    )
+    class_re = re.compile(
+        r"public\s+(?:static\s+)?final\s+class\s+(\w+)\s*\{"
+    )
+
+    class_stack = []
+    brace_depth = 0
+    result = {}
+    pos = 0
+
+    while True:
+        next_class = class_re.search(content, pos)
+        next_string = string_const_re.search(content, pos)
+        next_open = content.find("{", pos)
+        next_close = content.find("}", pos)
+
+        best_pos = len(content)
+        next_event = None
+        if next_class and next_class.start() < best_pos:
+            best_pos = next_class.start()
+            next_event = ("class", next_class)
+        if next_string and next_string.start() < best_pos:
+            best_pos = next_string.start()
+            next_event = ("string", next_string)
+        if next_open >= 0 and next_open < best_pos:
+            best_pos = next_open
+            next_event = ("open", None)
+        if next_close >= 0 and next_close < best_pos:
+            best_pos = next_close
+            next_event = ("close", None)
+
+        if next_event is None:
+            break
+
+        kind, match = next_event
+        pos = best_pos + 1
+
+        if kind == "class" and match:
+            name = match.group(1)
+            brace_depth += 1
+            if name == "ResourceName":
+                class_stack = [("ResourceName", 1)]
+            elif class_stack:
+                class_stack.append((name, brace_depth))
+            pos = match.end()
+        elif kind == "string" and match and class_stack:
+            const_name, value = match.group(1), match.group(2)
+            if value and value != "DECIDE_IN_RUNTIME_MCPServer":
+                path = [c[0] for c in class_stack]
+
+                if len(path) == 2:
+                    key = (path[1], "Java")
+                    if key not in result:
+                        result[key] = {}
+                    result[key][const_name] = value
+                elif len(path) == 3 and path[2] == "Python":
+                    key = (path[1], "Python")
+                    if key not in result:
+                        result[key] = {}
+                    result[key][const_name] = value
+            pos = match.end()
+        elif kind == "open":
+            brace_depth += 1
+            pos = next_open + 1
+        elif kind == "close":
+            brace_depth -= 1
+            if class_stack and brace_depth == class_stack[-1][1] - 1:
+                class_stack.pop()
+            pos = next_close + 1
+
+    return result
+
+
+def get_python_resource_name_map(python_path: Path) -> dict:
+    root = python_path.parent.parent
+    python_dir = root / "python"
+
+    try:
+        if str(python_dir) not in sys.path:
+            sys.path.insert(0, str(python_dir))
+        for p in python_dir.glob(".venv/lib/python*/site-packages"):
+            if str(p) not in sys.path:
+                sys.path.insert(0, str(p))
+                break
+        from flink_agents.api.resource import ResourceName
+
+        python_map = {}
+        for resource_name in ["ChatModel", "EmbeddingModel", "VectorStore"]:
+            if not hasattr(ResourceName, resource_name):
+                continue
+            resource_cls = getattr(ResourceName, resource_name)
+            py_impls = {
+                attr: getattr(resource_cls, attr)
+                for attr in dir(resource_cls)
+                if not attr.startswith("_")
+                and isinstance(getattr(resource_cls, attr), str)
+            }
+            if py_impls:
+                python_map[(resource_name, "Python")] = py_impls
+            if hasattr(resource_cls, "Java"):
+                java_impls = {
+                    attr: getattr(resource_cls.Java, attr)
+                    for attr in dir(resource_cls.Java)
+                    if not attr.startswith("_")
+                    and isinstance(getattr(resource_cls.Java, attr), str)
+                }
+                if java_impls:
+                    python_map[(resource_name, "Java")] = java_impls
+        if hasattr(ResourceName, "MCP_SERVER"):
+            python_map[("MCP", "Python")] = {"MCP_SERVER": 
ResourceName.MCP_SERVER}
+        return python_map
+    except ImportError:
+        return _parse_python_resource_name(python_path)
+
+
+def _parse_python_resource_name(python_path: Path) -> dict:
+    content = python_path.read_text(encoding="utf-8")
+    string_const_re = re.compile(r'^\s+([A-Za-z0-9_]+)\s*=\s*"([^"]+)"\s*$')
+    class_re = re.compile(r"class\s+(\w+)\s*:")
+
+    result = {}
+    indent_stack = []  # [(indent, ["ResourceName", "ChatModel", ...])]
+    base_indent = -1
+
+    for line in content.split("\n"):
+        if "class ResourceName:" in line:
+            base_indent = len(line) - len(line.lstrip())
+            indent_stack = [(base_indent, ["ResourceName"])]
+            continue
+        if base_indent < 0:
+            continue
+
+        indent = len(line) - len(line.lstrip())
+        stripped = line.strip()
+
+        while len(indent_stack) > 1 and indent <= indent_stack[-1][0] and 
stripped:
+            indent_stack.pop()
+
+        if stripped.startswith("class ") and class_re.match(stripped):
+            name = class_re.match(stripped).group(1)
+            if indent == base_indent + 4:
+                indent_stack = [(base_indent, ["ResourceName"])]
+                indent_stack.append((indent, ["ResourceName", name]))
+            elif indent == base_indent + 8 and len(indent_stack) >= 2:
+                parent_path = indent_stack[-1][1]
+                indent_stack.append((indent, parent_path + [name]))
+
+        path = indent_stack[-1][1] if indent_stack else []
+        m = string_const_re.match(line)
+        if m and path:
+            const_name, value = m.group(1), m.group(2)
+            if value:
+                key = ".".join(path)
+                if key not in result:
+                    result[key] = {}
+                result[key][const_name] = value
+
+    python_map = {}
+    for key, consts in result.items():
+        parts = key.split(".")
+        if len(parts) >= 2 and parts[0] == "ResourceName":
+            rt = parts[1]
+            if len(parts) == 2:
+                python_map[(rt, "Python")] = consts
+            elif len(parts) == 3 and parts[2] == "Java":
+                python_map[(rt, "Java")] = consts
+    if "ResourceName" in result and "MCP_SERVER" in result["ResourceName"]:
+        python_map[("MCP", "Python")] = {"MCP_SERVER": 
result["ResourceName"]["MCP_SERVER"]}
+    return python_map
+
+
+_JAVA_ONLY_NAMES = frozenset({
+    "PYTHON_WRAPPER_CONNECTION", "PYTHON_WRAPPER_SETUP",
+    "PYTHON_WRAPPER_VECTOR_STORE", 
"PYTHON_WRAPPER_COLLECTION_MANAGEABLE_VECTOR_STORE",
+})
+_PYTHON_ONLY_NAMES = frozenset({
+    "JAVA_WRAPPER_CONNECTION", "JAVA_WRAPPER_SETUP",
+    "JAVA_WRAPPER_VECTOR_STORE", 
"JAVA_WRAPPER_COLLECTION_MANAGEABLE_VECTOR_STORE",
+})
+
+
+def _find_python_name_for_value(impls: dict, value: str, java_name: str) -> 
str | None:
+    return java_name if impls.get(java_name) == value else None
+
+
+def check_consistency(
+    java_map: dict, python_map: dict
+) -> tuple[list[str], list[str]]:
+
+    errors = []
+    warnings = []
+
+    all_resource_types = set()
+    for (rt, _) in java_map:
+        all_resource_types.add(rt)
+    for (rt, _) in python_map:
+        all_resource_types.add(rt)
+
+    for resource_type in sorted(all_resource_types):
+        if resource_type == "MCP":
+            continue
+
+        java_impls = java_map.get((resource_type, "Java"), {})
+        python_java_impls = python_map.get((resource_type, "Java"), {})
+        java_python_impls = java_map.get((resource_type, "Python"), {})
+        python_impls = python_map.get((resource_type, "Python"), {})
+
+        for name, value in java_impls.items():
+            if name in _JAVA_ONLY_NAMES:
+                continue
+            if name in python_java_impls:
+                if python_java_impls[name] != value:
+                    errors.append(
+                        f"[{resource_type}.Java.{name}] not consistent: "
+                        f"Java={value!r} vs Python={python_java_impls[name]!r}"
+                    )
+            else:
+                errors.append(
+                    f"Java have {resource_type}.{name},but Python 
ResourceName.{resource_type}.Java missing corresponding constant"
+                )
+
+        for name, value in java_python_impls.items():
+            py_name = _find_python_name_for_value(python_impls, value, name)
+            if py_name is not None:
+                if python_impls[py_name] != value:
+                    errors.append(
+                        f"[{resource_type}.Python.{name}] not consistent: "
+                        f"Java.Python={value!r} vs 
Python.{py_name}={python_impls[py_name]!r}"
+                    )
+            else:
+                errors.append(
+                    f"Java have {resource_type}.Python.{name}, but Python 
ResourceName.{resource_type} missing corresponding constant({value!r})"
+                )
+
+        for name in python_java_impls:
+            if name not in java_impls:
+                errors.append(
+                    f"Python have {resource_type}.Java.{name}, but Java 
ResourceName.{resource_type} missing corresponding constant"
+                )
+
+        for name, value in python_impls.items():
+            if name in _PYTHON_ONLY_NAMES or name.startswith("JAVA_"):
+                continue
+            if not _find_python_name_for_value(java_python_impls, value, name):
+                if value.startswith("flink_agents."):
+                    warnings.append(
+                        f"Python have {resource_type}.{name}(Python), but Java 
ResourceName.{resource_type}.Python missing corresponding constant"
+                    )
+
+    return errors, warnings
+
+
+def main() -> int:
+    root = Path(__file__).resolve().parent.parent.parent
+    java_path = root / 
"api/src/main/java/org/apache/flink/agents/api/resource/ResourceName.java"
+    python_path = root / "python/flink_agents/api/resource.py"
+
+    if not java_path.exists():
+        print(f"error: can`t find {java_path}", file=sys.stderr)
+        return 1
+    if not python_path.exists():
+        print(f"error: can`t find {python_path}", file=sys.stderr)
+        return 1
+
+    java_map = parse_java_resource_name(java_path)
+    python_map = get_python_resource_name_map(python_path)
+
+    debug = __import__("os").environ.get("RESOURCE_DEBUG")
+    if debug:
+        import json
+        print("Java map:", json.dumps({str(k): v for k, v in 
java_map.items()}, indent=2, ensure_ascii=False))
+        print("Python map:", json.dumps({str(k): v for k, v in 
python_map.items()}, indent=2, ensure_ascii=False))
+
+    errors, warnings = check_consistency(java_map, python_map)
+
+    if errors:
+        print("ResourceName Cross-language consistency check failed:", 
file=sys.stderr)
+        for e in errors:
+            print(f"  error: {e}", file=sys.stderr)
+        return 1
+
+    if warnings:
+        print("ResourceName Cross-language consistency check warn:", 
file=sys.stderr)
+        for w in warnings:
+            print(f"  warnings: {w}", file=sys.stderr)
+    else:
+        print("ResourceName Cross-language consistency check successful")
+
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/python/flink_agents/api/resource.py 
b/python/flink_agents/api/resource.py
index a684202d..13155380 100644
--- a/python/flink_agents/api/resource.py
+++ b/python/flink_agents/api/resource.py
@@ -226,7 +226,7 @@ class ResourceName:
     Example usage:
         # Python implementation
         ResourceName.ChatModel.OLLAMA_CONNECTION
-        ResourceName.ChatModel.OPENAI_SETUP
+        ResourceName.ChatModel.OPENAI_COMPLETIONS_SETUP
 
         # Java implementation
         ResourceName.ChatModel.Java.OLLAMA_CONNECTION
@@ -248,8 +248,8 @@ class ResourceName:
         OLLAMA_SETUP = 
"flink_agents.integrations.chat_models.ollama_chat_model.OllamaChatModelSetup"
 
         # OpenAI
-        OPENAI_CONNECTION = 
"flink_agents.integrations.chat_models.openai.openai_chat_model.OpenAIChatModelConnection"
-        OPENAI_SETUP = 
"flink_agents.integrations.chat_models.openai.openai_chat_model.OpenAIChatModelSetup"
+        OPENAI_COMPLETIONS_CONNECTION = 
"flink_agents.integrations.chat_models.openai.openai_chat_model.OpenAIChatModelConnection"
+        OPENAI_COMPLETIONS_SETUP = 
"flink_agents.integrations.chat_models.openai.openai_chat_model.OpenAIChatModelSetup"
 
         # Tongyi
         TONGYI_CONNECTION = 
"flink_agents.integrations.chat_models.tongyi_chat_model.TongyiChatModelConnection"
diff --git a/python/flink_agents/api/tests/test_resource_name.py 
b/python/flink_agents/api/tests/test_resource_name.py
new file mode 100644
index 00000000..5575f350
--- /dev/null
+++ b/python/flink_agents/api/tests/test_resource_name.py
@@ -0,0 +1,83 @@
+################################################################################
+#  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.
+################################################################################
+"""Verify ResourceName Python paths resolve to existing, importable classes."""
+from __future__ import annotations
+
+import importlib
+import inspect
+
+import pytest
+
+from flink_agents.api.resource import ResourceName
+
+PYTHON_PREFIX = "flink_agents."
+
+
+def _collect_python_class_paths() -> list[tuple[str, str]]:
+    paths = []
+    for resource_name in ["ChatModel", "EmbeddingModel", "VectorStore"]:
+        if not hasattr(ResourceName, resource_name):
+            continue
+        resource_cls = getattr(ResourceName, resource_name)
+        for attr in dir(resource_cls):
+            if attr.startswith("_"):
+                continue
+            val = getattr(resource_cls, attr)
+            if isinstance(val, str) and val.startswith(PYTHON_PREFIX):
+                paths.append((f"{resource_name}.{attr}", val))
+    if hasattr(ResourceName, "MCP_SERVER") and isinstance(
+        ResourceName.MCP_SERVER, str
+    ):
+        if ResourceName.MCP_SERVER.startswith(PYTHON_PREFIX):
+            paths.append(("MCP_SERVER", ResourceName.MCP_SERVER))
+    return paths
+
+
+def _class_exists(full_class_path: str) -> tuple[bool, str]:
+    if not full_class_path or "." not in full_class_path:
+        return False, "Invalid classpath format"
+    last_dot = full_class_path.rindex(".")
+    module_path = full_class_path[:last_dot]
+    class_name = full_class_path[last_dot + 1 :]
+    try:
+        module = importlib.import_module(module_path)
+        cls = getattr(module, class_name, None)
+        if cls is None:
+            return False, f"module {module_path!r} Attribute does not exist 
{class_name!r}"
+        if not inspect.isclass(cls):
+            return False, f"{full_class_path!r} is not a class"
+    except Exception as e:
+        return False, f"import error: {type(e).__name__}: {e}"
+    else:
+        return True, ""
+
+
+_RESOURCE_PATHS = _collect_python_class_paths()
+
+
[email protected](
+    ("path_name", "full_path"),
+    _RESOURCE_PATHS,
+    ids=[p[0] for p in _RESOURCE_PATHS],
+)
+def test_resource_name_python_classes_exist(path_name: str, full_path: str) -> 
None:
+    """Each parameterized path must import as a class."""
+    exists, err_msg = _class_exists(full_path)
+    assert exists, (
+        f"ResourceName.{path_name} = {full_path!r} The corresponding class 
does not exist: {err_msg} "
+    )
diff --git 
a/python/flink_agents/e2e_tests/e2e_tests_integration/chat_model_integration_agent.py
 
b/python/flink_agents/e2e_tests/e2e_tests_integration/chat_model_integration_agent.py
index 2838bbf5..f98795c5 100644
--- 
a/python/flink_agents/e2e_tests/e2e_tests_integration/chat_model_integration_agent.py
+++ 
b/python/flink_agents/e2e_tests/e2e_tests_integration/chat_model_integration_agent.py
@@ -42,7 +42,7 @@ class ChatModelTestAgent(Agent):
     def openai_connection() -> ResourceDescriptor:
         """ChatModelConnection responsible for openai model service 
connection."""
         return ResourceDescriptor(
-            clazz=ResourceName.ChatModel.OPENAI_CONNECTION, 
api_key=os.environ.get("OPENAI_API_KEY")
+            clazz=ResourceName.ChatModel.OPENAI_COMPLETIONS_CONNECTION, 
api_key=os.environ.get("OPENAI_API_KEY")
         )
 
     @chat_model_connection
@@ -92,7 +92,7 @@ class ChatModelTestAgent(Agent):
             )
         elif model_provider == "OpenAI":
             return ResourceDescriptor(
-                clazz=ResourceName.ChatModel.OPENAI_SETUP,
+                clazz=ResourceName.ChatModel.OPENAI_COMPLETIONS_SETUP,
                 connection="openai_connection",
                 model=os.environ.get("OPENAI_CHAT_MODEL", "gpt-3.5-turbo"),
                 tools=["add"],
@@ -128,7 +128,7 @@ class ChatModelTestAgent(Agent):
             )
         elif model_provider == "OpenAI":
             return ResourceDescriptor(
-                clazz=ResourceName.ChatModel.OPENAI_SETUP,
+                clazz=ResourceName.ChatModel.OPENAI_COMPLETIONS_SETUP,
                 connection="openai_connection",
                 model=os.environ.get("OPENAI_CHAT_MODEL", "gpt-3.5-turbo"),
             )
diff --git a/tools/e2e.sh b/tools/e2e.sh
index 9be84640..886705fa 100755
--- a/tools/e2e.sh
+++ b/tools/e2e.sh
@@ -84,6 +84,15 @@ function run_resource_cross_language_test_in_python {
     cd "$python_dir" && uv run --no-sync pytest flink_agents -s -k 
"e2e_tests_resource_cross_language"
 }
 
+function run_resource_name_consistency_check {
+  if [[ ! -d "$python_dir" ]]; then
+    echo "Error: Python directory '$python_dir' does not exist. Skipping test."
+    return 1
+  fi
+
+  cd "$python_dir" && uv run --no-sync python 
../e2e-test/test-scripts/check_resource_consistency.py
+}
+
 export TOTAL=0
 export PASSED=0
 
@@ -152,6 +161,7 @@ run_test "Resource Cross-Language end-to-end test in Java" 
"run_resource_cross_l
 run_test "Resource Cross-Language end-to-end test in Python" 
"run_resource_cross_language_test_in_python"
 run_test "Agent plan compatibility end-to-end test" 
"run_agent_plan_compatibility_test"
 run_test "Cross-Language Config Option end-to-end test" 
"run_cross_language_config_test"
+run_test "ResourceName Java vs Python consistency check" 
"run_resource_name_consistency_check"
 
 # Clean up temporary directory
 if [[ -d "$tempdir" ]]; then

Reply via email to