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