From ba0d6f9a69797173c8829beb10eaaad66ba500df Mon Sep 17 00:00:00 2001
From: rosemaryYuan <91046107+rosemarYuan@users.noreply.github.com>
Date: Wed, 3 Jun 2026 11:10:36 +0800
Subject: [PATCH 01/13] [api] Introduce EventType constants for built-in event
types
---
.../apache/flink/agents/api/EventType.java | 167 ++++++++++
.../flink/agents/api/EventTypeTest.java | 301 ++++++++++++++++++
python/flink_agents/api/events/event_type.py | 157 +++++++++
.../flink_agents/api/tests/test_event_type.py | 66 ++++
4 files changed, 691 insertions(+)
create mode 100644 api/src/main/java/org/apache/flink/agents/api/EventType.java
create mode 100644 api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java
create mode 100644 python/flink_agents/api/events/event_type.py
create mode 100644 python/flink_agents/api/tests/test_event_type.py
diff --git a/api/src/main/java/org/apache/flink/agents/api/EventType.java b/api/src/main/java/org/apache/flink/agents/api/EventType.java
new file mode 100644
index 000000000..e0e825ad0
--- /dev/null
+++ b/api/src/main/java/org/apache/flink/agents/api/EventType.java
@@ -0,0 +1,167 @@
+/*
+ * 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.api;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * Compile-time constants for built-in event types and a runtime registry for user-defined events.
+ *
+ *
Resolution via {@link #lookupOrSelf}: built-in → user-registered → passthrough.
+ */
+public final class EventType {
+
+ public static final String InputEvent = "_input_event";
+ public static final String OutputEvent = "_output_event";
+ public static final String ChatRequestEvent = "_chat_request_event";
+ public static final String ChatResponseEvent = "_chat_response_event";
+ public static final String ToolRequestEvent = "_tool_request_event";
+ public static final String ToolResponseEvent = "_tool_response_event";
+ public static final String ContextRetrievalRequestEvent = "_context_retrieval_request_event";
+ public static final String ContextRetrievalResponseEvent = "_context_retrieval_response_event";
+
+ private static final Map BUILTIN;
+
+ static {
+ Map m = new HashMap<>();
+ m.put("InputEvent", InputEvent);
+ m.put("OutputEvent", OutputEvent);
+ m.put("ChatRequestEvent", ChatRequestEvent);
+ m.put("ChatResponseEvent", ChatResponseEvent);
+ m.put("ToolRequestEvent", ToolRequestEvent);
+ m.put("ToolResponseEvent", ToolResponseEvent);
+ m.put("ContextRetrievalRequestEvent", ContextRetrievalRequestEvent);
+ m.put("ContextRetrievalResponseEvent", ContextRetrievalResponseEvent);
+ BUILTIN = Collections.unmodifiableMap(m);
+ }
+
+ private static final ConcurrentMap USER_REGISTERED = new ConcurrentHashMap<>();
+
+ private EventType() {}
+
+ /**
+ * Registers a user-defined event class so that its simple name resolves to its {@code
+ * EVENT_TYPE} value. The class must declare {@code public static final String EVENT_TYPE}.
+ * Idempotent for the same {@code (name, EVENT_TYPE)} pair.
+ *
+ * @param eventClass the event class to register
+ */
+ public static void register(Class extends Event> eventClass) {
+ if (eventClass == null) {
+ throw new IllegalArgumentException("eventClass must not be null");
+ }
+ String name = eventClass.getSimpleName();
+ if (BUILTIN.containsKey(name)) {
+ throw new IllegalArgumentException(
+ "Short name '" + name + "' collides with a built-in EventType");
+ }
+ String eventType = readEventTypeField(eventClass);
+ String previous = USER_REGISTERED.putIfAbsent(name, eventType);
+ if (previous != null && !previous.equals(eventType)) {
+ throw new IllegalStateException(
+ "Short name '"
+ + name
+ + "' already registered with EVENT_TYPE='"
+ + previous
+ + "', cannot re-register with EVENT_TYPE='"
+ + eventType
+ + "'");
+ }
+ }
+
+ /**
+ * Returns the {@code EVENT_TYPE} for a registered short name, or {@code null} if unknown.
+ * Built-in names take precedence over user-registered ones.
+ */
+ public static String lookup(String name) {
+ if (name == null) {
+ return null;
+ }
+ String v = BUILTIN.get(name);
+ if (v != null) {
+ return v;
+ }
+ return USER_REGISTERED.get(name);
+ }
+
+ /** Like {@link #lookup}, but returns {@code name} unchanged if not registered. */
+ public static String lookupOrSelf(String name) {
+ String v = lookup(name);
+ return v != null ? v : name;
+ }
+
+ /** Returns {@code true} if {@code name} is a registered short name. */
+ public static boolean isKnown(String name) {
+ return lookup(name) != null;
+ }
+
+ /** Returns an unmodifiable snapshot of all registrations (built-in + user-registered). */
+ public static Map all() {
+ Map snapshot = new HashMap<>(BUILTIN);
+ snapshot.putAll(USER_REGISTERED);
+ return Collections.unmodifiableMap(snapshot);
+ }
+
+ private static String readEventTypeField(Class extends Event> eventClass) {
+ Field field;
+ try {
+ field = eventClass.getDeclaredField("EVENT_TYPE");
+ } catch (NoSuchFieldException e) {
+ throw new IllegalArgumentException(
+ eventClass.getName() + " must declare 'static final String EVENT_TYPE'", e);
+ }
+ int mods = field.getModifiers();
+ if (!Modifier.isStatic(mods)
+ || !Modifier.isFinal(mods)
+ || field.getType() != String.class) {
+ throw new IllegalArgumentException(
+ eventClass.getName() + ".EVENT_TYPE must be static final String");
+ }
+ try {
+ field.setAccessible(true);
+ Object value = field.get(null);
+ if (!(value instanceof String) || ((String) value).isEmpty()) {
+ throw new IllegalArgumentException(
+ eventClass.getName() + ".EVENT_TYPE must be a non-empty String");
+ }
+ return (String) value;
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(
+ "Cannot read " + eventClass.getName() + ".EVENT_TYPE", e);
+ }
+ }
+
+ /** Test-only: reset user registrations between unit tests. */
+ static void clearUserRegisteredForTesting() {
+ USER_REGISTERED.clear();
+ }
+}
diff --git a/api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java b/api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java
new file mode 100644
index 000000000..dd3a02a5b
--- /dev/null
+++ b/api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java
@@ -0,0 +1,301 @@
+/*
+ * 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.api;
+
+import org.apache.flink.agents.api.event.ChatRequestEvent;
+import org.apache.flink.agents.api.event.ChatResponseEvent;
+import org.apache.flink.agents.api.event.ContextRetrievalRequestEvent;
+import org.apache.flink.agents.api.event.ContextRetrievalResponseEvent;
+import org.apache.flink.agents.api.event.ToolRequestEvent;
+import org.apache.flink.agents.api.event.ToolResponseEvent;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Tests for {@link EventType}. */
+class EventTypeTest {
+
+ @BeforeEach
+ @AfterEach
+ void resetUserRegistry() {
+ EventType.clearUserRegisteredForTesting();
+ }
+
+ // -----------------------------------------------------------------------
+ // Built-in constants must agree byte-for-byte with XxxEvent.EVENT_TYPE.
+ // This is the consistency invariant called out in the EventType Javadoc.
+ // -----------------------------------------------------------------------
+
+ @Test
+ void builtInConstantsMatchEventClassConstants() {
+ assertEquals(InputEvent.EVENT_TYPE, EventType.InputEvent);
+ assertEquals(OutputEvent.EVENT_TYPE, EventType.OutputEvent);
+ assertEquals(ChatRequestEvent.EVENT_TYPE, EventType.ChatRequestEvent);
+ assertEquals(ChatResponseEvent.EVENT_TYPE, EventType.ChatResponseEvent);
+ assertEquals(ToolRequestEvent.EVENT_TYPE, EventType.ToolRequestEvent);
+ assertEquals(ToolResponseEvent.EVENT_TYPE, EventType.ToolResponseEvent);
+ assertEquals(
+ ContextRetrievalRequestEvent.EVENT_TYPE, EventType.ContextRetrievalRequestEvent);
+ assertEquals(
+ ContextRetrievalResponseEvent.EVENT_TYPE, EventType.ContextRetrievalResponseEvent);
+ }
+
+ @Test
+ void lookupReturnsBuiltInsByShortName() {
+ assertEquals(InputEvent.EVENT_TYPE, EventType.lookup("InputEvent"));
+ assertEquals(OutputEvent.EVENT_TYPE, EventType.lookup("OutputEvent"));
+ assertEquals(ChatResponseEvent.EVENT_TYPE, EventType.lookup("ChatResponseEvent"));
+ }
+
+ @Test
+ void lookupReturnsNullForUnknown() {
+ assertNull(EventType.lookup("DoesNotExist"));
+ assertNull(EventType.lookup(""));
+ assertNull(EventType.lookup(null));
+ }
+
+ @Test
+ void lookupOrSelfReturnsInputForUnknown() {
+ // Raw EVENT_TYPE strings pass through unchanged.
+ assertEquals("_input_event", EventType.lookupOrSelf("_input_event"));
+ // Custom strings pass through unchanged.
+ assertEquals("MyCustomEvent", EventType.lookupOrSelf("MyCustomEvent"));
+ // CEL expressions pass through unchanged.
+ String cel = "type == '_input_event' && price >= 125";
+ assertEquals(cel, EventType.lookupOrSelf(cel));
+ }
+
+ @Test
+ void isKnownTracksBuiltins() {
+ assertTrue(EventType.isKnown("InputEvent"));
+ assertTrue(EventType.isKnown("ChatResponseEvent"));
+ assertFalse(EventType.isKnown("_input_event"));
+ assertFalse(EventType.isKnown("DoesNotExist"));
+ assertFalse(EventType.isKnown(""));
+ assertFalse(EventType.isKnown(null));
+ }
+
+ @Test
+ void allReturnsSnapshotIncludingBuiltins() {
+ Map all = EventType.all();
+ assertEquals(8, all.size());
+ assertEquals("_input_event", all.get("InputEvent"));
+ assertEquals("_chat_response_event", all.get("ChatResponseEvent"));
+ // Snapshot is unmodifiable.
+ assertThrows(UnsupportedOperationException.class, () -> all.put("Foo", "bar"));
+ }
+
+ // -----------------------------------------------------------------------
+ // User registration.
+ // -----------------------------------------------------------------------
+
+ public static class MyCustomEvent extends Event {
+ public static final String EVENT_TYPE = "_my_custom_event";
+
+ public MyCustomEvent() {
+ super(EVENT_TYPE);
+ }
+ }
+
+ public static class AnotherEvent extends Event {
+ public static final String EVENT_TYPE = "_another_event";
+
+ public AnotherEvent() {
+ super(EVENT_TYPE);
+ }
+ }
+
+ @Test
+ void registerExposesUserDefinedEvent() {
+ EventType.register(MyCustomEvent.class);
+ assertEquals("_my_custom_event", EventType.lookup("MyCustomEvent"));
+ assertTrue(EventType.isKnown("MyCustomEvent"));
+ assertEquals(9, EventType.all().size());
+ }
+
+ @Test
+ void registerIsIdempotentForSamePair() {
+ EventType.register(MyCustomEvent.class);
+ EventType.register(MyCustomEvent.class); // must not throw
+ assertEquals("_my_custom_event", EventType.lookup("MyCustomEvent"));
+ }
+
+ /** Re-registering the same simple name with a different EVENT_TYPE must fail loudly. */
+ public static class CollidingEvent extends Event {
+ public static final String EVENT_TYPE = "_different_event";
+
+ public CollidingEvent() {
+ super(EVENT_TYPE);
+ }
+ }
+
+ public static class ShadowingMyCustomEvent extends Event {
+ public static final String EVENT_TYPE = "_shadow_event";
+
+ public ShadowingMyCustomEvent() {
+ super(EVENT_TYPE);
+ }
+ }
+
+ @Test
+ void registerDifferentClassesWithDifferentSimpleNamesCoexist() {
+ EventType.register(MyCustomEvent.class);
+ EventType.register(ShadowingMyCustomEvent.class);
+ assertEquals("_my_custom_event", EventType.lookup("MyCustomEvent"));
+ assertEquals("_shadow_event", EventType.lookup("ShadowingMyCustomEvent"));
+ }
+
+ /**
+ * Direct test of the conflict path: register one class, then register a second class whose
+ * {@code getSimpleName()} happens to collide, with a different EVENT_TYPE. We construct the
+ * collision by using two top-level-style nested classes whose simple names match.
+ */
+ @Test
+ void registerRejectsSameNameDifferentEventType() {
+ EventType.register(MyCustomEvent.class);
+ // Manually trigger the conflict by re-using the registry with a synthetic mapping.
+ // Easiest path: declare a second class with the same simple name in a different
+ // enclosing scope.
+ IllegalStateException ex =
+ assertThrows(
+ IllegalStateException.class,
+ () -> EventType.register(Nested.MyCustomEvent.class));
+ assertTrue(ex.getMessage().contains("already registered"));
+ }
+
+ static class Nested {
+ public static class MyCustomEvent extends Event {
+ public static final String EVENT_TYPE = "_nested_my_custom_event";
+
+ public MyCustomEvent() {
+ super(EVENT_TYPE);
+ }
+ }
+ }
+
+ @Test
+ void registerRejectsCollisionWithBuiltIn() {
+ // A user-defined class whose simple name equals a built-in must be rejected.
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> EventType.register(MyInputEvent.InputEvent.class));
+ assertTrue(ex.getMessage().contains("collides with a built-in"));
+ }
+
+ // Wrap in an enclosing class so we can name the inner class "InputEvent" without colliding
+ // with the api package's InputEvent at the source level.
+ static class MyInputEvent {
+ public static class InputEvent extends Event {
+ public static final String EVENT_TYPE = "_user_input_event";
+
+ public InputEvent() {
+ super(EVENT_TYPE);
+ }
+ }
+ }
+
+ public static class NoEventTypeField extends Event {
+ public NoEventTypeField() {
+ super("_x");
+ }
+ }
+
+ @Test
+ void registerRejectsClassWithoutEventTypeField() {
+ IllegalArgumentException ex =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> EventType.register(NoEventTypeField.class));
+ assertTrue(ex.getMessage().contains("EVENT_TYPE"));
+ }
+
+ @Test
+ void registerRejectsNull() {
+ assertThrows(IllegalArgumentException.class, () -> EventType.register(null));
+ }
+
+ // -----------------------------------------------------------------------
+ // Concurrency: two threads registering the same class should both succeed
+ // (idempotent), and the registry should hold exactly one entry.
+ // -----------------------------------------------------------------------
+
+ @Test
+ void concurrentRegisterIsSafe() throws InterruptedException {
+ int threads = 16;
+ CountDownLatch start = new CountDownLatch(1);
+ CountDownLatch done = new CountDownLatch(threads);
+ AtomicInteger errors = new AtomicInteger();
+ ExecutorService pool = Executors.newFixedThreadPool(threads);
+ try {
+ for (int i = 0; i < threads; i++) {
+ pool.submit(
+ () -> {
+ try {
+ start.await();
+ EventType.register(MyCustomEvent.class);
+ } catch (Exception e) {
+ errors.incrementAndGet();
+ } finally {
+ done.countDown();
+ }
+ });
+ }
+ start.countDown();
+ assertTrue(done.await(10, TimeUnit.SECONDS));
+ assertEquals(0, errors.get());
+ assertEquals("_my_custom_event", EventType.lookup("MyCustomEvent"));
+ // Built-in count (8) + our one registration = 9
+ assertEquals(9, EventType.all().size());
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // Snapshot independence.
+ // -----------------------------------------------------------------------
+
+ @Test
+ void allReturnsSnapshotNotLiveView() {
+ Map before = EventType.all();
+ EventType.register(MyCustomEvent.class);
+ Map after = EventType.all();
+ assertNotNull(before);
+ // Snapshot before registration must not contain the new entry.
+ assertFalse(before.containsKey("MyCustomEvent"));
+ assertTrue(after.containsKey("MyCustomEvent"));
+ assertEquals(new HashMap<>(before).size() + 1, after.size());
+ }
+}
diff --git a/python/flink_agents/api/events/event_type.py b/python/flink_agents/api/events/event_type.py
new file mode 100644
index 000000000..e36655ac9
--- /dev/null
+++ b/python/flink_agents/api/events/event_type.py
@@ -0,0 +1,157 @@
+################################################################################
+# 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.
+#################################################################################
+"""IDE-discoverable constants for built-in event types, plus a tiny registry
+for user-defined event classes.
+
+Built-in events are exposed as :class:`EventType` attributes; user events
+declare ``EVENT_TYPE: ClassVar[str]`` and are registered via
+:func:`EventType.register`::
+
+ @action(EventType.InputEvent, EventType.OutputEvent)
+ def handle(...): ...
+
+ EventType.register(MyCustomEvent)
+ EventType.lookup("MyCustomEvent") # -> "_my_custom_event"
+"""
+
+from __future__ import annotations
+
+import threading
+from typing import Dict, Optional, Type
+
+
+# Hard-coded to avoid an event_type -> event -> event_type circular import.
+# A consistency test asserts each value matches XxxEvent.EVENT_TYPE.
+_BUILTIN: Dict[str, str] = {
+ "InputEvent": "_input_event",
+ "OutputEvent": "_output_event",
+ "ChatRequestEvent": "_chat_request_event",
+ "ChatResponseEvent": "_chat_response_event",
+ "ToolRequestEvent": "_tool_request_event",
+ "ToolResponseEvent": "_tool_response_event",
+ "ContextRetrievalRequestEvent": "_context_retrieval_request_event",
+ "ContextRetrievalResponseEvent": "_context_retrieval_response_event",
+}
+
+_USER_REGISTERED: Dict[str, str] = {}
+_LOCK = threading.Lock()
+
+
+def register(event_class: Type) -> None:
+ """Register a user-defined event class.
+
+ The class must declare ``EVENT_TYPE: ClassVar[str]`` as a non-empty string.
+ Re-registering the same ``(class_name, EVENT_TYPE)`` pair is a no-op.
+
+ Raises:
+ ValueError: if ``event_class`` is None, lacks a non-empty ``EVENT_TYPE``,
+ or its ``__name__`` collides with a built-in.
+ RuntimeError: if the same name is already bound to a different
+ ``EVENT_TYPE`` value.
+ """
+ if event_class is None:
+ msg = "event_class must not be None"
+ raise ValueError(msg)
+ name = event_class.__name__
+ if name in _BUILTIN:
+ msg = f"Short name {name!r} collides with a built-in EventType"
+ raise ValueError(msg)
+ event_type = getattr(event_class, "EVENT_TYPE", None)
+ if not isinstance(event_type, str) or not event_type:
+ msg = (
+ f"{event_class.__module__}.{event_class.__name__} must declare "
+ f"EVENT_TYPE as a non-empty string"
+ )
+ raise ValueError(msg)
+ with _LOCK:
+ existing = _USER_REGISTERED.get(name)
+ if existing is None:
+ _USER_REGISTERED[name] = event_type
+ return
+ if existing != event_type:
+ msg = (
+ f"Short name {name!r} already registered with EVENT_TYPE={existing!r}; "
+ f"cannot re-register with EVENT_TYPE={event_type!r}"
+ )
+ raise RuntimeError(msg)
+
+
+def lookup(name: Optional[str]) -> Optional[str]:
+ """Return the ``EVENT_TYPE`` string for a registered short name, else ``None``.
+
+ Built-in names take precedence over user-registered ones.
+ """
+ if name is None:
+ return None
+ builtin = _BUILTIN.get(name)
+ if builtin is not None:
+ return builtin
+ return _USER_REGISTERED.get(name)
+
+
+def lookup_or_self(name: str) -> str:
+ """Like :func:`lookup`, but returns ``name`` unchanged when not registered."""
+ v = lookup(name)
+ return v if v is not None else name
+
+
+def is_known(name: Optional[str]) -> bool:
+ """Return ``True`` if ``name`` is a registered short name."""
+ return lookup(name) is not None
+
+
+def all_registered() -> Dict[str, str]:
+ """Return a snapshot of all registrations (built-in + user-registered)."""
+ snapshot = dict(_BUILTIN)
+ snapshot.update(_USER_REGISTERED)
+ return snapshot
+
+
+def _clear_user_registered_for_testing() -> None:
+ """Test-only: drop user registrations between unit tests."""
+ with _LOCK:
+ _USER_REGISTERED.clear()
+
+
+class EventType:
+ """Namespace of built-in event-type constants, byte-equal to each
+ ``XxxEvent.EVENT_TYPE``. Use inside ``trigger_conditions``::
+
+ @action(EventType.InputEvent)
+
+ For user-defined events, call :func:`register` first, then :func:`lookup`.
+ """
+
+ InputEvent: str = _BUILTIN["InputEvent"]
+ OutputEvent: str = _BUILTIN["OutputEvent"]
+ ChatRequestEvent: str = _BUILTIN["ChatRequestEvent"]
+ ChatResponseEvent: str = _BUILTIN["ChatResponseEvent"]
+ ToolRequestEvent: str = _BUILTIN["ToolRequestEvent"]
+ ToolResponseEvent: str = _BUILTIN["ToolResponseEvent"]
+ ContextRetrievalRequestEvent: str = _BUILTIN["ContextRetrievalRequestEvent"]
+ ContextRetrievalResponseEvent: str = _BUILTIN["ContextRetrievalResponseEvent"]
+
+ register = staticmethod(register)
+ lookup = staticmethod(lookup)
+ lookup_or_self = staticmethod(lookup_or_self)
+ is_known = staticmethod(is_known)
+ all_registered = staticmethod(all_registered)
+
+ def __init__(self) -> None:
+ msg = "EventType is a namespace; do not instantiate"
+ raise TypeError(msg)
diff --git a/python/flink_agents/api/tests/test_event_type.py b/python/flink_agents/api/tests/test_event_type.py
new file mode 100644
index 000000000..76ef904b9
--- /dev/null
+++ b/python/flink_agents/api/tests/test_event_type.py
@@ -0,0 +1,66 @@
+################################################################################
+# 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.
+#################################################################################
+"""Python smoke tests for :mod:`flink_agents.api.events.event_type`.
+
+The authoritative test suite lives in Java
+(``api/src/test/java/.../EventTypeTest.java``) per cross-language test policy
+(issue #006, Q5). These two cases only verify the Python language-binding
+layer; full semantic coverage belongs on the Java side.
+"""
+
+from __future__ import annotations
+
+from typing import ClassVar
+
+import pytest
+
+from flink_agents.api.events.event import Event, InputEvent
+from flink_agents.api.events.event_type import (
+ EventType,
+ _clear_user_registered_for_testing,
+ lookup_or_self,
+ register,
+)
+
+
+@pytest.fixture(autouse=True)
+def _reset_user_registry() -> None:
+ _clear_user_registered_for_testing()
+ yield
+ _clear_user_registered_for_testing()
+
+
+def test_builtin_lookup_and_constant_alignment() -> None:
+ """Smoke: built-in EventType constants resolve and match XxxEvent.EVENT_TYPE."""
+ assert EventType.InputEvent == InputEvent.EVENT_TYPE
+ assert lookup_or_self("InputEvent") == InputEvent.EVENT_TYPE
+ # Unknown short name passes through unchanged.
+ assert lookup_or_self("NotRegistered") == "NotRegistered"
+
+
+def test_register_user_event_then_lookup_resolves_short_name() -> None:
+ """Smoke: register a user event and lookup the short name."""
+
+ class MyOrderEvent(Event):
+ EVENT_TYPE: ClassVar[str] = "_my_order_event"
+
+ def __init__(self) -> None:
+ super().__init__(type=MyOrderEvent.EVENT_TYPE)
+
+ register(MyOrderEvent)
+ assert lookup_or_self("MyOrderEvent") == "_my_order_event"
From b8fe3a7b4ea840caa52a51b61502edb22f7adbec Mon Sep 17 00:00:00 2001
From: rosemaryYuan <91046107+rosemarYuan@users.noreply.github.com>
Date: Wed, 3 Jun 2026 12:10:06 +0800
Subject: [PATCH 02/13] [api][plan] Unify @Action trigger entry as value(),
migrate Java callers
---
.../flink/agents/api/agents/ReActAgent.java | 3 +-
.../flink/agents/api/annotation/Action.java | 35 +++++++++----------
docs/content/docs/development/chat_models.md | 8 ++---
.../docs/development/embedding_models.md | 4 +--
.../development/memory/long_term_memory.md | 4 +--
.../memory/sensory_and_short_term_memory.md | 6 ++--
docs/content/docs/development/prompts.md | 2 +-
.../content/docs/development/vector_stores.md | 6 ++--
.../docs/development/workflow_agent.md | 27 +++++++-------
.../get-started/quickstart/skills_agent.md | 4 +--
.../get-started/quickstart/workflow_agent.md | 4 +--
docs/content/docs/operations/monitoring.md | 2 +-
.../java/agent_plan_with_python_action.json | 8 ++---
.../python/agent_plan_with_java_action.json | 8 ++---
.../integration/test/AsyncExecutionAgent.java | 11 +++---
.../test/ChatModelIntegrationAgent.java | 5 +--
.../test/EmbeddingIntegrationAgent.java | 3 +-
.../test/FlinkIntegrationAgent.java | 9 ++---
.../integration/test/MemoryObjectAgent.java | 3 +-
.../test/SkillsIntegrationAgent.java | 5 +--
.../test/VectorStoreIntegrationAgent.java | 5 +--
.../test/ChatModelCrossLanguageAgent.java | 5 +--
.../test/EmbeddingCrossLanguageAgent.java | 3 +-
.../resource/test/MCPCrossLanguageAgent.java | 3 +-
.../test/Mem0LongTermMemoryAgent.java | 5 +--
.../test/VectorStoreCrossLanguageAgent.java | 5 +--
.../agents/examples/agents/MathAgent.java | 5 +--
.../agents/ProductSuggestionAgent.java | 5 +--
.../examples/agents/ReviewAnalysisAgent.java | 5 +--
.../agents/TableReviewAnalysisAgent.java | 5 +--
.../apache/flink/agents/plan/AgentPlan.java | 16 ++++-----
.../flink/agents/plan/actions/Action.java | 33 +++++++++++------
.../serializer/ActionJsonDeserializer.java | 15 +++++---
.../plan/serializer/ActionJsonSerializer.java | 8 ++---
.../plan/AgentPlanDeclareChatModelTest.java | 4 +--
.../plan/AgentPlanDeclareMCPServerTest.java | 4 +--
.../plan/AgentPlanDeclareToolFieldTest.java | 4 +--
.../plan/AgentPlanDeclareToolMethodTest.java | 4 +--
.../flink/agents/plan/AgentPlanTest.java | 21 ++++++-----
.../compatibility/GenerateAgentPlanJson.java | 6 ++--
.../serializer/ActionJsonSerializerTest.java | 16 ++++-----
.../AgentPlanJsonSerializerTest.java | 13 ++++---
.../actions/action_java_function.json | 2 +-
.../actions/action_python_function.json | 2 +-
.../resources/agent_plans/agent_plan.json | 4 +--
...t_plan_with_python_resource_providers.json | 8 ++---
.../flink/agents/runtime/RescalingTest.java | 5 +--
.../agents/runtime/ResourceCacheTest.java | 3 +-
.../ShortTermMemoryTTLIntegrationTest.java | 3 +-
49 files changed, 207 insertions(+), 167 deletions(-)
diff --git a/api/src/main/java/org/apache/flink/agents/api/agents/ReActAgent.java b/api/src/main/java/org/apache/flink/agents/api/agents/ReActAgent.java
index 0b394baab..e13661ee9 100644
--- a/api/src/main/java/org/apache/flink/agents/api/agents/ReActAgent.java
+++ b/api/src/main/java/org/apache/flink/agents/api/agents/ReActAgent.java
@@ -23,6 +23,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.ClassUtils;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.annotation.Action;
@@ -169,7 +170,7 @@ public static void startAction(Event event, RunnerContext ctx) {
ctx.sendEvent(new ChatRequestEvent(DEFAULT_CHAT_MODEL, inputMessages, outputSchema));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void stopAction(Event event, RunnerContext ctx) {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
ChatMessage response = chatResponse.getResponse();
diff --git a/api/src/main/java/org/apache/flink/agents/api/annotation/Action.java b/api/src/main/java/org/apache/flink/agents/api/annotation/Action.java
index fd7f62c92..ae11de6b2 100644
--- a/api/src/main/java/org/apache/flink/agents/api/annotation/Action.java
+++ b/api/src/main/java/org/apache/flink/agents/api/annotation/Action.java
@@ -24,23 +24,24 @@
import java.lang.annotation.Target;
/**
- * Annotation for marking a method as an agent action.
+ * Marks a method as an agent action triggered by matching events.
*
- *
This annotation specifies which event types the action should respond to. The annotated method
- * will be triggered when any of the specified event types occur.
+ *
Each {@link #value()} entry is an event type name string. Use the {@code EVENT_TYPE} constants
+ * on built-in event classes, the {@link org.apache.flink.agents.api.EventType} constants, or plain
+ * strings for custom events. Multiple entries combine with OR.
*
- *
Events are specified as type strings via {@link #listenEventTypes()}. Use the {@code
- * EVENT_TYPE} constants on built-in event classes for standard events, or plain strings for custom
- * events.
+ *
{@code
+ * // Built-in event type via the EventType constant
+ * @Action(EventType.InputEvent)
*
- *
Example usage:
+ * // Equivalent via the legacy class constant
+ * @Action(InputEvent.EVENT_TYPE)
*
- *
For a cross-language action, set {@link #target()} to a {@link PythonFunction} with a
@@ -49,7 +50,7 @@
*
*
{@code
* @Action(
- * listenEventTypes = {InputEvent.EVENT_TYPE},
+ * value = EventType.InputEvent,
* target = @PythonFunction(module = "my_pkg.handlers", qualname = "handle_input"))
* public void handleInput(Event event, RunnerContext ctx) {
* throw new UnsupportedOperationException("cross-language stub");
@@ -59,12 +60,8 @@
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
- /**
- * List of event type strings that this action should respond to.
- *
- * @return Array of event type strings
- */
- String[] listenEventTypes();
+ /** Event type name strings; multiple entries have OR semantics. */
+ String[] value();
/**
* Cross-language target. When {@link PythonFunction#module()} is non-empty, dispatch routes to
diff --git a/docs/content/docs/development/chat_models.md b/docs/content/docs/development/chat_models.md
index f6bfe7b77..0e7f40239 100644
--- a/docs/content/docs/development/chat_models.md
+++ b/docs/content/docs/development/chat_models.md
@@ -121,7 +121,7 @@ public class MyAgent extends Agent {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
ChatMessage userMessage =
@@ -129,7 +129,7 @@ public class MyAgent extends Agent {
ctx.sendEvent(new ChatRequestEvent("ollamaChatModel", List.of(userMessage)));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processResponse(Event event, RunnerContext ctx)
throws Exception {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
@@ -1237,7 +1237,7 @@ public class MyAgent extends Agent {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
ChatMessage userMessage =
@@ -1245,7 +1245,7 @@ public class MyAgent extends Agent {
ctx.sendEvent(new ChatRequestEvent("pythonChatModel", List.of(userMessage)));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processResponse(Event event, RunnerContext ctx)
throws Exception {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
diff --git a/docs/content/docs/development/embedding_models.md b/docs/content/docs/development/embedding_models.md
index 3646bcf52..c3f789836 100644
--- a/docs/content/docs/development/embedding_models.md
+++ b/docs/content/docs/development/embedding_models.md
@@ -165,7 +165,7 @@ public class MyAgent extends Agent {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processText(Event event, RunnerContext ctx)
throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
@@ -657,7 +657,7 @@ public class MyAgent extends Agent {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
// Use the Python embedding model from Java
diff --git a/docs/content/docs/development/memory/long_term_memory.md b/docs/content/docs/development/memory/long_term_memory.md
index eeb4c79d7..f3ad2ee74 100644
--- a/docs/content/docs/development/memory/long_term_memory.md
+++ b/docs/content/docs/development/memory/long_term_memory.md
@@ -162,7 +162,7 @@ def process_event(event: Event, ctx: RunnerContext) -> None:
{{< tab "Java" >}}
```java
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+@Action(EventType.InputEvent)
public static void processEvent(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
BaseLongTermMemory ltm = ctx.getLongTermMemory();
@@ -501,7 +501,7 @@ agents_config.set(LongTermMemoryOptions.Mem0.VECTOR_STORE, "my_vector_store")
{{< tab "Java" >}}
```java
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+@Action(EventType.InputEvent)
public static void processEvent(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
BaseLongTermMemory ltm = ctx.getLongTermMemory();
diff --git a/docs/content/docs/development/memory/sensory_and_short_term_memory.md b/docs/content/docs/development/memory/sensory_and_short_term_memory.md
index f3aa38105..34c47b314 100644
--- a/docs/content/docs/development/memory/sensory_and_short_term_memory.md
+++ b/docs/content/docs/development/memory/sensory_and_short_term_memory.md
@@ -103,7 +103,7 @@ def process_event(event: Event, ctx: RunnerContext) -> None:
{{< tab "Java" >}}
```java
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+@Action(EventType.InputEvent)
public static void processEvent(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
MemoryObject memory = ctx.getSensoryMemory(); // ctx.getShortTermMemory();
@@ -225,7 +225,7 @@ def second_action(event: Event, ctx: RunnerContext):
{{< tab "Java" >}}
```java
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+@Action(EventType.InputEvent)
public static void firstAction(Event event, RunnerContext ctx) throws Exception {
...
MemoryObject sensoryMemory = ctx.getSensoryMemory();
@@ -235,7 +235,7 @@ public static void firstAction(Event event, RunnerContext ctx) throws Exception
...
}
-@Action(listenEventTypes = {MyEvent.EVENT_TYPE})
+@Action("MyEvent")
public static void secondAction(Event event, RunnerContext ctx) throws Exception {
MyEvent myEvent = MyEvent.fromEvent(event);
...
diff --git a/docs/content/docs/development/prompts.md b/docs/content/docs/development/prompts.md
index 6bf9e77bb..231b1cf85 100644
--- a/docs/content/docs/development/prompts.md
+++ b/docs/content/docs/development/prompts.md
@@ -308,7 +308,7 @@ public class ReviewAnalysisAgent extends Agent {
}
/** Process input event and send chat request for review analysis. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
String input = (String) inputEvent.getInput();
diff --git a/docs/content/docs/development/vector_stores.md b/docs/content/docs/development/vector_stores.md
index a00b0e359..8124ba91c 100644
--- a/docs/content/docs/development/vector_stores.md
+++ b/docs/content/docs/development/vector_stores.md
@@ -416,7 +416,7 @@ public class MyAgent extends Agent {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void searchDocuments(Event event, RunnerContext ctx) {
InputEvent inputEvent = InputEvent.fromEvent(event);
// Option 1: Manual search via the vector store
@@ -429,7 +429,7 @@ public class MyAgent extends Agent {
ctx.sendEvent(new ContextRetrievalRequestEvent(queryText, "vectorStore"));
}
- @Action(listenEventTypes = {ContextRetrievalResponseEvent.EVENT_TYPE})
+ @Action(EventType.ContextRetrievalResponseEvent)
public static void onSearchResponse(Event event, RunnerContext ctx) {
ContextRetrievalResponseEvent response = ContextRetrievalResponseEvent.fromEvent(event);
List documents = response.getDocuments();
@@ -1096,7 +1096,7 @@ public class MyAgent extends Agent {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
// Use Python vector store from Java
diff --git a/docs/content/docs/development/workflow_agent.md b/docs/content/docs/development/workflow_agent.md
index b8b7e860d..8ba4ae4ec 100644
--- a/docs/content/docs/development/workflow_agent.md
+++ b/docs/content/docs/development/workflow_agent.md
@@ -26,7 +26,7 @@ under the License.
A workflow style agent in Flink-Agents is an agent whose reasoning and behavior are organized as a directed workflow of modular steps, called actions, connected by events. This design is inspired by the need to orchestrate complex, multi-stage tasks in a transparent, extensible, and data-centric way, leveraging Apache Flink's streaming architecture.
-In Flink-Agents, a workflow agent is defined as a class that inherits from the `Agent` base class. The agent's logic is expressed as a set of actions, each of which is a function decorated with `@action(EventType)` in python (or a method annotated with `@Action(listenEventTypes = {})` in java). Actions consume events, perform reasoning or tool calls, and emit new events, which may trigger downstream actions. This event-driven workflow forms a directed cyclic graph of computation, where each node is an action and each edge is an event type.
+In Flink-Agents, a workflow agent is defined as a class that inherits from the `Agent` base class. The agent's logic is expressed as a set of actions, each of which is a function decorated with `@action(EventType.X)` in python (or a method annotated with `@Action(EventType.X)` in java). Actions consume events, perform reasoning or tool calls, and emit new events, which may trigger downstream actions. This event-driven workflow forms a directed cyclic graph of computation, where each node is an action and each edge is an event type.
A workflow agent is well-suited for scenarios where the solution requires explicit orchestration, branching, or multi-step reasoning, such as data enrichment, multi-tool pipelines, or complex business logic.
@@ -176,7 +176,7 @@ public class ReviewAnalysisAgent extends Agent {
}
/** Process input event and send chat request for review analysis. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
String input = (String) inputEvent.getInput();
@@ -197,7 +197,7 @@ public class ReviewAnalysisAgent extends Agent {
"reviewAnalysisModel", List.of(msg), Map.of("input", content), null));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(Event event, RunnerContext ctx)
throws Exception {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
@@ -253,7 +253,7 @@ class ReviewAnalysisAgent(Agent):
```java
public class ReviewAnalysisAgent extends Agent {
/** Process input event and send chat request for review analysis. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
// the action logic
@@ -282,9 +282,10 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
{{< tab "Java" >}}
```java
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
-public static void processInput(Event event, RunnerContext ctx) {
- // send a ChatRequestEvent to trigger the built-in chat-model action
+@Action(EventType.InputEvent)
+public static void processInput(Event event, RunnerContext ctx) throws Exception {
+ InputEvent inputEvent = InputEvent.fromEvent(event);
+ // send ChatRequestEvent
ctx.sendEvent(new ChatRequestEvent("my_model", messages));
}
```
@@ -400,7 +401,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
{{< tab "Java" >}}
Java actions use `DurableCallable` with `ctx.durableExecute(...)`, where `getId()` must be stable and `getResultClass()` supports recovery deserialization.
```java
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+@Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
DurableCallable call = new DurableCallable<>() {
@@ -428,7 +429,7 @@ public static void processInput(Event event, RunnerContext ctx) throws Exception
Java actions can also override `reconciler()` to recover an execution outcome during recovery.
```java
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+@Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
DurableCallable call = new DurableCallable<>() {
@@ -498,7 +499,7 @@ functions like `asyncio.gather`, `asyncio.wait`, `asyncio.create_task`, and
Use `ctx.durableExecuteAsync(DurableCallable)`; on **JDK 21+** it yields using Continuation,
and on **JDK < 21** it falls back to synchronous execution. The same optional `reconciler()` hook can be used for recovery.
```java
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+@Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
DurableCallable call = new DurableCallable<>() {
@@ -557,7 +558,7 @@ class MyAgent(Agent):
```java
public class MyAgent extends Agent {
@Action(
- listenEventTypes = {InputEvent.EVENT_TYPE},
+ value = EventType.InputEvent,
target = @PythonFunction(
module = "my_pkg.handlers",
qualname = "handle_input"))
@@ -614,13 +615,13 @@ def handle_my_event(event: Event, ctx: RunnerContext) -> None:
{{< tab "Java" >}}
```java
// Send a unified event from one action
-@Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+@Action(EventType.InputEvent)
public static void createMyEvent(Event event, RunnerContext ctx) {
ctx.sendEvent(new Event("my_event", Map.of("field1", "test", "field2", 42)));
}
// Consume it in another action
-@Action(listenEventTypes = {"my_event"})
+@Action("my_event")
public static void handleMyEvent(Event event, RunnerContext ctx) {
String field1 = (String) event.getAttr("field1");
int field2 = (int) event.getAttr("field2");
diff --git a/docs/content/docs/get-started/quickstart/skills_agent.md b/docs/content/docs/get-started/quickstart/skills_agent.md
index 9a15b3707..677d8c6aa 100644
--- a/docs/content/docs/get-started/quickstart/skills_agent.md
+++ b/docs/content/docs/get-started/quickstart/skills_agent.md
@@ -167,7 +167,7 @@ public class MathAgent extends Agent {
}
/** Process input event and send a chat request to evaluate the question. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(InputEvent event, RunnerContext ctx) {
ctx.sendEvent(
new ChatRequestEvent(
@@ -177,7 +177,7 @@ public class MathAgent extends Agent {
}
/** Process chat response event and send the answer as output. */
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(ChatResponseEvent event, RunnerContext ctx) {
ctx.sendEvent(new OutputEvent(event.getResponse().getContent()));
}
diff --git a/docs/content/docs/get-started/quickstart/workflow_agent.md b/docs/content/docs/get-started/quickstart/workflow_agent.md
index 3fc3d81da..4e0e97869 100644
--- a/docs/content/docs/get-started/quickstart/workflow_agent.md
+++ b/docs/content/docs/get-started/quickstart/workflow_agent.md
@@ -218,7 +218,7 @@ public class ReviewAnalysisAgent extends Agent {
}
/** Process input event and send chat request for review analysis. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
String input = (String) inputEvent.getInput();
@@ -239,7 +239,7 @@ public class ReviewAnalysisAgent extends Agent {
"reviewAnalysisModel", List.of(msg), Map.of("input", content), null));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(Event event, RunnerContext ctx)
throws Exception {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
diff --git a/docs/content/docs/operations/monitoring.md b/docs/content/docs/operations/monitoring.md
index eb6570237..c24a07051 100644
--- a/docs/content/docs/operations/monitoring.md
+++ b/docs/content/docs/operations/monitoring.md
@@ -88,7 +88,7 @@ class MyAgent(Agent):
```java
public class MyAgent extends Agent {
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void firstAction(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
long startTime = System.currentTimeMillis();
diff --git a/e2e-test/cross-language-agent-plan-snapshots/java/agent_plan_with_python_action.json b/e2e-test/cross-language-agent-plan-snapshots/java/agent_plan_with_python_action.json
index fd662469f..8b6e28867 100644
--- a/e2e-test/cross-language-agent-plan-snapshots/java/agent_plan_with_python_action.json
+++ b/e2e-test/cross-language-agent-plan-snapshots/java/agent_plan_with_python_action.json
@@ -8,7 +8,7 @@
"method_name" : "processChatRequestOrToolResponse",
"parameter_types" : [ "org.apache.flink.agents.api.Event", "org.apache.flink.agents.api.context.RunnerContext" ]
},
- "listen_event_types" : [ "_chat_request_event", "_tool_response_event" ],
+ "trigger_conditions" : [ "_chat_request_event", "_tool_response_event" ],
"config" : null
},
"context_retrieval_action" : {
@@ -19,7 +19,7 @@
"method_name" : "processContextRetrievalRequest",
"parameter_types" : [ "org.apache.flink.agents.api.Event", "org.apache.flink.agents.api.context.RunnerContext" ]
},
- "listen_event_types" : [ "_context_retrieval_request_event" ],
+ "trigger_conditions" : [ "_context_retrieval_request_event" ],
"config" : null
},
"handle" : {
@@ -29,7 +29,7 @@
"module" : "flink_agents.plan.tests.test_agent_plan_cross_language",
"qualname" : "_dummy_action"
},
- "listen_event_types" : [ "_input_event" ],
+ "trigger_conditions" : [ "_input_event" ],
"config" : null
},
"tool_call_action" : {
@@ -40,7 +40,7 @@
"method_name" : "processToolRequest",
"parameter_types" : [ "org.apache.flink.agents.api.Event", "org.apache.flink.agents.api.context.RunnerContext" ]
},
- "listen_event_types" : [ "_tool_request_event" ],
+ "trigger_conditions" : [ "_tool_request_event" ],
"config" : null
}
},
diff --git a/e2e-test/cross-language-agent-plan-snapshots/python/agent_plan_with_java_action.json b/e2e-test/cross-language-agent-plan-snapshots/python/agent_plan_with_java_action.json
index dda6405d1..19dee444a 100644
--- a/e2e-test/cross-language-agent-plan-snapshots/python/agent_plan_with_java_action.json
+++ b/e2e-test/cross-language-agent-plan-snapshots/python/agent_plan_with_java_action.json
@@ -11,7 +11,7 @@
"org.apache.flink.agents.api.context.RunnerContext"
]
},
- "listen_event_types": [
+ "trigger_conditions": [
"_input_event"
],
"config": null
@@ -23,7 +23,7 @@
"module": "flink_agents.plan.actions.chat_model_action",
"qualname": "process_chat_request_or_tool_response"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_chat_request_event",
"_tool_response_event"
],
@@ -36,7 +36,7 @@
"module": "flink_agents.plan.actions.tool_call_action",
"qualname": "process_tool_request"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_tool_request_event"
],
"config": null
@@ -48,7 +48,7 @@
"module": "flink_agents.plan.actions.context_retrieval_action",
"qualname": "process_context_retrieval_request"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_context_retrieval_request_event"
],
"config": null
diff --git a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/AsyncExecutionAgent.java b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/AsyncExecutionAgent.java
index 6d7fbd26f..c8df332f5 100644
--- a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/AsyncExecutionAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/AsyncExecutionAgent.java
@@ -18,6 +18,7 @@
package org.apache.flink.agents.integration.test;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -94,7 +95,7 @@ public String getProcessedResult() {
*/
public static class SimpleAsyncAgent extends Agent {
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
AsyncRequest request = (AsyncRequest) inputEvent.getInput();
@@ -135,7 +136,7 @@ public String call() {
* @param event The processed event
* @param ctx The runner context for sending events
*/
- @Action(listenEventTypes = {AsyncProcessedEvent.EVENT_TYPE})
+ @Action(AsyncProcessedEvent.EVENT_TYPE)
public static void generateOutput(Event event, RunnerContext ctx) throws Exception {
AsyncProcessedEvent processedEvent = (AsyncProcessedEvent) event;
@@ -153,7 +154,7 @@ public static void generateOutput(Event event, RunnerContext ctx) throws Excepti
/** Agent that chains multiple durableExecuteAsync calls. */
public static class MultiAsyncAgent extends Agent {
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processWithMultipleAsync(Event event, RunnerContext ctx)
throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
@@ -261,7 +262,7 @@ public String getTimestampDir() {
return timestampDir;
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processWithTiming(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
AsyncRequest request = (AsyncRequest) inputEvent.getInput();
@@ -303,7 +304,7 @@ public String call() {
/** Agent that uses durableExecute (sync) for simulating slow operations. */
public static class SyncDurableAgent extends Agent {
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInputSync(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
AsyncRequest request = (AsyncRequest) inputEvent.getInput();
diff --git a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/ChatModelIntegrationAgent.java b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/ChatModelIntegrationAgent.java
index 4492a8f48..ee64d1dff 100644
--- a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/ChatModelIntegrationAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/ChatModelIntegrationAgent.java
@@ -19,6 +19,7 @@
package org.apache.flink.agents.integration.test;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -217,7 +218,7 @@ public static double createRandomNumber() {
return Math.random();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void process(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
ctx.sendEvent(
@@ -228,7 +229,7 @@ public static void process(Event event, RunnerContext ctx) throws Exception {
MessageRole.USER, (String) inputEvent.getInput()))));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(Event event, RunnerContext ctx) {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
ctx.sendEvent(new OutputEvent(chatResponse.getResponse().getContent()));
diff --git a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/EmbeddingIntegrationAgent.java b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/EmbeddingIntegrationAgent.java
index 08fa3a1cc..2167d1332 100644
--- a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/EmbeddingIntegrationAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/EmbeddingIntegrationAgent.java
@@ -20,6 +20,7 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -130,7 +131,7 @@ public static float validateSimilarityCalculation(
}
/** Main test action that processes input and validates embedding generation. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void testEmbeddingGeneration(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
String input = (String) inputEvent.getInput();
diff --git a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/FlinkIntegrationAgent.java b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/FlinkIntegrationAgent.java
index 47cbd5924..bacb4aa0c 100644
--- a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/FlinkIntegrationAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/FlinkIntegrationAgent.java
@@ -18,6 +18,7 @@
package org.apache.flink.agents.integration.test;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -89,7 +90,7 @@ public static class DataStreamAgent extends Agent {
* @param event The input event to process
* @param ctx The runner context for sending events
*/
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
ItemData item = (ItemData) inputEvent.getInput();
@@ -114,7 +115,7 @@ public static void processInput(Event event, RunnerContext ctx) throws Exception
* @param event The processed event
* @param ctx The runner context for sending events
*/
- @Action(listenEventTypes = {ProcessedEvent.EVENT_TYPE})
+ @Action(ProcessedEvent.EVENT_TYPE)
public static void generateOutput(Event event, RunnerContext ctx) throws Exception {
ProcessedEvent processedEvent = (ProcessedEvent) event;
MemoryRef itemRef = processedEvent.getItemRef();
@@ -143,7 +144,7 @@ public static class TableAgent extends Agent {
* @param event The input event to process
* @param ctx The runner context for sending events
*/
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
Object input = inputEvent.getInput();
@@ -168,7 +169,7 @@ public static void processInput(Event event, RunnerContext ctx) throws Exception
* @param event The processed event
* @param ctx The runner context for sending events
*/
- @Action(listenEventTypes = {ProcessedEvent.EVENT_TYPE})
+ @Action(ProcessedEvent.EVENT_TYPE)
public static void generateOutput(Event event, RunnerContext ctx) throws Exception {
ProcessedEvent processedEvent = (ProcessedEvent) event;
MemoryRef inputRef = processedEvent.getItemRef();
diff --git a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/MemoryObjectAgent.java b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/MemoryObjectAgent.java
index d8ec4e7f0..094daf270 100644
--- a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/MemoryObjectAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/MemoryObjectAgent.java
@@ -18,6 +18,7 @@
package org.apache.flink.agents.integration.test;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -76,7 +77,7 @@ public int hashCode() {
}
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void testMemoryObject(Event event, RunnerContext ctx) throws Exception {
MemoryObject stm = ctx.getShortTermMemory();
MemoryObject sm = ctx.getSensoryMemory();
diff --git a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/SkillsIntegrationAgent.java b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/SkillsIntegrationAgent.java
index b35338242..858f58d1a 100644
--- a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/SkillsIntegrationAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/SkillsIntegrationAgent.java
@@ -18,6 +18,7 @@
package org.apache.flink.agents.integration.test;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -103,7 +104,7 @@ public static org.apache.flink.agents.api.prompt.Prompt systemPrompt() {
+ "first and strictly follow the instructions of the skill.")));
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void process(InputEvent event, RunnerContext ctx) throws Exception {
ctx.sendEvent(
new ChatRequestEvent(
@@ -112,7 +113,7 @@ public static void process(InputEvent event, RunnerContext ctx) throws Exception
new ChatMessage(MessageRole.USER, (String) event.getInput()))));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(ChatResponseEvent event, RunnerContext ctx) {
ctx.sendEvent(new OutputEvent(event.getResponse().getContent()));
}
diff --git a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/VectorStoreIntegrationAgent.java b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/VectorStoreIntegrationAgent.java
index 9740eddb3..ad48d5eac 100644
--- a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/VectorStoreIntegrationAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/VectorStoreIntegrationAgent.java
@@ -19,6 +19,7 @@
package org.apache.flink.agents.integration.test;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -93,7 +94,7 @@ public static ResourceDescriptor vectorStore() {
}
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void inputEvent(Event event, RunnerContext ctx) {
InputEvent inputEvent = InputEvent.fromEvent(event);
final String input = (String) inputEvent.getInput();
@@ -101,7 +102,7 @@ public static void inputEvent(Event event, RunnerContext ctx) {
ctx.sendEvent(new ContextRetrievalRequestEvent(input, "vectorStore"));
}
- @Action(listenEventTypes = {ContextRetrievalResponseEvent.EVENT_TYPE})
+ @Action(EventType.ContextRetrievalResponseEvent)
public static void contextRetrievalResponseEvent(Event event, RunnerContext ctx) {
ContextRetrievalResponseEvent responseEvent =
ContextRetrievalResponseEvent.fromEvent(event);
diff --git a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/ChatModelCrossLanguageAgent.java b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/ChatModelCrossLanguageAgent.java
index dec657042..a2d78ecfb 100644
--- a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/ChatModelCrossLanguageAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/ChatModelCrossLanguageAgent.java
@@ -19,6 +19,7 @@
package org.apache.flink.agents.resource.test;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -148,7 +149,7 @@ public static double createRandomNumber() {
return Math.random();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void process(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
String model;
@@ -166,7 +167,7 @@ public static void process(Event event, RunnerContext ctx) throws Exception {
MessageRole.USER, (String) inputEvent.getInput()))));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(Event event, RunnerContext ctx) {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
ctx.sendEvent(new OutputEvent(chatResponse.getResponse().getContent()));
diff --git a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/EmbeddingCrossLanguageAgent.java b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/EmbeddingCrossLanguageAgent.java
index 84ed246c1..be5f8d494 100644
--- a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/EmbeddingCrossLanguageAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/EmbeddingCrossLanguageAgent.java
@@ -20,6 +20,7 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -68,7 +69,7 @@ public static ResourceDescriptor embeddingModel() {
}
/** Main test action that processes input and validates embedding generation. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void testEmbeddingGeneration(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
String input = (String) inputEvent.getInput();
diff --git a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/MCPCrossLanguageAgent.java b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/MCPCrossLanguageAgent.java
index 03893eda9..30b921e8c 100644
--- a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/MCPCrossLanguageAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/MCPCrossLanguageAgent.java
@@ -18,6 +18,7 @@
package org.apache.flink.agents.resource.test;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -47,7 +48,7 @@ public static ResourceDescriptor pythonMCPServer() {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void process(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
Map testResult = new HashMap<>();
diff --git a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/Mem0LongTermMemoryAgent.java b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/Mem0LongTermMemoryAgent.java
index 9a7655b61..04e1970ae 100644
--- a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/Mem0LongTermMemoryAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/Mem0LongTermMemoryAgent.java
@@ -19,6 +19,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -172,7 +173,7 @@ public static ResourceDescriptor milvusLtmStore() {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void addItems(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
ItemData input = (ItemData) inputEvent.getInput();
@@ -213,7 +214,7 @@ public Void call() throws Exception {
}
@SuppressWarnings("unchecked")
- @Action(listenEventTypes = {MyEvent})
+ @Action(MyEvent)
public static void retrieveItems(Event event, RunnerContext ctx) throws Exception {
Map record = (Map) event.getAttr("value");
record.put("timestamp_second_action", Instant.now().toString());
diff --git a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/VectorStoreCrossLanguageAgent.java b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/VectorStoreCrossLanguageAgent.java
index cacf62444..5518978ab 100644
--- a/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/VectorStoreCrossLanguageAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-resource-cross-language/src/test/java/org/apache/flink/agents/resource/test/VectorStoreCrossLanguageAgent.java
@@ -19,6 +19,7 @@
package org.apache.flink.agents.resource.test;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -106,7 +107,7 @@ public static ResourceDescriptor vectorStore() {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void inputEvent(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
final String input = (String) inputEvent.getInput();
@@ -203,7 +204,7 @@ public static void inputEvent(Event event, RunnerContext ctx) throws Exception {
ctx.sendEvent(new ContextRetrievalRequestEvent(input, "vectorStore"));
}
- @Action(listenEventTypes = {ContextRetrievalResponseEvent.EVENT_TYPE})
+ @Action(EventType.ContextRetrievalResponseEvent)
public static void contextRetrievalResponseEvent(Event event, RunnerContext ctx) {
ContextRetrievalResponseEvent responseEvent =
ContextRetrievalResponseEvent.fromEvent(event);
diff --git a/examples/src/main/java/org/apache/flink/agents/examples/agents/MathAgent.java b/examples/src/main/java/org/apache/flink/agents/examples/agents/MathAgent.java
index 1056f877b..ba1ece089 100644
--- a/examples/src/main/java/org/apache/flink/agents/examples/agents/MathAgent.java
+++ b/examples/src/main/java/org/apache/flink/agents/examples/agents/MathAgent.java
@@ -17,6 +17,7 @@
*/
package org.apache.flink.agents.examples.agents;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -84,7 +85,7 @@ public static ResourceDescriptor mathModel() {
}
/** Process input event and send a chat request to evaluate the question. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(InputEvent event, RunnerContext ctx) {
ctx.sendEvent(
new ChatRequestEvent(
@@ -94,7 +95,7 @@ public static void processInput(InputEvent event, RunnerContext ctx) {
}
/** Process chat response event and send the answer as output. */
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(ChatResponseEvent event, RunnerContext ctx) {
ctx.sendEvent(new OutputEvent(event.getResponse().getContent()));
}
diff --git a/examples/src/main/java/org/apache/flink/agents/examples/agents/ProductSuggestionAgent.java b/examples/src/main/java/org/apache/flink/agents/examples/agents/ProductSuggestionAgent.java
index 2d1e12225..8d5d0b90f 100644
--- a/examples/src/main/java/org/apache/flink/agents/examples/agents/ProductSuggestionAgent.java
+++ b/examples/src/main/java/org/apache/flink/agents/examples/agents/ProductSuggestionAgent.java
@@ -21,6 +21,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -72,7 +73,7 @@ public static org.apache.flink.agents.api.prompt.Prompt productSuggestionPrompt(
}
/** Process input event. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
String input = (String) inputEvent.getInput();
@@ -95,7 +96,7 @@ public static void processInput(Event event, RunnerContext ctx) throws Exception
}
/** Process chat response event. */
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(Event event, RunnerContext ctx) throws Exception {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
// Fail fast on a malformed LLM response: a parse error here propagates and fails the
diff --git a/examples/src/main/java/org/apache/flink/agents/examples/agents/ReviewAnalysisAgent.java b/examples/src/main/java/org/apache/flink/agents/examples/agents/ReviewAnalysisAgent.java
index 7fbfe752a..2862e0ae4 100644
--- a/examples/src/main/java/org/apache/flink/agents/examples/agents/ReviewAnalysisAgent.java
+++ b/examples/src/main/java/org/apache/flink/agents/examples/agents/ReviewAnalysisAgent.java
@@ -21,6 +21,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -87,7 +88,7 @@ public static void notifyShippingManager(
}
/** Process input event and send chat request for review analysis. */
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
String input = (String) inputEvent.getInput();
@@ -108,7 +109,7 @@ public static void processInput(Event event, RunnerContext ctx) throws Exception
"reviewAnalysisModel", List.of(msg), Map.of("input", content), null));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(Event event, RunnerContext ctx) throws Exception {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
// Fail fast on a malformed LLM response: a parse error here propagates and fails the
diff --git a/examples/src/main/java/org/apache/flink/agents/examples/agents/TableReviewAnalysisAgent.java b/examples/src/main/java/org/apache/flink/agents/examples/agents/TableReviewAnalysisAgent.java
index 18d754be2..a692b3e9b 100644
--- a/examples/src/main/java/org/apache/flink/agents/examples/agents/TableReviewAnalysisAgent.java
+++ b/examples/src/main/java/org/apache/flink/agents/examples/agents/TableReviewAnalysisAgent.java
@@ -20,6 +20,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -108,7 +109,7 @@ public static void notifyShippingManager(
*
When using {@code fromTable()}, the input is a {@link Row} with fields matching the table
* column names.
*/
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void processInput(Event event, RunnerContext ctx) throws Exception {
InputEvent inputEvent = InputEvent.fromEvent(event);
Row row = (Row) inputEvent.getInput();
@@ -128,7 +129,7 @@ public static void processInput(Event event, RunnerContext ctx) throws Exception
"reviewAnalysisModel", List.of(msg), Map.of("input", content), null));
}
- @Action(listenEventTypes = {ChatResponseEvent.EVENT_TYPE})
+ @Action(EventType.ChatResponseEvent)
public static void processChatResponse(Event event, RunnerContext ctx) throws Exception {
ChatResponseEvent chatResponse = ChatResponseEvent.fromEvent(event);
// Fail fast on a malformed LLM response: a parse error here propagates and fails the
diff --git a/plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java b/plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java
index 1bd647764..e9d59083a 100644
--- a/plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java
+++ b/plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java
@@ -184,27 +184,27 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE
private void extractActions(
String actionName,
- String[] listenEventTypeStrings,
+ String[] triggerEntries,
org.apache.flink.agents.plan.Function function,
Map config)
throws Exception {
- List eventTypeNames = new ArrayList<>(Arrays.asList(listenEventTypeStrings));
+ List triggerConditions = new ArrayList<>(Arrays.asList(triggerEntries));
- if (eventTypeNames.isEmpty()) {
+ if (triggerConditions.isEmpty()) {
throw new IllegalArgumentException(
"Action "
+ actionName
- + " must specify at least one event type via listenEventTypes.");
+ + " must specify at least one trigger entry via @Action(value = ...).");
}
// Create an Action
- Action action = new Action(actionName, function, eventTypeNames, config);
+ Action action = new Action(actionName, function, triggerConditions, config);
// Add to actions map
actions.put(action.getName(), action);
// Add to actionsByEvent map
- for (String eventTypeName : eventTypeNames) {
+ for (String eventTypeName : triggerConditions) {
actionsByEvent.computeIfAbsent(eventTypeName, k -> new ArrayList<>()).add(action);
}
}
@@ -251,7 +251,7 @@ private void extractActionsFromAgent(Agent agent) throws Exception {
Objects.requireNonNull(
method.getAnnotation(
org.apache.flink.agents.api.annotation.Action.class));
- String[] listenEventTypeStrings = actionAnnotation.listenEventTypes();
+ String[] triggerEntries = actionAnnotation.value();
org.apache.flink.agents.api.annotation.PythonFunction target =
actionAnnotation.target();
String targetModule = target.module();
@@ -276,7 +276,7 @@ private void extractActionsFromAgent(Agent agent) throws Exception {
+ method.getName()
+ "' must set both module and qualname");
}
- extractActions(method.getName(), listenEventTypeStrings, execFunction, null);
+ extractActions(method.getName(), triggerEntries, execFunction, null);
}
for (Map.Entry<
diff --git a/plan/src/main/java/org/apache/flink/agents/plan/actions/Action.java b/plan/src/main/java/org/apache/flink/agents/plan/actions/Action.java
index 859795a69..18e3b83b4 100644
--- a/plan/src/main/java/org/apache/flink/agents/plan/actions/Action.java
+++ b/plan/src/main/java/org/apache/flink/agents/plan/actions/Action.java
@@ -33,17 +33,17 @@
import java.util.Objects;
/**
- * Representation of an agent action with event listening and function execution.
+ * Representation of an agent action with unified trigger conditions.
*
- *
This class encapsulates a named agent action that listens for specific event types and
- * executes an associated function when those events occur.
+ *
Each entry of {@code triggerConditions} is an event type name string. Multiple entries combine
+ * with OR.
*/
@JsonSerialize(using = ActionJsonSerializer.class)
@JsonDeserialize(using = ActionJsonDeserializer.class)
public class Action {
private final String name;
private final Function exec;
- private final List listenEventTypes;
+ private final List triggerConditions;
// TODO: support nested map/list with non primitive type value.
@Nullable private final Map config;
@@ -51,18 +51,18 @@ public class Action {
public Action(
String name,
Function exec,
- List listenEventTypes,
+ List triggerConditions,
@Nullable Map config)
throws Exception {
this.name = name;
this.exec = exec;
- this.listenEventTypes = listenEventTypes;
+ this.triggerConditions = triggerConditions;
this.config = config;
exec.checkSignature(new Class[] {Event.class, RunnerContext.class});
}
- public Action(String name, Function exec, List listenEventTypes) throws Exception {
- this(name, exec, listenEventTypes, null);
+ public Action(String name, Function exec, List triggerConditions) throws Exception {
+ this(name, exec, triggerConditions, null);
}
public String getName() {
@@ -73,8 +73,19 @@ public Function getExec() {
return exec;
}
+ /** Returns the full trigger conditions list. */
+ public List getTriggerConditions() {
+ return triggerConditions;
+ }
+
+ /**
+ * Returns event-type names. Kept for callers that still consume the old naming; in this PR all
+ * trigger entries are plain event-type names so the list is identical to {@link
+ * #getTriggerConditions()}. A follow-up PR introduces CEL expressions and overrides this to
+ * filter out non-type entries.
+ */
public List getListenEventTypes() {
- return listenEventTypes;
+ return triggerConditions;
}
@Nullable
@@ -89,11 +100,11 @@ public boolean equals(Object o) {
Action other = (Action) o;
return name.equals(other.name)
&& exec.equals(other.exec)
- && listenEventTypes.equals(other.listenEventTypes);
+ && Objects.equals(triggerConditions, other.triggerConditions);
}
@Override
public int hashCode() {
- return Objects.hash(name, exec, listenEventTypes);
+ return Objects.hash(name, exec, triggerConditions);
}
}
diff --git a/plan/src/main/java/org/apache/flink/agents/plan/serializer/ActionJsonDeserializer.java b/plan/src/main/java/org/apache/flink/agents/plan/serializer/ActionJsonDeserializer.java
index fec2f126f..c6a87fa46 100644
--- a/plan/src/main/java/org/apache/flink/agents/plan/serializer/ActionJsonDeserializer.java
+++ b/plan/src/main/java/org/apache/flink/agents/plan/serializer/ActionJsonDeserializer.java
@@ -66,10 +66,15 @@ public Action deserialize(JsonParser jsonParser, DeserializationContext deserial
throw new IOException("Unsupported function type: " + funcType);
}
- // Deserialize listenEventTypes
- List listenEventTypes = new ArrayList<>();
- node.get("listen_event_types")
- .forEach(eventTypeNode -> listenEventTypes.add(eventTypeNode.asText()));
+ // Deserialize trigger_conditions (fall back to legacy listen_event_types for older JSONs)
+ List triggerConditions = new ArrayList<>();
+ JsonNode triggerNode = node.get("trigger_conditions");
+ if (triggerNode == null) {
+ triggerNode = node.get("listen_event_types");
+ }
+ if (triggerNode != null) {
+ triggerNode.forEach(entryNode -> triggerConditions.add(entryNode.asText()));
+ }
// Deserialize params
Map config = null;
@@ -88,7 +93,7 @@ public Action deserialize(JsonParser jsonParser, DeserializationContext deserial
}
try {
- return new Action(name, func, listenEventTypes, config);
+ return new Action(name, func, triggerConditions, config);
} catch (Exception e) {
throw new RuntimeException(
String.format("Failed to create Action with name \"%s\"", name), e);
diff --git a/plan/src/main/java/org/apache/flink/agents/plan/serializer/ActionJsonSerializer.java b/plan/src/main/java/org/apache/flink/agents/plan/serializer/ActionJsonSerializer.java
index 77581dadf..85e46823e 100644
--- a/plan/src/main/java/org/apache/flink/agents/plan/serializer/ActionJsonSerializer.java
+++ b/plan/src/main/java/org/apache/flink/agents/plan/serializer/ActionJsonSerializer.java
@@ -62,11 +62,11 @@ public void serialize(
"Unsupported function type: " + action.getExec().getClass().getName());
}
- // Write listenEventTypes field
- jsonGenerator.writeFieldName("listen_event_types");
+ // Write trigger_conditions field
+ jsonGenerator.writeFieldName("trigger_conditions");
jsonGenerator.writeStartArray();
- for (String eventType : action.getListenEventTypes()) {
- jsonGenerator.writeString(eventType);
+ for (String entry : action.getTriggerConditions()) {
+ jsonGenerator.writeString(entry);
}
jsonGenerator.writeEndArray();
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareChatModelTest.java b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareChatModelTest.java
index 0ac51609e..561b2b5c6 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareChatModelTest.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareChatModelTest.java
@@ -22,7 +22,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
-import org.apache.flink.agents.api.InputEvent;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.agents.Agent;
import org.apache.flink.agents.api.annotation.Action;
import org.apache.flink.agents.api.annotation.ChatModelSetup;
@@ -85,7 +85,7 @@ public static ResourceDescriptor testChatModel() {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public void onInput(Event e, RunnerContext ctx) {
// no-op for this test; validates action registration signature
}
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareMCPServerTest.java b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareMCPServerTest.java
index 719f195f5..70c8f7aa2 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareMCPServerTest.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareMCPServerTest.java
@@ -20,7 +20,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
-import org.apache.flink.agents.api.InputEvent;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.agents.Agent;
import org.apache.flink.agents.api.annotation.Action;
import org.apache.flink.agents.api.context.RunnerContext;
@@ -80,7 +80,7 @@ public static ResourceDescriptor testMcpServer() {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public void process(Event event, RunnerContext ctx) {
// no-op
}
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareToolFieldTest.java b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareToolFieldTest.java
index c87cc53f3..76f4f3ca5 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareToolFieldTest.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareToolFieldTest.java
@@ -21,7 +21,7 @@
package org.apache.flink.agents.plan;
import org.apache.flink.agents.api.Event;
-import org.apache.flink.agents.api.InputEvent;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.agents.Agent;
import org.apache.flink.agents.api.annotation.Action;
import org.apache.flink.agents.api.annotation.ToolParam;
@@ -86,7 +86,7 @@ static class TestAgent extends Agent {
@org.apache.flink.agents.api.annotation.Tool
private final Tool weather = createWeatherTool();
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public void onInput(Event e, RunnerContext ctx) {
/* no-op */
}
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareToolMethodTest.java b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareToolMethodTest.java
index a3784be59..a24cfbc76 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareToolMethodTest.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanDeclareToolMethodTest.java
@@ -22,7 +22,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
-import org.apache.flink.agents.api.InputEvent;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.agents.Agent;
import org.apache.flink.agents.api.annotation.Action;
import org.apache.flink.agents.api.annotation.ToolParam;
@@ -84,7 +84,7 @@ public static String getWeather(
location, temp, "fahrenheit".equals(units) ? "F" : "C");
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public void process(Event event, RunnerContext ctx) {
// no-op
}
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanTest.java b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanTest.java
index acdf14343..230eb75f2 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanTest.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/AgentPlanTest.java
@@ -19,6 +19,7 @@
package org.apache.flink.agents.plan;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -126,13 +127,15 @@ public Object getPythonResource() {
/** Test agent class with annotated methods. */
public static class TestAgent extends Agent {
- @org.apache.flink.agents.api.annotation.Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action(EventType.InputEvent)
public void handleInputEvent(Event event, RunnerContext context) {
InputEvent inputEvent = InputEvent.fromEvent(event);
}
- @org.apache.flink.agents.api.annotation.Action(
- listenEventTypes = {TestEvent.EVENT_TYPE, OutputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action({
+ TestEvent.EVENT_TYPE,
+ EventType.OutputEvent
+ })
public void handleMultipleEvents(Event event, RunnerContext context) {
// Test action implementation
}
@@ -161,7 +164,7 @@ public static ResourceDescriptor pythonChatModel() {
@Tool private TestTool anotherTool = new TestTool("anotherTool");
- @org.apache.flink.agents.api.annotation.Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action(EventType.InputEvent)
public void handleInputEvent(Event event, RunnerContext context) {
InputEvent inputEvent = InputEvent.fromEvent(event);
}
@@ -262,7 +265,7 @@ public void testBuiltInActionsAreJavaNativeAfterCompile() throws Exception {
/** Cross-language action via {@code @Action(target = @PythonFunction(...))}. */
public static class AgentWithCrossLanguageAction extends Agent {
@org.apache.flink.agents.api.annotation.Action(
- listenEventTypes = {InputEvent.EVENT_TYPE},
+ value = EventType.InputEvent,
target =
@org.apache.flink.agents.api.annotation.PythonFunction(
module = "my_pkg.handlers",
@@ -291,7 +294,7 @@ public void testActionWithPythonTargetCompilesToPythonFunctionExec() throws Exce
/** Plain {@code @Action} (no {@code target}) compiles to a native Java exec. */
public static class AgentWithNativeJavaAction extends Agent {
- @org.apache.flink.agents.api.annotation.Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action(EventType.InputEvent)
public static void handle(Event event, RunnerContext ctx) {
// intentionally empty
}
@@ -311,7 +314,7 @@ public void testActionWithEmptyTargetCompilesToJavaFunctionExec() throws Excepti
/** Partially-set target (module without qualname) — must be rejected at compile. */
public static class AgentWithHalfSetPythonTargetMissingQualname extends Agent {
@org.apache.flink.agents.api.annotation.Action(
- listenEventTypes = {InputEvent.EVENT_TYPE},
+ value = EventType.InputEvent,
target = @org.apache.flink.agents.api.annotation.PythonFunction(module = "pkg"))
public static void handle(Event event, RunnerContext ctx) {
throw new UnsupportedOperationException("cross-language stub");
@@ -329,7 +332,7 @@ public void testActionWithPythonTargetMissingQualnameIsRejected() {
/** Partially-set target (qualname without module) — must be rejected at compile. */
public static class AgentWithHalfSetPythonTargetMissingModule extends Agent {
@org.apache.flink.agents.api.annotation.Action(
- listenEventTypes = {InputEvent.EVENT_TYPE},
+ value = EventType.InputEvent,
target =
@org.apache.flink.agents.api.annotation.PythonFunction(
qualname = "handle_input"))
@@ -350,7 +353,7 @@ public void testActionWithPythonTargetMissingModuleIsRejected() {
* @Action declared on a parent agent class — must be rejected loudly, not silently dropped.
*/
public abstract static class BaseAgentWithInheritedAction extends Agent {
- @org.apache.flink.agents.api.annotation.Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action(EventType.InputEvent)
public static void sharedAction(Event event, RunnerContext ctx) {
throw new UnsupportedOperationException("test stub");
}
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/compatibility/GenerateAgentPlanJson.java b/plan/src/test/java/org/apache/flink/agents/plan/compatibility/GenerateAgentPlanJson.java
index 5051966ed..e657378d9 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/compatibility/GenerateAgentPlanJson.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/compatibility/GenerateAgentPlanJson.java
@@ -20,7 +20,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
-import org.apache.flink.agents.api.InputEvent;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.agents.Agent;
import org.apache.flink.agents.api.annotation.Action;
import org.apache.flink.agents.api.context.RunnerContext;
@@ -48,12 +48,12 @@ public MyEvent() {
/** Agent class for generating java agent plan json. */
public static class JavaAgentPlanCompatibilityTestAgent extends Agent {
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public void firstAction(Event event, RunnerContext context) {
// Test action implementation
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE, MyEvent.EVENT_TYPE})
+ @Action({EventType.InputEvent, MyEvent.EVENT_TYPE})
public void secondAction(Event event, RunnerContext context) {
// Test action implementation
}
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/serializer/ActionJsonSerializerTest.java b/plan/src/test/java/org/apache/flink/agents/plan/serializer/ActionJsonSerializerTest.java
index 6247b88c3..3f25cc127 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/serializer/ActionJsonSerializerTest.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/serializer/ActionJsonSerializerTest.java
@@ -68,8 +68,8 @@ public void testSerializeJavaFunction() throws Exception {
assertTrue(
json.contains("\"method_name\":\"legal\""), "JSON should contain the method name");
assertTrue(
- json.contains("\"listen_event_types\":["),
- "JSON should contain the listen event types");
+ json.contains("\"trigger_conditions\":["),
+ "JSON should contain the trigger conditions");
assertTrue(
json.contains("\"" + InputEvent.EVENT_TYPE + "\""),
"JSON should contain the event type string");
@@ -103,8 +103,8 @@ public void testSerializePythonFunction() throws Exception {
json.contains("\"qualname\":\"test_function\""),
"JSON should contain the qualified name");
assertTrue(
- json.contains("\"listen_event_types\":["),
- "JSON should contain the listen event types");
+ json.contains("\"trigger_conditions\":["),
+ "JSON should contain the trigger conditions");
assertTrue(
json.contains("\"" + InputEvent.EVENT_TYPE + "\""),
"JSON should contain the event type string");
@@ -133,8 +133,8 @@ public void testSerializeMultipleEventTypes() throws Exception {
json.contains("\"name\":\"multiEventAction\""),
"JSON should contain the action name");
assertTrue(
- json.contains("\"listen_event_types\":["),
- "JSON should contain the listen event types");
+ json.contains("\"trigger_conditions\":["),
+ "JSON should contain the trigger conditions");
assertTrue(
json.contains("\"" + InputEvent.EVENT_TYPE + "\""),
"JSON should contain the InputEvent type string");
@@ -166,8 +166,8 @@ public void testSerializeEmptyEventTypes() throws Exception {
json.contains("\"name\":\"emptyEventsAction\""),
"JSON should contain the action name");
assertTrue(
- json.contains("\"listen_event_types\":[]"),
- "JSON should contain an empty listen event types array");
+ json.contains("\"trigger_conditions\":[]"),
+ "JSON should contain an empty trigger conditions array");
}
@Test
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/serializer/AgentPlanJsonSerializerTest.java b/plan/src/test/java/org/apache/flink/agents/plan/serializer/AgentPlanJsonSerializerTest.java
index 2b9a91484..c8ba012b1 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/serializer/AgentPlanJsonSerializerTest.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/serializer/AgentPlanJsonSerializerTest.java
@@ -20,6 +20,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -42,18 +43,20 @@ public class AgentPlanJsonSerializerTest {
/** Test Agent class with @Action annotated methods. */
public static class TestAgent extends Agent {
- @org.apache.flink.agents.api.annotation.Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action(EventType.InputEvent)
public void handleInputEvent(Event event, RunnerContext context) {
InputEvent inputEvent = InputEvent.fromEvent(event);
}
- @org.apache.flink.agents.api.annotation.Action(listenEventTypes = {OutputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action(EventType.OutputEvent)
public void processOutputEvent(Event event, RunnerContext context) {
OutputEvent outputEvent = OutputEvent.fromEvent(event);
}
- @org.apache.flink.agents.api.annotation.Action(
- listenEventTypes = {InputEvent.EVENT_TYPE, OutputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action({
+ EventType.InputEvent,
+ EventType.OutputEvent
+ })
public void handleMultipleEvents(Event event, RunnerContext context) {
// Test action logic for multiple event types
}
@@ -94,7 +97,7 @@ public void testSerializeAgentPlanWithActions() throws Exception {
assertThat(json).contains("\"func_type\":\"JavaFunction\"");
assertThat(json).contains("\"qualname\":\"org.apache.flink.agents.plan.TestAction\"");
assertThat(json).contains("\"method_name\":\"legal\"");
- assertThat(json).contains("\"listen_event_types\":[");
+ assertThat(json).contains("\"trigger_conditions\":[");
assertThat(json).contains("\"" + InputEvent.EVENT_TYPE + "\"");
assertThat(json).contains("\"actions_by_event\":{}");
}
diff --git a/plan/src/test/resources/actions/action_java_function.json b/plan/src/test/resources/actions/action_java_function.json
index 35fcbeedd..6b73f324a 100644
--- a/plan/src/test/resources/actions/action_java_function.json
+++ b/plan/src/test/resources/actions/action_java_function.json
@@ -6,5 +6,5 @@
"method_name": "legal",
"parameter_types": ["org.apache.flink.agents.api.Event", "org.apache.flink.agents.api.context.RunnerContext"]
},
- "listen_event_types": ["_input_event"]
+ "trigger_conditions": ["_input_event"]
}
diff --git a/plan/src/test/resources/actions/action_python_function.json b/plan/src/test/resources/actions/action_python_function.json
index 70fdf3aca..5711b873d 100644
--- a/plan/src/test/resources/actions/action_python_function.json
+++ b/plan/src/test/resources/actions/action_python_function.json
@@ -5,5 +5,5 @@
"module": "test_module",
"qualname": "test_function"
},
- "listen_event_types": ["_input_event"]
+ "trigger_conditions": ["_input_event"]
}
diff --git a/plan/src/test/resources/agent_plans/agent_plan.json b/plan/src/test/resources/agent_plans/agent_plan.json
index 3cb8dcb25..52a1b5ad8 100644
--- a/plan/src/test/resources/agent_plans/agent_plan.json
+++ b/plan/src/test/resources/agent_plans/agent_plan.json
@@ -8,7 +8,7 @@
"parameter_types": ["org.apache.flink.agents.api.Event", "org.apache.flink.agents.api.context.RunnerContext"],
"func_type": "JavaFunction"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_input_event"
]
},
@@ -20,7 +20,7 @@
"parameter_types": ["org.apache.flink.agents.api.Event", "org.apache.flink.agents.api.context.RunnerContext"],
"func_type": "JavaFunction"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_input_event",
"MyEvent"
]
diff --git a/plan/src/test/resources/agent_plans/agent_plan_with_python_resource_providers.json b/plan/src/test/resources/agent_plans/agent_plan_with_python_resource_providers.json
index cb7e9170d..abd08e2d3 100644
--- a/plan/src/test/resources/agent_plans/agent_plan_with_python_resource_providers.json
+++ b/plan/src/test/resources/agent_plans/agent_plan_with_python_resource_providers.json
@@ -7,7 +7,7 @@
"module": "flink_agents.plan.tests.compatibility.python_agent_plan_compatibility_test_agent",
"qualname": "PythonAgentPlanCompatibilityTestAgent.first_action"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_input_event"
]
},
@@ -18,7 +18,7 @@
"module": "flink_agents.plan.tests.compatibility.python_agent_plan_compatibility_test_agent",
"qualname": "PythonAgentPlanCompatibilityTestAgent.second_action"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_input_event",
"_my_event"
]
@@ -30,7 +30,7 @@
"module": "flink_agents.plan.actions.chat_model_action",
"qualname": "process_chat_request_or_tool_response"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_chat_request_event",
"_tool_response_event"
]
@@ -42,7 +42,7 @@
"module": "flink_agents.plan.actions.tool_call_action",
"qualname": "process_tool_request"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_tool_request_event"
]
}
diff --git a/runtime/src/test/java/org/apache/flink/agents/runtime/RescalingTest.java b/runtime/src/test/java/org/apache/flink/agents/runtime/RescalingTest.java
index a829e62d2..bdd5efe1b 100644
--- a/runtime/src/test/java/org/apache/flink/agents/runtime/RescalingTest.java
+++ b/runtime/src/test/java/org/apache/flink/agents/runtime/RescalingTest.java
@@ -19,6 +19,7 @@
package org.apache.flink.agents.runtime;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -504,14 +505,14 @@ public static class TestAgent extends Agent {
public static final AtomicInteger numProcessedEvent = new AtomicInteger(0);
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void handleInputEvent(Event event, RunnerContext context) {
InputEvent inputEvent = InputEvent.fromEvent(event);
numProcessedEvent.incrementAndGet();
context.sendEvent(new TestEvent((Integer) inputEvent.getInput()));
}
- @Action(listenEventTypes = {TestEvent.EVENT_TYPE})
+ @Action(TestEvent.EVENT_TYPE)
public static void handleTestEvent(Event event, RunnerContext context) {
TestEvent testEvent = (TestEvent) event;
numProcessedEvent.incrementAndGet();
diff --git a/runtime/src/test/java/org/apache/flink/agents/runtime/ResourceCacheTest.java b/runtime/src/test/java/org/apache/flink/agents/runtime/ResourceCacheTest.java
index a48f618e5..f17e16d48 100644
--- a/runtime/src/test/java/org/apache/flink/agents/runtime/ResourceCacheTest.java
+++ b/runtime/src/test/java/org/apache/flink/agents/runtime/ResourceCacheTest.java
@@ -19,6 +19,7 @@
package org.apache.flink.agents.runtime;
import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.agents.Agent;
import org.apache.flink.agents.api.annotation.ChatModelSetup;
@@ -124,7 +125,7 @@ public static ResourceDescriptor pythonChatModel() {
@Tool private TestTool anotherTool = new TestTool("anotherTool");
- @org.apache.flink.agents.api.annotation.Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @org.apache.flink.agents.api.annotation.Action(EventType.InputEvent)
public void handleInputEvent(Event event, RunnerContext context) {
InputEvent inputEvent = InputEvent.fromEvent(event);
}
diff --git a/runtime/src/test/java/org/apache/flink/agents/runtime/memory/ShortTermMemoryTTLIntegrationTest.java b/runtime/src/test/java/org/apache/flink/agents/runtime/memory/ShortTermMemoryTTLIntegrationTest.java
index 1e9113840..97e4099e2 100644
--- a/runtime/src/test/java/org/apache/flink/agents/runtime/memory/ShortTermMemoryTTLIntegrationTest.java
+++ b/runtime/src/test/java/org/apache/flink/agents/runtime/memory/ShortTermMemoryTTLIntegrationTest.java
@@ -18,6 +18,7 @@
package org.apache.flink.agents.runtime.memory;
import org.apache.flink.agents.api.AgentsExecutionEnvironment;
+import org.apache.flink.agents.api.EventType;
import org.apache.flink.agents.api.InputEvent;
import org.apache.flink.agents.api.OutputEvent;
import org.apache.flink.agents.api.agents.Agent;
@@ -56,7 +57,7 @@ private TestInput(String eventKey, long sleepMs) {
public static class TTLTestAgent extends Agent {
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void input(org.apache.flink.agents.api.Event event, RunnerContext ctx)
throws Exception {
InputEvent inputEvent = (InputEvent) event;
From 5217fce7bbbff1ffa7e5c3245d1d90f42585df94 Mon Sep 17 00:00:00 2001
From: rosemaryYuan <91046107+rosemarYuan@users.noreply.github.com>
Date: Wed, 3 Jun 2026 13:13:11 +0800
Subject: [PATCH 03/13] [python][plan] Unify @action trigger entry as
trigger_conditions, migrate callers
---
docs/content/docs/development/chat_models.md | 8 ++--
.../docs/development/embedding_models.md | 4 +-
.../development/memory/long_term_memory.md | 4 +-
.../memory/sensory_and_short_term_memory.md | 4 +-
docs/content/docs/development/prompts.md | 2 +-
.../content/docs/development/vector_stores.md | 4 +-
.../docs/development/workflow_agent.md | 16 ++++----
.../get-started/quickstart/skills_agent.md | 4 +-
.../get-started/quickstart/workflow_agent.md | 4 +-
docs/content/docs/operations/monitoring.md | 4 +-
python/flink_agents/api/agents/agent.py | 2 +-
python/flink_agents/api/agents/react_agent.py | 3 +-
python/flink_agents/api/decorators.py | 29 +++++++++------
python/flink_agents/api/events/event_type.py | 14 ++++---
.../flink_agents/api/tests/test_decorators.py | 37 ++++++++++---------
.../agent_skills_test.py | 5 ++-
.../built_in_action_async_execution_test.py | 5 ++-
.../chat_model_integration_agent.py | 5 ++-
.../e2e_tests_mcp/mcp_test.py | 5 ++-
.../execute_test_agent.py | 9 +++--
.../flink_integration_agent.py | 7 ++--
.../long_term_memory_test.py | 3 +-
.../python_event_logging_test.py | 3 +-
.../short_term_memory_ttl_test.py | 3 +-
.../e2e_tests_integration/workflow_test.py | 3 +-
.../chat_model_cross_language_agent.py | 5 ++-
.../embedding_model_cross_language_agent.py | 3 +-
.../vector_store_cross_language_agent.py | 5 ++-
.../examples/quickstart/agents/math_agent.py | 5 ++-
.../agents/product_suggestion_agent.py | 5 ++-
.../agents/review_analysis_agent.py | 5 ++-
.../agents/table_review_analysis_agent.py | 5 ++-
.../examples/rag/rag_agent_example.py | 7 ++--
python/flink_agents/plan/actions/action.py | 26 +++++++++----
.../plan/actions/chat_model_action.py | 2 +-
.../plan/actions/context_retrieval_action.py | 2 +-
.../plan/actions/tool_call_action.py | 2 +-
python/flink_agents/plan/agent_plan.py | 20 +++++-----
.../create_python_agent_plan_from_json.py | 4 +-
...hon_agent_plan_compatibility_test_agent.py | 5 ++-
.../plan/tests/resources/action.json | 2 +-
.../plan/tests/resources/agent_plan.json | 10 ++---
python/flink_agents/plan/tests/test_action.py | 10 ++---
.../tests/test_agent_plan_cross_language.py | 10 ++---
.../runtime/tests/test_built_in_actions.py | 5 ++-
.../tests/test_get_resource_in_action.py | 3 +-
.../tests/test_local_execution_environment.py | 11 +++---
.../tests/test_runner_context_execute.py | 11 +++---
48 files changed, 197 insertions(+), 153 deletions(-)
diff --git a/docs/content/docs/development/chat_models.md b/docs/content/docs/development/chat_models.md
index 0e7f40239..c7b40ae05 100644
--- a/docs/content/docs/development/chat_models.md
+++ b/docs/content/docs/development/chat_models.md
@@ -80,7 +80,7 @@ class MyAgent(Agent):
temperature=0.7
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
input_event = InputEvent.from_event(event)
@@ -93,7 +93,7 @@ class MyAgent(Agent):
ChatRequestEvent(model="ollama_chat_model", messages=[user_message])
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_response(event: Event, ctx: RunnerContext) -> None:
chat_response = ChatResponseEvent.from_event(event)
@@ -1178,7 +1178,7 @@ class MyAgent(Agent):
extract_reasoning=True,
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
input_event = InputEvent.from_event(event)
@@ -1191,7 +1191,7 @@ class MyAgent(Agent):
ChatRequestEvent(model="java_chat_model", messages=[user_message])
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_response(event: Event, ctx: RunnerContext) -> None:
chat_response = ChatResponseEvent.from_event(event)
diff --git a/docs/content/docs/development/embedding_models.md b/docs/content/docs/development/embedding_models.md
index c3f789836..a745dd154 100644
--- a/docs/content/docs/development/embedding_models.md
+++ b/docs/content/docs/development/embedding_models.md
@@ -129,7 +129,7 @@ class MyAgent(Agent):
model="your-embedding-model-here"
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_text(event: Event, ctx: RunnerContext) -> None:
# Get the embedding model from the runtime context
@@ -612,7 +612,7 @@ class MyAgent(Agent):
model="nomic-embed-text"
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
# Use the Java embedding model from Python
diff --git a/docs/content/docs/development/memory/long_term_memory.md b/docs/content/docs/development/memory/long_term_memory.md
index f3ad2ee74..46ffc3ac6 100644
--- a/docs/content/docs/development/memory/long_term_memory.md
+++ b/docs/content/docs/development/memory/long_term_memory.md
@@ -148,7 +148,7 @@ from flink_agents.api.decorators import action
from flink_agents.api.events.event import InputEvent, Event
from flink_agents.api.runner_context import RunnerContext
-@action(InputEvent.EVENT_TYPE)
+@action(EventType.InputEvent)
@staticmethod
def process_event(event: Event, ctx: RunnerContext) -> None:
ltm = ctx.long_term_memory
@@ -464,7 +464,7 @@ from flink_agents.api.runner_context import RunnerContext
class PersonalizedAssistant:
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_event(event: Event, ctx: RunnerContext) -> None:
"""Respond to user using long-term memory."""
diff --git a/docs/content/docs/development/memory/sensory_and_short_term_memory.md b/docs/content/docs/development/memory/sensory_and_short_term_memory.md
index 34c47b314..7d92452a9 100644
--- a/docs/content/docs/development/memory/sensory_and_short_term_memory.md
+++ b/docs/content/docs/development/memory/sensory_and_short_term_memory.md
@@ -79,7 +79,7 @@ The key of the pairs store in `MemoryObject` must be string, and the value can b
{{< tab "Python" >}}
```python
-@action(InputEvent.EVENT_TYPE)
+@action(EventType.InputEvent)
def process_event(event: Event, ctx: RunnerContext) -> None:
memory: MemoryObject = ctx.sensory_memory # or ctx.short_term_memory
# store primitive
@@ -211,7 +211,7 @@ def first_action(event: Event, ctx: RunnerContext):
ctx.send_event(MyEvent(value=data_ref))
...
-@action(MyEvent.EVENT_TYPE)
+@action("MyEvent")
@staticmethod
def second_action(event: Event, ctx: RunnerContext):
my_event = MyEvent.from_event(event)
diff --git a/docs/content/docs/development/prompts.md b/docs/content/docs/development/prompts.md
index 231b1cf85..8c57b5b0e 100644
--- a/docs/content/docs/development/prompts.md
+++ b/docs/content/docs/development/prompts.md
@@ -243,7 +243,7 @@ class ReviewAnalysisAgent(Agent):
extract_reasoning=True,
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event and send chat request for review analysis."""
diff --git a/docs/content/docs/development/vector_stores.md b/docs/content/docs/development/vector_stores.md
index 8124ba91c..07d5eafa7 100644
--- a/docs/content/docs/development/vector_stores.md
+++ b/docs/content/docs/development/vector_stores.md
@@ -362,7 +362,7 @@ class MyAgent(Agent):
collection="my_chroma_store"
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def search_documents(event: Event, ctx: RunnerContext) -> None:
# Get the vector store from the runtime context
@@ -1049,7 +1049,7 @@ class MyAgent(Agent):
dims=768
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
# Use Java vector store from Python
diff --git a/docs/content/docs/development/workflow_agent.md b/docs/content/docs/development/workflow_agent.md
index 8ba4ae4ec..133ca9291 100644
--- a/docs/content/docs/development/workflow_agent.md
+++ b/docs/content/docs/development/workflow_agent.md
@@ -85,7 +85,7 @@ class ReviewAnalysisAgent(Agent):
extract_reasoning=True,
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event and send chat request for review analysis."""
@@ -106,7 +106,7 @@ class ReviewAnalysisAgent(Agent):
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat response event and send output event."""
@@ -242,7 +242,7 @@ The decorated/annotated function signature should be `(Event, RunnerContext) ->
{{< tab "Python" >}}
```python
class ReviewAnalysisAgent(Agent):
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
# the action logic
@@ -272,7 +272,7 @@ In the function, user can also send new events, to trigger other actions, or out
{{< tab "Python" >}}
```python
-@action(InputEvent.EVENT_TYPE)
+@action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
# send a ChatRequestEvent to trigger the built-in chat-model action
@@ -360,7 +360,7 @@ Use a reconciler for durable calls when the original call may already have compl
{{< tab "Python" >}}
Python actions can call `ctx.durable_execute(...)` to run a synchronous durable code block.
```python
-@action(InputEvent.EVENT_TYPE)
+@action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
input_event = InputEvent.from_event(event)
@@ -375,7 +375,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
You can also pass an optional `reconciler` callable to recover an execution outcome during recovery.
```python
-@action(InputEvent.EVENT_TYPE)
+@action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
input_event = InputEvent.from_event(event)
@@ -477,7 +477,7 @@ Async execution uses the same durable semantics but yields while waiting for a t
{{< tab "Python" >}}
Define an `async def` action and `await ctx.durable_execute_async(...)`. The same optional `reconciler=...` argument is available for recovery.
```python
-@action(InputEvent.EVENT_TYPE)
+@action(EventType.InputEvent)
@staticmethod
async def process_with_async(event: Event, ctx: RunnerContext) -> None:
input_event = InputEvent.from_event(event)
@@ -596,7 +596,7 @@ For simple cases, users can pass data between actions directly using `Event` wit
{{< tab "Python" >}}
```python
# Send a unified event from one action
-@action(InputEvent.EVENT_TYPE)
+@action(EventType.InputEvent)
@staticmethod
def create_my_event(event: Event, ctx: RunnerContext) -> None:
ctx.send_event(
diff --git a/docs/content/docs/get-started/quickstart/skills_agent.md b/docs/content/docs/get-started/quickstart/skills_agent.md
index 677d8c6aa..a95a276a9 100644
--- a/docs/content/docs/get-started/quickstart/skills_agent.md
+++ b/docs/content/docs/get-started/quickstart/skills_agent.md
@@ -107,7 +107,7 @@ class MathAgent(Agent):
allowed_commands=["echo", "bc"],
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event and send a chat request to evaluate the question."""
@@ -119,7 +119,7 @@ class MathAgent(Agent):
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat response event and send the answer as output."""
diff --git a/docs/content/docs/get-started/quickstart/workflow_agent.md b/docs/content/docs/get-started/quickstart/workflow_agent.md
index 4e0e97869..c3c442fcb 100644
--- a/docs/content/docs/get-started/quickstart/workflow_agent.md
+++ b/docs/content/docs/get-started/quickstart/workflow_agent.md
@@ -128,7 +128,7 @@ class ReviewAnalysisAgent(Agent):
extract_reasoning=True,
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event and send chat request for review analysis."""
@@ -148,7 +148,7 @@ class ReviewAnalysisAgent(Agent):
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat response event and send output event."""
diff --git a/docs/content/docs/operations/monitoring.md b/docs/content/docs/operations/monitoring.md
index c24a07051..021452125 100644
--- a/docs/content/docs/operations/monitoring.md
+++ b/docs/content/docs/operations/monitoring.md
@@ -62,7 +62,7 @@ Here is the user case example:
{{< tab "Python" >}}
```python
class MyAgent(Agent):
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def first_action(event: Event, ctx: RunnerContext):
start_time = time.time_ns()
@@ -130,7 +130,7 @@ The Flink Agents' log system uses Flink's logging framework. For more details, p
For adding logs in Java code, you can refer to [Flink documentation](https://nightlies.apache.org/flink/flink-docs-master/docs/deployment/advanced/logging/#best-practices-for-developers). In Python, you can add logs using `logging`. Here is a specific example:
```python
-@action(InputEvent.EVENT_TYPE)
+@action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
logging.info("Processing input event: %s", event)
diff --git a/python/flink_agents/api/agents/agent.py b/python/flink_agents/api/agents/agent.py
index 3a6aed852..b5b7de607 100644
--- a/python/flink_agents/api/agents/agent.py
+++ b/python/flink_agents/api/agents/agent.py
@@ -39,7 +39,7 @@ class Agent(ABC):
::
class MyAgent(Agent):
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def my_action(event: Event, ctx: RunnerContext) -> None:
action logic
diff --git a/python/flink_agents/api/agents/react_agent.py b/python/flink_agents/api/agents/react_agent.py
index cef651a1d..ca69b2b8a 100644
--- a/python/flink_agents/api/agents/react_agent.py
+++ b/python/flink_agents/api/agents/react_agent.py
@@ -33,6 +33,7 @@
from flink_agents.api.decorators import action
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import ResourceDescriptor, ResourceType
from flink_agents.api.runner_context import RunnerContext
@@ -205,7 +206,7 @@ def start_action(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def stop_action(event: Event, ctx: RunnerContext) -> None:
"""Stop action to output result."""
diff --git a/python/flink_agents/api/decorators.py b/python/flink_agents/api/decorators.py
index 6025ac393..5c3fd47dc 100644
--- a/python/flink_agents/api/decorators.py
+++ b/python/flink_agents/api/decorators.py
@@ -33,17 +33,19 @@ def _validate_target(target: Function, owner: str) -> None:
def action(
- *listen_events: str,
+ *trigger_conditions: str,
target: Function | None = None,
) -> Callable:
"""Decorator for marking a function as an agent action.
- Each argument is a type-identifier string that this action responds to.
+ Each argument is an event-type name string that this action responds to.
+ Multiple entries combine with OR semantics — the action triggers if any
+ one matches.
Parameters
----------
- listen_events : str
- Type-identifier strings that this action responds to.
+ trigger_conditions : str
+ Event-type name strings that this action responds to.
target : Function, optional
Cross-language function descriptor dispatched instead of the
decorated body. The body becomes a stub — raise
@@ -52,22 +54,25 @@ def action(
Returns:
-------
Callable
- Decorator function that marks the target function with event listeners.
+ Decorator function that marks the target function with trigger conditions.
Raises:
------
AssertionError
- If no events are provided or if an argument is not a string.
+ If no conditions are given or any entry is not a non-empty string.
TypeError
If ``target`` is provided but is not a :class:`Function` descriptor.
"""
- assert len(listen_events) > 0, (
- "action must have at least one event type to listen to"
+ assert len(trigger_conditions) > 0, (
+ "action must have at least one trigger condition (event-type name)"
)
- for evt in listen_events:
- assert isinstance(evt, str), (
- f"action must listen to string type identifiers, got {evt!r}"
+ for entry in trigger_conditions:
+ assert isinstance(entry, str), (
+ f"action trigger condition must be a string, got {entry!r}"
+ )
+ assert entry != "", (
+ f"action trigger condition must be non-empty, got {entry!r}"
)
if target is not None and not isinstance(target, Function):
@@ -81,7 +86,7 @@ def decorator(func: Callable) -> Callable:
if target is not None:
_validate_target(target, func.__qualname__)
func._target = target
- func._listen_events = listen_events
+ func._trigger_conditions = trigger_conditions
return func
return decorator
diff --git a/python/flink_agents/api/events/event_type.py b/python/flink_agents/api/events/event_type.py
index e36655ac9..7cea2add4 100644
--- a/python/flink_agents/api/events/event_type.py
+++ b/python/flink_agents/api/events/event_type.py
@@ -32,8 +32,7 @@ def handle(...): ...
from __future__ import annotations
import threading
-from typing import Dict, Optional, Type
-
+from typing import Dict, Type
# Hard-coded to avoid an event_type -> event -> event_type circular import.
# A consistency test asserts each value matches XxxEvent.EVENT_TYPE.
@@ -91,7 +90,7 @@ def register(event_class: Type) -> None:
raise RuntimeError(msg)
-def lookup(name: Optional[str]) -> Optional[str]:
+def lookup(name: str | None) -> str | None:
"""Return the ``EVENT_TYPE`` string for a registered short name, else ``None``.
Built-in names take precedence over user-registered ones.
@@ -110,7 +109,7 @@ def lookup_or_self(name: str) -> str:
return v if v is not None else name
-def is_known(name: Optional[str]) -> bool:
+def is_known(name: str | None) -> bool:
"""Return ``True`` if ``name`` is a registered short name."""
return lookup(name) is not None
@@ -129,8 +128,10 @@ def _clear_user_registered_for_testing() -> None:
class EventType:
- """Namespace of built-in event-type constants, byte-equal to each
- ``XxxEvent.EVENT_TYPE``. Use inside ``trigger_conditions``::
+ """Namespace of built-in event-type constants.
+
+ Each constant is byte-equal to the corresponding ``XxxEvent.EVENT_TYPE``
+ and is meant to be used inside ``trigger_conditions``::
@action(EventType.InputEvent)
@@ -153,5 +154,6 @@ class EventType:
all_registered = staticmethod(all_registered)
def __init__(self) -> None:
+ """Reject instantiation; ``EventType`` is a namespace, not a class."""
msg = "EventType is a namespace; do not instantiate"
raise TypeError(msg)
diff --git a/python/flink_agents/api/tests/test_decorators.py b/python/flink_agents/api/tests/test_decorators.py
index 94ff8d86f..ccd58241b 100644
--- a/python/flink_agents/api/tests/test_decorators.py
+++ b/python/flink_agents/api/tests/test_decorators.py
@@ -19,29 +19,30 @@
from flink_agents.api.decorators import action
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.function import JavaFunction, PythonFunction
from flink_agents.api.runner_context import RunnerContext
def test_action_decorator() -> None:
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
def forward_action(event: Event, ctx: RunnerContext) -> None:
input = InputEvent.from_event(event).input
ctx.send_event(OutputEvent(output=input))
- assert hasattr(forward_action, "_listen_events")
- listen_events = forward_action._listen_events
+ assert hasattr(forward_action, "_trigger_conditions")
+ listen_events = forward_action._trigger_conditions
assert listen_events == (InputEvent.EVENT_TYPE,)
def test_action_decorator_listen_multi_events() -> None:
- @action(InputEvent.EVENT_TYPE, OutputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent, EventType.OutputEvent)
def forward_action(event: Event, ctx: RunnerContext) -> None:
input = InputEvent.from_event(event).input
ctx.send_event(OutputEvent(output=input))
- assert hasattr(forward_action, "_listen_events")
- listen_events = forward_action._listen_events
+ assert hasattr(forward_action, "_trigger_conditions")
+ listen_events = forward_action._trigger_conditions
assert listen_events == (InputEvent.EVENT_TYPE, OutputEvent.EVENT_TYPE)
@@ -70,8 +71,8 @@ def test_action_decorator_with_string_identifier() -> None:
def my_handler(event: Event, ctx: RunnerContext) -> None:
pass
- assert hasattr(my_handler, "_listen_events")
- assert my_handler._listen_events == ("MyCustomEvent",)
+ assert hasattr(my_handler, "_trigger_conditions")
+ assert my_handler._trigger_conditions == ("MyCustomEvent",)
def test_action_decorator_multiple_strings() -> None:
@@ -81,7 +82,7 @@ def test_action_decorator_multiple_strings() -> None:
def mixed_handler(event: Event, ctx: RunnerContext) -> None:
pass
- assert mixed_handler._listen_events == ("_input_event", "AnotherEvent")
+ assert mixed_handler._trigger_conditions == ("_input_event", "AnotherEvent")
def test_action_decorator_rejects_invalid_types() -> None:
@@ -100,25 +101,25 @@ def _java_target() -> JavaFunction:
def test_action_decorator_with_cross_language_target() -> None:
target = _java_target()
- @action(InputEvent.EVENT_TYPE, target=target)
+ @action(EventType.InputEvent, target=target)
def stub(event: Event, ctx: RunnerContext) -> None:
msg = "cross-language stub"
raise NotImplementedError(msg)
- assert stub._listen_events == (InputEvent.EVENT_TYPE,)
+ assert stub._trigger_conditions == (InputEvent.EVENT_TYPE,)
assert stub._target is target
def test_action_decorator_rejects_non_function_target() -> None:
with pytest.raises(TypeError, match="api-layer Function descriptor"):
- @action(InputEvent.EVENT_TYPE, target="not a function") # type: ignore[arg-type]
+ @action(EventType.InputEvent, target="not a function") # type: ignore[arg-type]
def stub(event: Event, ctx: RunnerContext) -> None:
pass
def test_action_decorator_without_target_does_not_set_attribute() -> None:
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
def regular(event: Event, ctx: RunnerContext) -> None:
pass
@@ -129,7 +130,7 @@ def test_action_decorator_rejects_java_target_with_empty_qualname() -> None:
bad = JavaFunction(qualname="", method_name="handle", parameter_types=[])
with pytest.raises(ValueError, match="qualname"):
- @action(InputEvent.EVENT_TYPE, target=bad)
+ @action(EventType.InputEvent, target=bad)
def stub(event: Event, ctx: RunnerContext) -> None:
pass
@@ -138,7 +139,7 @@ def test_action_decorator_rejects_java_target_with_empty_method_name() -> None:
bad = JavaFunction(qualname="com.example.X", method_name="", parameter_types=[])
with pytest.raises(ValueError, match="method_name"):
- @action(InputEvent.EVENT_TYPE, target=bad)
+ @action(EventType.InputEvent, target=bad)
def stub(event: Event, ctx: RunnerContext) -> None:
pass
@@ -147,7 +148,7 @@ def test_action_decorator_rejects_python_target_with_empty_module() -> None:
bad = PythonFunction(module="", qualname="handle")
with pytest.raises(ValueError, match="module"):
- @action(InputEvent.EVENT_TYPE, target=bad)
+ @action(EventType.InputEvent, target=bad)
def stub(event: Event, ctx: RunnerContext) -> None:
pass
@@ -156,7 +157,7 @@ def test_action_decorator_rejects_python_target_with_empty_qualname() -> None:
bad = PythonFunction(module="pkg.mod", qualname="")
with pytest.raises(ValueError, match="qualname"):
- @action(InputEvent.EVENT_TYPE, target=bad)
+ @action(EventType.InputEvent, target=bad)
def stub(event: Event, ctx: RunnerContext) -> None:
pass
@@ -165,6 +166,6 @@ def test_action_decorator_target_error_names_decorated_function() -> None:
bad = PythonFunction(module="pkg.mod", qualname="")
with pytest.raises(ValueError, match="my_named_stub"):
- @action(InputEvent.EVENT_TYPE, target=bad)
+ @action(EventType.InputEvent, target=bad)
def my_named_stub(event: Event, ctx: RunnerContext) -> None:
pass
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/agent_skills_test.py b/python/flink_agents/e2e_tests/e2e_tests_integration/agent_skills_test.py
index 8e5d13f8a..2fdc42354 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/agent_skills_test.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/agent_skills_test.py
@@ -42,6 +42,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import ResourceDescriptor, ResourceName, ResourceType
@@ -107,7 +108,7 @@ def system_prompt() -> Prompt:
],
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
input_event = InputEvent.from_event(event)
@@ -138,7 +139,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
chat_response = ChatResponseEvent.from_event(event)
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/built_in_action_async_execution_test.py b/python/flink_agents/e2e_tests/e2e_tests_integration/built_in_action_async_execution_test.py
index f59d25f00..cb0de86ee 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/built_in_action_async_execution_test.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/built_in_action_async_execution_test.py
@@ -28,6 +28,7 @@
from flink_agents.api.decorators import action, chat_model_setup, tool
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.resource import ResourceDescriptor
from flink_agents.api.runner_context import RunnerContext
@@ -83,7 +84,7 @@ def add(a: int, b: int) -> int:
time.sleep(5) # Simulate slow tool execution
return a + b
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
input_event = InputEvent.from_event(event)
@@ -96,7 +97,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
input = ChatResponseEvent.from_event(event).response
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 2b9b7da70..c7158dbd1 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
@@ -27,6 +27,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.resource import (
ResourceDescriptor,
ResourceName,
@@ -162,7 +163,7 @@ def add(a: int, b: int) -> int:
"""
return a + b
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing input.
@@ -183,7 +184,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing chat model response."""
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/e2e_tests_mcp/mcp_test.py b/python/flink_agents/e2e_tests/e2e_tests_integration/e2e_tests_mcp/mcp_test.py
index b617e825a..2c994f1fb 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/e2e_tests_mcp/mcp_test.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/e2e_tests_mcp/mcp_test.py
@@ -46,6 +46,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.resource import (
ResourceDescriptor,
@@ -106,7 +107,7 @@ def math_chat_model() -> ResourceDescriptor:
)
return ResourceDescriptor(**descriptor_kwargs)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input and send chat request.
@@ -136,7 +137,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
ctx.send_event(ChatRequestEvent(model="math_chat_model", messages=[msg]))
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat response and output result."""
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/execute_test_agent.py b/python/flink_agents/e2e_tests/e2e_tests_integration/execute_test_agent.py
index c1f5aa50b..6bd662f41 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/execute_test_agent.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/execute_test_agent.py
@@ -23,6 +23,7 @@
from flink_agents.api.agents.agent import Agent
from flink_agents.api.decorators import action
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.runner_context import RunnerContext
@@ -97,7 +98,7 @@ def raise_exception(message: str) -> None:
class ExecuteTestAgent(Agent):
"""Agent that uses synchronous durable_execute() method for testing."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event using durable_execute()."""
@@ -112,7 +113,7 @@ def process(event: Event, ctx: RunnerContext) -> None:
class ExecuteMultipleTestAgent(Agent):
"""Agent that makes multiple durable_execute() calls."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event with multiple durable_execute() calls."""
@@ -127,7 +128,7 @@ def process(event: Event, ctx: RunnerContext) -> None:
class ExecuteWithAsyncTestAgent(Agent):
"""Agent that uses both durable_execute() and durable_execute_async()."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
async def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event using both durable_execute() and durable_execute_async()."""
@@ -144,7 +145,7 @@ async def process(event: Event, ctx: RunnerContext) -> None:
class ExecuteWithAsyncExceptionTestAgent(Agent):
"""Agent that tests exception handling in durable_execute_async()."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
async def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event and capture durable_execute_async() exceptions."""
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/flink_integration_agent.py b/python/flink_agents/e2e_tests/e2e_tests_integration/flink_integration_agent.py
index 2982c5aa3..c584570a5 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/flink_integration_agent.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/flink_integration_agent.py
@@ -27,6 +27,7 @@
from flink_agents.api.agents.agent import Agent
from flink_agents.api.decorators import action, tool
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.resource import ResourceType
from flink_agents.api.runner_context import RunnerContext
@@ -105,7 +106,7 @@ def my_tool(input: str) -> str:
"""
return input + " call my tool"
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
async def first_action(event: Event, ctx: RunnerContext) -> None:
def log_to_stdout(input: Any, total: int) -> bool:
@@ -152,7 +153,7 @@ class TableAgent(Agent):
to __main__.
"""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def first_action(event: Event, ctx: RunnerContext) -> None:
content = InputEvent.from_event(event).input
@@ -175,7 +176,7 @@ class DataStreamToTableAgent(Agent):
to __main__.
"""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def first_action(event: Event, ctx: RunnerContext) -> None:
content = ItemData.model_validate(InputEvent.from_event(event).input)
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/long_term_memory_test.py b/python/flink_agents/e2e_tests/e2e_tests_integration/long_term_memory_test.py
index a5dc32836..3db2ae010 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/long_term_memory_test.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/long_term_memory_test.py
@@ -53,6 +53,7 @@
vector_store,
)
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.memory.long_term_memory import (
LongTermMemoryOptions,
@@ -189,7 +190,7 @@ def chroma_ltm_store() -> ResourceDescriptor:
collection="context",
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
async def add_items(event: Event, ctx: RunnerContext) -> None:
input_data = ItemData.model_validate(InputEvent.from_event(event).input)
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/python_event_logging_test.py b/python/flink_agents/e2e_tests/e2e_tests_integration/python_event_logging_test.py
index 898079276..2aade7d80 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/python_event_logging_test.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/python_event_logging_test.py
@@ -36,6 +36,7 @@
from flink_agents.api.agents.agent import Agent
from flink_agents.api.decorators import action
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.runner_context import RunnerContext
@@ -53,7 +54,7 @@ def get_key(self, value: dict) -> int:
class PythonEventLoggingAgent(Agent):
"""Agent for testing Python event logging."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event and send an output event."""
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/short_term_memory_ttl_test.py b/python/flink_agents/e2e_tests/e2e_tests_integration/short_term_memory_ttl_test.py
index 19c149fc9..472c12ae5 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/short_term_memory_ttl_test.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/short_term_memory_ttl_test.py
@@ -33,6 +33,7 @@
)
from flink_agents.api.decorators import action
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.runner_context import RunnerContext
@@ -53,7 +54,7 @@ def get_key(self, value: TtlTestInput) -> str:
class ShortTermMemoryTtlTestAgent(Agent):
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def input(event: Event, ctx: RunnerContext) -> None:
input_data = TtlTestInput.model_validate(InputEvent.from_event(event).input)
diff --git a/python/flink_agents/e2e_tests/e2e_tests_integration/workflow_test.py b/python/flink_agents/e2e_tests/e2e_tests_integration/workflow_test.py
index ab71fa7b1..8195de44e 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_integration/workflow_test.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_integration/workflow_test.py
@@ -24,6 +24,7 @@
from flink_agents.api.agents.agent import Agent
from flink_agents.api.decorators import action
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.runner_context import RunnerContext
@@ -68,7 +69,7 @@ class MyAgent(Agent):
validation.
"""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def first_action(event: Event, ctx: RunnerContext) -> None:
key = ctx.key
diff --git a/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/chat_model_cross_language_agent.py b/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/chat_model_cross_language_agent.py
index 2aca6a2bd..3920f2389 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/chat_model_cross_language_agent.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/chat_model_cross_language_agent.py
@@ -28,6 +28,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import (
ResourceDescriptor,
@@ -129,7 +130,7 @@ def add(a: int, b: int) -> int:
"""
return a + b
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing input.
@@ -150,7 +151,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing chat model response."""
diff --git a/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/embedding_model_cross_language_agent.py b/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/embedding_model_cross_language_agent.py
index 9a2ba4ed9..dbce4891b 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/embedding_model_cross_language_agent.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/embedding_model_cross_language_agent.py
@@ -24,6 +24,7 @@
embedding_model_setup,
)
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.resource import (
ResourceDescriptor,
ResourceName,
@@ -56,7 +57,7 @@ def embedding_model() -> ResourceDescriptor:
model=os.environ.get("OLLAMA_EMBEDDING_MODEL", "nomic-embed-text:latest"),
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing input.
diff --git a/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/vector_store_cross_language_agent.py b/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/vector_store_cross_language_agent.py
index 18671bea2..52000147a 100644
--- a/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/vector_store_cross_language_agent.py
+++ b/python/flink_agents/e2e_tests/e2e_tests_resource_cross_language/vector_store_cross_language_agent.py
@@ -30,6 +30,7 @@
ContextRetrievalResponseEvent,
)
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.resource import (
ResourceDescriptor,
ResourceName,
@@ -153,7 +154,7 @@ def vector_store() -> ResourceDescriptor:
msg = f"Unsupported vector store backend: {backend}"
raise ValueError(msg)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing input.
@@ -273,7 +274,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ContextRetrievalResponseEvent.EVENT_TYPE)
+ @action(EventType.ContextRetrievalResponseEvent)
@staticmethod
def contextRetrievalResponseEvent(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing context retrieval response.
diff --git a/python/flink_agents/examples/quickstart/agents/math_agent.py b/python/flink_agents/examples/quickstart/agents/math_agent.py
index 20e3b8acf..77f177607 100644
--- a/python/flink_agents/examples/quickstart/agents/math_agent.py
+++ b/python/flink_agents/examples/quickstart/agents/math_agent.py
@@ -20,6 +20,7 @@
from flink_agents.api.decorators import action, chat_model_setup, prompt, skills
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import ResourceDescriptor, ResourceName
from flink_agents.api.runner_context import RunnerContext
@@ -79,7 +80,7 @@ def math_model() -> ResourceDescriptor:
allowed_commands=["echo", "bc"],
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event and send a chat request to evaluate the question."""
@@ -91,7 +92,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat response event and send the answer as output."""
diff --git a/python/flink_agents/examples/quickstart/agents/product_suggestion_agent.py b/python/flink_agents/examples/quickstart/agents/product_suggestion_agent.py
index 20350835a..b0979fa76 100644
--- a/python/flink_agents/examples/quickstart/agents/product_suggestion_agent.py
+++ b/python/flink_agents/examples/quickstart/agents/product_suggestion_agent.py
@@ -26,6 +26,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import ResourceDescriptor, ResourceName
from flink_agents.api.runner_context import RunnerContext
@@ -66,7 +67,7 @@ def generate_suggestion_model() -> ResourceDescriptor:
extract_reasoning=True,
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event."""
@@ -87,7 +88,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat response event."""
diff --git a/python/flink_agents/examples/quickstart/agents/review_analysis_agent.py b/python/flink_agents/examples/quickstart/agents/review_analysis_agent.py
index 8bd3b74dc..8e7f9a105 100644
--- a/python/flink_agents/examples/quickstart/agents/review_analysis_agent.py
+++ b/python/flink_agents/examples/quickstart/agents/review_analysis_agent.py
@@ -27,6 +27,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import ResourceDescriptor, ResourceName
from flink_agents.api.runner_context import RunnerContext
@@ -83,7 +84,7 @@ def review_analysis_model() -> ResourceDescriptor:
extract_reasoning=True,
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event and send chat request for review analysis."""
@@ -103,7 +104,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat response event and send output event."""
diff --git a/python/flink_agents/examples/quickstart/agents/table_review_analysis_agent.py b/python/flink_agents/examples/quickstart/agents/table_review_analysis_agent.py
index 1ee1f801c..7ddf94390 100644
--- a/python/flink_agents/examples/quickstart/agents/table_review_analysis_agent.py
+++ b/python/flink_agents/examples/quickstart/agents/table_review_analysis_agent.py
@@ -31,6 +31,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import ResourceDescriptor, ResourceName
from flink_agents.api.runner_context import RunnerContext
@@ -108,7 +109,7 @@ def review_analysis_model() -> ResourceDescriptor:
extract_reasoning=True,
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process input event from Table data (dictionary format).
@@ -135,7 +136,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat response event and send output event."""
diff --git a/python/flink_agents/examples/rag/rag_agent_example.py b/python/flink_agents/examples/rag/rag_agent_example.py
index de1d9ebe6..6972cdf43 100644
--- a/python/flink_agents/examples/rag/rag_agent_example.py
+++ b/python/flink_agents/examples/rag/rag_agent_example.py
@@ -32,6 +32,7 @@
ContextRetrievalResponseEvent,
)
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import (
@@ -103,7 +104,7 @@ def chat_model() -> ResourceDescriptor:
model=OLLAMA_CHAT_MODEL,
)
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""Process user input and retrieve relevant context."""
@@ -116,7 +117,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ContextRetrievalResponseEvent.EVENT_TYPE)
+ @action(EventType.ContextRetrievalResponseEvent)
@staticmethod
def process_retrieved_context(
event: Event, ctx: RunnerContext
@@ -147,7 +148,7 @@ def process_retrieved_context(
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""Process chat model response and generate output."""
diff --git a/python/flink_agents/plan/actions/action.py b/python/flink_agents/plan/actions/action.py
index 96f2f5f0a..97f1f1b88 100644
--- a/python/flink_agents/plan/actions/action.py
+++ b/python/flink_agents/plan/actions/action.py
@@ -29,10 +29,10 @@
class Action(BaseModel):
- """Representation of an agent action with event listening and function execution.
+ """Representation of an agent action with unified trigger conditions.
- This class encapsulates a named agent action that listens for specific event
- types and executes an associated function when those events occur.
+ This class encapsulates a named agent action that triggers on matching
+ events and executes an associated function.
Attributes:
----------
@@ -40,8 +40,9 @@ class Action(BaseModel):
Name/identifier of the agent Action.
exec : Function
To be executed when the Action is triggered.
- listen_event_types : List[str]
- List of event types that will trigger this Action's execution.
+ trigger_conditions : List[str]
+ Event-type name strings that will trigger this Action. Multiple
+ entries combine with OR semantics.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
@@ -49,9 +50,18 @@ class Action(BaseModel):
name: str
# TODO: Raise a warning when the action has a return value, as it will be ignored.
exec: PythonFunction | JavaFunction
- listen_event_types: List[str]
+ trigger_conditions: List[str]
config: Dict[str, Any] | None = None
+ @property
+ def listen_event_types(self) -> List[str]:
+ """Event-type names. Kept for callers that still consume the old naming;
+ in this PR all entries are plain event-type names so the list is
+ identical to ``trigger_conditions``. A follow-up PR introduces CEL
+ expressions and overrides this to filter out non-type entries.
+ """
+ return self.trigger_conditions
+
@field_serializer("config")
def __serialize_config(self, config: Dict[str, Any]) -> Dict[str, Any] | None:
if config is None:
@@ -97,12 +107,12 @@ def __init__(
self,
name: str,
exec: Function,
- listen_event_types: List[str],
+ trigger_conditions: List[str],
config: Dict[str, Any] | None = None,
) -> None:
"""Action will check function signature when init."""
super().__init__(
- name=name, exec=exec, listen_event_types=listen_event_types, config=config
+ name=name, exec=exec, trigger_conditions=trigger_conditions, config=config
)
# TODO: Update expected signature after import State and Context.
self.exec.check_signature(Event, RunnerContext)
diff --git a/python/flink_agents/plan/actions/chat_model_action.py b/python/flink_agents/plan/actions/chat_model_action.py
index 188eb9e71..56ebf084e 100644
--- a/python/flink_agents/plan/actions/chat_model_action.py
+++ b/python/flink_agents/plan/actions/chat_model_action.py
@@ -454,7 +454,7 @@ async def process_chat_request_or_tool_response(
CHAT_MODEL_ACTION = Action(
name="chat_model_action",
exec=PythonFunction.from_callable(process_chat_request_or_tool_response),
- listen_event_types=[
+ trigger_conditions=[
ChatRequestEvent.EVENT_TYPE,
ToolResponseEvent.EVENT_TYPE,
],
diff --git a/python/flink_agents/plan/actions/context_retrieval_action.py b/python/flink_agents/plan/actions/context_retrieval_action.py
index d65f6d7f1..70d45ec3f 100644
--- a/python/flink_agents/plan/actions/context_retrieval_action.py
+++ b/python/flink_agents/plan/actions/context_retrieval_action.py
@@ -64,5 +64,5 @@ async def process_context_retrieval_request(event: Event, ctx: RunnerContext) ->
CONTEXT_RETRIEVAL_ACTION = Action(
name="context_retrieval_action",
exec=PythonFunction.from_callable(process_context_retrieval_request),
- listen_event_types=[ContextRetrievalRequestEvent.EVENT_TYPE],
+ trigger_conditions=[ContextRetrievalRequestEvent.EVENT_TYPE],
)
diff --git a/python/flink_agents/plan/actions/tool_call_action.py b/python/flink_agents/plan/actions/tool_call_action.py
index 6c7aaf270..1c8a8ce7d 100644
--- a/python/flink_agents/plan/actions/tool_call_action.py
+++ b/python/flink_agents/plan/actions/tool_call_action.py
@@ -63,5 +63,5 @@ async def process_tool_request(event: Event, ctx: RunnerContext) -> None:
TOOL_CALL_ACTION = Action(
name="tool_call_action",
exec=PythonFunction.from_callable(process_tool_request),
- listen_event_types=[ToolRequestEvent.EVENT_TYPE],
+ trigger_conditions=[ToolRequestEvent.EVENT_TYPE],
)
diff --git a/python/flink_agents/plan/agent_plan.py b/python/flink_agents/plan/agent_plan.py
index 24aca0caf..749a83f3f 100644
--- a/python/flink_agents/plan/agent_plan.py
+++ b/python/flink_agents/plan/agent_plan.py
@@ -145,7 +145,7 @@ def from_agent(agent: Agent, config: AgentConfiguration) -> "AgentPlan":
for action in _get_actions(agent) + BUILT_IN_ACTIONS:
assert action.name not in actions, f"Duplicate action name: {action.name}"
actions[action.name] = action
- for event_type in action.listen_event_types:
+ for event_type in action.trigger_conditions:
if event_type not in actions_by_event:
actions_by_event[event_type] = []
actions_by_event[event_type].append(action.name)
@@ -227,9 +227,9 @@ def _resolve_event_type(evt: Any) -> str:
def _action_marker(value: Any) -> tuple | None:
- """Return ``(inner_callable, listen_events, target)`` if ``value`` is an @action.
+ """Return ``(inner_callable, trigger_conditions, target)`` if ``value`` is @action.
- ``@action`` may set ``_listen_events`` on the outer wrapper (when ``@action``
+ ``@action`` may set ``_trigger_conditions`` on the outer wrapper (when ``@action``
is the outer decorator) or on ``__func__`` (when ``@staticmethod`` is outer
and ``@action`` inner). Accept either by checking both candidates.
"""
@@ -238,14 +238,14 @@ def _action_marker(value: Any) -> tuple | None:
return None
marker = (
value
- if hasattr(value, "_listen_events")
+ if hasattr(value, "_trigger_conditions")
else inner
- if hasattr(inner, "_listen_events")
+ if hasattr(inner, "_trigger_conditions")
else None
)
if marker is None:
return None
- return inner, marker._listen_events, getattr(marker, "_target", None)
+ return inner, marker._trigger_conditions, getattr(marker, "_target", None)
def _get_actions(agent: Agent) -> List[Action]:
@@ -279,7 +279,7 @@ def _get_actions(agent: Agent) -> List[Action]:
marker = _action_marker(value)
if marker is None:
continue
- inner, listen_events, target = marker
+ inner, trigger_conditions, target = marker
exec_ = (
_to_plan_function(target)
if target is not None
@@ -289,7 +289,9 @@ def _get_actions(agent: Agent) -> List[Action]:
Action(
name=name,
exec=exec_,
- listen_event_types=[_resolve_event_type(et) for et in listen_events],
+ trigger_conditions=[
+ _resolve_event_type(et) for et in trigger_conditions
+ ],
)
)
for name, action_tuple in agent.actions.items():
@@ -297,7 +299,7 @@ def _get_actions(agent: Agent) -> List[Action]:
Action(
name=name,
exec=_to_plan_function(action_tuple[1]),
- listen_event_types=[
+ trigger_conditions=[
_resolve_event_type(et)
for et in action_tuple[0]
],
diff --git a/python/flink_agents/plan/tests/compatibility/create_python_agent_plan_from_json.py b/python/flink_agents/plan/tests/compatibility/create_python_agent_plan_from_json.py
index c4397a84c..21096bc3d 100644
--- a/python/flink_agents/plan/tests/compatibility/create_python_agent_plan_from_json.py
+++ b/python/flink_agents/plan/tests/compatibility/create_python_agent_plan_from_json.py
@@ -51,7 +51,7 @@
event,
runner_context,
]
- listen_event_types1 = action1.listen_event_types
+ listen_event_types1 = action1.trigger_conditions
assert listen_event_types1 == [input_event]
# check the second action
@@ -65,7 +65,7 @@
event,
runner_context,
]
- listen_event_types2 = action2.listen_event_types
+ listen_event_types2 = action2.trigger_conditions
assert sorted(listen_event_types2) == [
my_event,
input_event,
diff --git a/python/flink_agents/plan/tests/compatibility/python_agent_plan_compatibility_test_agent.py b/python/flink_agents/plan/tests/compatibility/python_agent_plan_compatibility_test_agent.py
index 5080cefbe..dca9197ca 100644
--- a/python/flink_agents/plan/tests/compatibility/python_agent_plan_compatibility_test_agent.py
+++ b/python/flink_agents/plan/tests/compatibility/python_agent_plan_compatibility_test_agent.py
@@ -21,7 +21,8 @@
from flink_agents.api.chat_message import ChatMessage
from flink_agents.api.chat_models.chat_model import BaseChatModelSetup
from flink_agents.api.decorators import action, chat_model_setup, tool
-from flink_agents.api.events.event import Event, InputEvent
+from flink_agents.api.events.event import Event
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.resource import ResourceDescriptor
from flink_agents.api.runner_context import RunnerContext
@@ -51,7 +52,7 @@ def chat(self, messages: Sequence[ChatMessage], **kwargs: Any) -> ChatMessage:
class PythonAgentPlanCompatibilityTestAgent(Agent):
"""Agent for generating python agent plan json."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def first_action(event: Event, ctx: RunnerContext) -> None:
"""Test implementation."""
diff --git a/python/flink_agents/plan/tests/resources/action.json b/python/flink_agents/plan/tests/resources/action.json
index 7ec09d6c0..09393e475 100644
--- a/python/flink_agents/plan/tests/resources/action.json
+++ b/python/flink_agents/plan/tests/resources/action.json
@@ -5,7 +5,7 @@
"module": "flink_agents.plan.tests.test_action",
"qualname": "legal_signature"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_input_event"
],
"config": {
diff --git a/python/flink_agents/plan/tests/resources/agent_plan.json b/python/flink_agents/plan/tests/resources/agent_plan.json
index 9f9a3f416..6e545ab1d 100644
--- a/python/flink_agents/plan/tests/resources/agent_plan.json
+++ b/python/flink_agents/plan/tests/resources/agent_plan.json
@@ -7,7 +7,7 @@
"module": "flink_agents.plan.tests.test_agent_plan",
"qualname": "MyAgent.first_action"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_input_event"
],
"config": null
@@ -19,7 +19,7 @@
"module": "flink_agents.plan.tests.test_agent_plan",
"qualname": "MyAgent.second_action"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_input_event",
"_my_event"
],
@@ -32,7 +32,7 @@
"module": "flink_agents.plan.actions.chat_model_action",
"qualname": "process_chat_request_or_tool_response"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_chat_request_event",
"_tool_response_event"
],
@@ -45,7 +45,7 @@
"module": "flink_agents.plan.actions.tool_call_action",
"qualname": "process_tool_request"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_tool_request_event"
],
"config": null
@@ -57,7 +57,7 @@
"module": "flink_agents.plan.actions.context_retrieval_action",
"qualname": "process_context_retrieval_request"
},
- "listen_event_types": [
+ "trigger_conditions": [
"_context_retrieval_request_event"
],
"config": null
diff --git a/python/flink_agents/plan/tests/test_action.py b/python/flink_agents/plan/tests/test_action.py
index 5a75f7186..f93ee4531 100644
--- a/python/flink_agents/plan/tests/test_action.py
+++ b/python/flink_agents/plan/tests/test_action.py
@@ -40,7 +40,7 @@ def test_action_signature_legal() -> None:
Action(
name="legal",
exec=PythonFunction.from_callable(legal_signature),
- listen_event_types=[InputEvent.EVENT_TYPE],
+ trigger_conditions=[InputEvent.EVENT_TYPE],
)
@@ -49,7 +49,7 @@ def test_action_signature_illegal() -> None:
Action(
name="illegal",
exec=PythonFunction.from_callable(illegal_signature),
- listen_event_types=[InputEvent.EVENT_TYPE],
+ trigger_conditions=[InputEvent.EVENT_TYPE],
)
@@ -59,7 +59,7 @@ def action() -> Action:
return Action(
name="legal",
exec=func,
- listen_event_types=[InputEvent.EVENT_TYPE],
+ trigger_conditions=[InputEvent.EVENT_TYPE],
config={
"output_schema": OutputSchema(
output_schema=RowTypeInfo(
@@ -88,7 +88,7 @@ def test_action_deserialize(action: Action) -> None:
expected_json = f.read()
action = Action.model_validate_json(expected_json)
assert action.name == "legal"
- assert action.listen_event_types == ["_input_event"]
+ assert action.trigger_conditions== ["_input_event"]
func = action.exec
assert func.module == "flink_agents.plan.tests.test_action"
assert func.qualname == "legal_signature"
@@ -103,7 +103,7 @@ def test_action_deserialize_java_shape_config_unwraps_primitives() -> None:
"module": "flink_agents.plan.tests.test_action",
"qualname": "legal_signature",
},
- "listen_event_types": ["_input_event"],
+ "trigger_conditions": ["_input_event"],
"config": {
"__config_type__": "java",
"timeout_sec": {"@class": "java.lang.Integer", "value": 30},
diff --git a/python/flink_agents/plan/tests/test_agent_plan_cross_language.py b/python/flink_agents/plan/tests/test_agent_plan_cross_language.py
index fbf467116..7b144e57b 100644
--- a/python/flink_agents/plan/tests/test_agent_plan_cross_language.py
+++ b/python/flink_agents/plan/tests/test_agent_plan_cross_language.py
@@ -87,7 +87,7 @@ def test_compile_agent_with_python_function_descriptor() -> None:
)
assert action.exec.module == pf.module
assert action.exec.qualname == pf.qualname
- assert action.listen_event_types == [InputEvent.EVENT_TYPE]
+ assert action.trigger_conditions== [InputEvent.EVENT_TYPE]
def test_compile_agent_with_java_function_descriptor() -> None:
@@ -110,7 +110,7 @@ def test_compile_agent_with_java_function_descriptor() -> None:
assert action.exec.qualname == jf.qualname
assert action.exec.method_name == jf.method_name
assert list(action.exec.parameter_types) == list(jf.parameter_types)
- assert action.listen_event_types == [InputEvent.EVENT_TYPE]
+ assert action.trigger_conditions== [InputEvent.EVENT_TYPE]
def test_python_plan_compile_does_not_validate_java_class_exists() -> None:
@@ -318,7 +318,7 @@ def test_python_plan_with_java_action_matches_runtime_operator_wire_shape() -> N
handle_block = emitted["actions"]["handle"]
assert handle_block["name"] == "handle"
- assert handle_block["listen_event_types"] == [InputEvent.EVENT_TYPE]
+ assert handle_block["trigger_conditions"] == [InputEvent.EVENT_TYPE]
assert handle_block["config"] is None
assert handle_block["exec"] == {
"func_type": "JavaFunction",
@@ -340,7 +340,7 @@ def test_python_preserves_conf_data_types_and_event_ordering() -> None:
"module": _dummy_action.__module__,
"qualname": _dummy_action.__qualname__,
},
- "listen_event_types": [InputEvent.EVENT_TYPE],
+ "trigger_conditions": [InputEvent.EVENT_TYPE],
"config": None,
},
"second": {
@@ -350,7 +350,7 @@ def test_python_preserves_conf_data_types_and_event_ordering() -> None:
"module": _dummy_action.__module__,
"qualname": _dummy_action.__qualname__,
},
- "listen_event_types": [InputEvent.EVENT_TYPE],
+ "trigger_conditions": [InputEvent.EVENT_TYPE],
"config": None,
},
},
diff --git a/python/flink_agents/runtime/tests/test_built_in_actions.py b/python/flink_agents/runtime/tests/test_built_in_actions.py
index b7eb5a294..057291c15 100644
--- a/python/flink_agents/runtime/tests/test_built_in_actions.py
+++ b/python/flink_agents/runtime/tests/test_built_in_actions.py
@@ -33,6 +33,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.prompts.prompt import Prompt
from flink_agents.api.resource import ResourceDescriptor, ResourceType
@@ -171,7 +172,7 @@ def add(a: int, b: int) -> int:
"""
return a + b
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process_input(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing input.
@@ -187,7 +188,7 @@ def process_input(event: Event, ctx: RunnerContext) -> None:
)
)
- @action(ChatResponseEvent.EVENT_TYPE)
+ @action(EventType.ChatResponseEvent)
@staticmethod
def process_chat_response(event: Event, ctx: RunnerContext) -> None:
"""User defined action for processing chat model response."""
diff --git a/python/flink_agents/runtime/tests/test_get_resource_in_action.py b/python/flink_agents/runtime/tests/test_get_resource_in_action.py
index f1f1e8e15..8c5988bb1 100644
--- a/python/flink_agents/runtime/tests/test_get_resource_in_action.py
+++ b/python/flink_agents/runtime/tests/test_get_resource_in_action.py
@@ -22,6 +22,7 @@
from flink_agents.api.chat_models.chat_model import BaseChatModelSetup
from flink_agents.api.decorators import action, chat_model_setup, tool
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.resource import ResourceDescriptor, ResourceType
from flink_agents.api.runner_context import RunnerContext
@@ -74,7 +75,7 @@ def mock_tool(input: str) -> str:
"""
return input + " mock tools just for testing."
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def mock_action(event: Event, ctx: RunnerContext) -> None:
input = InputEvent.from_event(event).input
diff --git a/python/flink_agents/runtime/tests/test_local_execution_environment.py b/python/flink_agents/runtime/tests/test_local_execution_environment.py
index 1c861b359..294549d22 100644
--- a/python/flink_agents/runtime/tests/test_local_execution_environment.py
+++ b/python/flink_agents/runtime/tests/test_local_execution_environment.py
@@ -35,6 +35,7 @@
)
from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.resource import ResourceDescriptor, ResourceType
from flink_agents.api.runner_context import RunnerContext
@@ -42,7 +43,7 @@
class Agent1(Agent):
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def increment(event: Event, ctx: RunnerContext): # noqa D102
input = InputEvent.from_event(event).input
@@ -51,7 +52,7 @@ def increment(event: Event, ctx: RunnerContext): # noqa D102
class Agent1WithAsync(Agent):
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
async def increment(event: Event, ctx: RunnerContext): # noqa D102
def my_func(value: int) -> int:
@@ -64,7 +65,7 @@ def my_func(value: int) -> int:
class Agent2(Agent):
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def decrease(event: Event, ctx: RunnerContext): # noqa D102
input = InputEvent.from_event(event).input
@@ -145,7 +146,7 @@ def test_local_execution_environment_call_from_list_twice() -> None:
class UnifiedEventAgent(Agent):
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def on_input(event: Event, ctx: RunnerContext) -> None:
ctx.send_event(
@@ -199,7 +200,7 @@ def data(self) -> str:
class MixedEventAgent(Agent):
"""Agent mixing subclassed and string-based event routing."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def start(event: Event, ctx: RunnerContext) -> None:
ctx.send_event(Step1Event(data=str(InputEvent.from_event(event).input)))
diff --git a/python/flink_agents/runtime/tests/test_runner_context_execute.py b/python/flink_agents/runtime/tests/test_runner_context_execute.py
index fd2a54733..abde15a63 100644
--- a/python/flink_agents/runtime/tests/test_runner_context_execute.py
+++ b/python/flink_agents/runtime/tests/test_runner_context_execute.py
@@ -20,6 +20,7 @@
from flink_agents.api.agents.agent import Agent
from flink_agents.api.decorators import action
from flink_agents.api.events.event import Event, InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
from flink_agents.api.execution_environment import AgentsExecutionEnvironment
from flink_agents.api.runner_context import RunnerContext
@@ -42,7 +43,7 @@ def raise_exception(msg: str) -> None:
class AgentWithDurableExecute(Agent):
"""Agent that uses synchronous durable_execute() method."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event using durable_execute()."""
@@ -54,7 +55,7 @@ def process(event: Event, ctx: RunnerContext) -> None:
class AgentWithMultipleDurableExecute(Agent):
"""Agent that makes multiple durable_execute() calls."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event with multiple durable_execute() calls."""
@@ -67,7 +68,7 @@ def process(event: Event, ctx: RunnerContext) -> None:
class AgentWithDurableExecuteAndAsync(Agent):
"""Agent that uses both durable_execute() and durable_execute_async()."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
async def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event using both durable_execute() and durable_execute_async()."""
@@ -82,7 +83,7 @@ async def process(event: Event, ctx: RunnerContext) -> None:
class AgentWithDurableExecuteException(Agent):
"""Agent that uses durable_execute() with a function that raises an exception."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event where durable_execute() raises an exception."""
@@ -96,7 +97,7 @@ def process(event: Event, ctx: RunnerContext) -> None:
class AgentWithKwargs(Agent):
"""Agent that uses durable_execute() with keyword arguments."""
- @action(InputEvent.EVENT_TYPE)
+ @action(EventType.InputEvent)
@staticmethod
def process(event: Event, ctx: RunnerContext) -> None:
"""Process an event using durable_execute() with kwargs."""
From edceda5b3b90956cd13e2e4f6e1e48f98a7b53e3 Mon Sep 17 00:00:00 2001
From: rosemaryYuan <91046107+rosemarYuan@users.noreply.github.com>
Date: Thu, 4 Jun 2026 21:01:17 +0800
Subject: [PATCH 04/13] [api][python] Slim EventType to constants + add Action
legacy JSON fallback
---
.../apache/flink/agents/api/EventType.java | 155 +---------
.../flink/agents/api/EventTypeTest.java | 280 +-----------------
python/flink_agents/api/events/event_type.py | 161 +++-------
.../flink_agents/api/tests/test_event_type.py | 64 ++--
python/flink_agents/plan/actions/action.py | 8 +-
5 files changed, 84 insertions(+), 584 deletions(-)
diff --git a/api/src/main/java/org/apache/flink/agents/api/EventType.java b/api/src/main/java/org/apache/flink/agents/api/EventType.java
index e0e825ad0..f486a6d0e 100644
--- a/api/src/main/java/org/apache/flink/agents/api/EventType.java
+++ b/api/src/main/java/org/apache/flink/agents/api/EventType.java
@@ -18,150 +18,27 @@
package org.apache.flink.agents.api;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-
/**
- * Compile-time constants for built-in event types and a runtime registry for user-defined events.
- *
- *
Usage in {@code @Action}:
+ * Compile-time constants for built-in event types, sourced from each {@code XxxEvent.EVENT_TYPE}.
*
- *
In CEL trigger conditions, {@code EventType} is exposed as a top-level map variable so {@code
+ * type == EventType.InputEvent} resolves the constant at evaluation time. {@link #allConstants()}
+ * enumerates the constant fields for that activation map and for plan-load validation.
*/
public final class EventType {
@@ -40,5 +50,25 @@ public final class EventType {
public static final String ContextRetrievalResponseEvent =
org.apache.flink.agents.api.event.ContextRetrievalResponseEvent.EVENT_TYPE;
+ /**
+ * Returns all built-in constants as an unmodifiable {@code name → event-type value} map.
+ * Enumerated reflectively from the {@code public static final String} fields of this class so
+ * newly added constants are picked up automatically. Iteration order is unspecified.
+ */
+ public static Map allConstants() {
+ Map constants = new LinkedHashMap<>();
+ for (Field field : EventType.class.getFields()) {
+ if (Modifier.isStatic(field.getModifiers()) && field.getType() == String.class) {
+ try {
+ constants.put(field.getName(), (String) field.get(null));
+ } catch (IllegalAccessException e) {
+ // Unreachable: getFields() only returns public fields.
+ throw new IllegalStateException("Cannot read EventType." + field.getName(), e);
+ }
+ }
+ }
+ return Collections.unmodifiableMap(constants);
+ }
+
private EventType() {}
}
diff --git a/api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java b/api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java
index 4ac5aa728..08c29f958 100644
--- a/api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java
+++ b/api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java
@@ -26,22 +26,28 @@
import org.apache.flink.agents.api.event.ToolResponseEvent;
import org.junit.jupiter.api.Test;
+import java.util.Map;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
/** Tests for {@link EventType}. */
class EventTypeTest {
@Test
- void builtInConstantsMatchEventClassConstants() {
- assertEquals(InputEvent.EVENT_TYPE, EventType.InputEvent);
- assertEquals(OutputEvent.EVENT_TYPE, EventType.OutputEvent);
- assertEquals(ChatRequestEvent.EVENT_TYPE, EventType.ChatRequestEvent);
- assertEquals(ChatResponseEvent.EVENT_TYPE, EventType.ChatResponseEvent);
- assertEquals(ToolRequestEvent.EVENT_TYPE, EventType.ToolRequestEvent);
- assertEquals(ToolResponseEvent.EVENT_TYPE, EventType.ToolResponseEvent);
+ void allConstantsEnumeratesEveryBuiltInConstant() {
+ Map constants = EventType.allConstants();
+ assertEquals(8, constants.size());
+ assertEquals(InputEvent.EVENT_TYPE, constants.get("InputEvent"));
+ assertEquals(OutputEvent.EVENT_TYPE, constants.get("OutputEvent"));
+ assertEquals(ChatRequestEvent.EVENT_TYPE, constants.get("ChatRequestEvent"));
+ assertEquals(ChatResponseEvent.EVENT_TYPE, constants.get("ChatResponseEvent"));
+ assertEquals(ToolRequestEvent.EVENT_TYPE, constants.get("ToolRequestEvent"));
+ assertEquals(ToolResponseEvent.EVENT_TYPE, constants.get("ToolResponseEvent"));
assertEquals(
- ContextRetrievalRequestEvent.EVENT_TYPE, EventType.ContextRetrievalRequestEvent);
+ ContextRetrievalRequestEvent.EVENT_TYPE,
+ constants.get("ContextRetrievalRequestEvent"));
assertEquals(
- ContextRetrievalResponseEvent.EVENT_TYPE, EventType.ContextRetrievalResponseEvent);
+ ContextRetrievalResponseEvent.EVENT_TYPE,
+ constants.get("ContextRetrievalResponseEvent"));
}
}
From 7a34461f38106a8597a85574351f8f90bd72ccde Mon Sep 17 00:00:00 2001
From: rosemaryYuan <91046107+rosemarYuan@users.noreply.github.com>
Date: Wed, 10 Jun 2026 02:11:25 +0800
Subject: [PATCH 08/13] [plan][runtime] Implement CEL-based @Action
triggerConditions
---
examples/pom.xml | 7 +
plan/pom.xml | 5 +
.../apache/flink/agents/plan/AgentPlan.java | 60 ++++-
.../flink/agents/plan/actions/Action.java | 78 +++++-
.../agents/plan/condition/CelMacroPolicy.java | 126 +++++++++
.../plan/condition/ParsedCondition.java | 140 ++++++++++
pom.xml | 1 +
runtime/pom.xml | 13 +
.../runtime/condition/ActionRouter.java | 124 +++++++++
.../condition/CelConditionEvaluator.java | 252 ++++++++++++++++++
.../condition/CelExpressionFacade.java | 216 +++++++++++++++
.../operator/ActionExecutionOperator.java | 2 +-
.../agents/runtime/operator/EventRouter.java | 15 +-
13 files changed, 1015 insertions(+), 24 deletions(-)
create mode 100644 plan/src/main/java/org/apache/flink/agents/plan/condition/CelMacroPolicy.java
create mode 100644 plan/src/main/java/org/apache/flink/agents/plan/condition/ParsedCondition.java
create mode 100644 runtime/src/main/java/org/apache/flink/agents/runtime/condition/ActionRouter.java
create mode 100644 runtime/src/main/java/org/apache/flink/agents/runtime/condition/CelConditionEvaluator.java
create mode 100644 runtime/src/main/java/org/apache/flink/agents/runtime/condition/CelExpressionFacade.java
diff --git a/examples/pom.xml b/examples/pom.xml
index 26f7f9f20..8fdaa7ad0 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -65,6 +65,13 @@ under the License.
${project.version}provided
+
+
+
+ dev.cel
+ cel
+ ${cel.version}
+
\ No newline at end of file
diff --git a/plan/pom.xml b/plan/pom.xml
index 02df3c2c3..8281fd1ab 100644
--- a/plan/pom.xml
+++ b/plan/pom.xml
@@ -50,6 +50,11 @@ under the License.
com.fasterxml.jackson.corejackson-databind
+
+ dev.cel
+ cel
+ ${cel.version}
+ io.github.bonede
diff --git a/plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java b/plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java
index e9d59083a..e76a6c76a 100644
--- a/plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java
+++ b/plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java
@@ -85,6 +85,13 @@ public class AgentPlan implements Serializable {
/** Mapping from event type string to list of actions that should be triggered by the event. */
private Map> actionsByEvent;
+ /**
+ * Actions that carry at least one CEL expression in their trigger conditions and therefore
+ * require runtime CEL evaluation in addition to (or instead of) the actionsByEvent reverse map.
+ * Rebuilt by {@link #rebuildActionsWithCel()} after construction and deserialization.
+ */
+ private transient List actionsWithCel = new ArrayList<>();
+
/** Two-level mapping of resource type to resource name to resource provider. */
private Map> resourceProviders;
@@ -95,6 +102,7 @@ public AgentPlan(Map actions, Map> actionsB
this.actionsByEvent = actionsByEvent;
this.resourceProviders = new HashMap<>();
this.config = new AgentConfiguration();
+ rebuildActionsWithCel();
}
public AgentPlan(
@@ -105,6 +113,7 @@ public AgentPlan(
this.actionsByEvent = actionsByEvent;
this.resourceProviders = resourceProviders;
this.config = new AgentConfiguration();
+ rebuildActionsWithCel();
}
public AgentPlan(
@@ -116,6 +125,7 @@ public AgentPlan(
this.actionsByEvent = actionsByEvent;
this.resourceProviders = resourceProviders;
this.config = config;
+ rebuildActionsWithCel();
}
/**
@@ -180,6 +190,7 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE
this.actionsByEvent = agentPlan.getActionsByEvent();
this.resourceProviders = agentPlan.getResourceProviders();
this.config = agentPlan.getConfig();
+ rebuildActionsWithCel();
}
private void extractActions(
@@ -199,24 +210,51 @@ private void extractActions(
// Create an Action
Action action = new Action(actionName, function, triggerConditions, config);
-
- // Add to actions map
- actions.put(action.getName(), action);
-
- // Add to actionsByEvent map
- for (String eventTypeName : triggerConditions) {
- actionsByEvent.computeIfAbsent(eventTypeName, k -> new ArrayList<>()).add(action);
- }
+ registerAction(action);
}
private void addBuiltAction(Action action) {
- // Add to actions map
- actions.put(action.getName(), action);
+ registerAction(action);
+ }
- // Add to actionsByEvent map
+ /**
+ * Registers an action into both {@link #actions}, {@link #actionsByEvent} (using type-only
+ * entries from {@link Action#getListenEventTypes()}), and {@link #actionsWithCel} if the action
+ * carries any CEL expression.
+ */
+ private void registerAction(Action action) {
+ actions.put(action.getName(), action);
for (String eventTypeName : action.getListenEventTypes()) {
actionsByEvent.computeIfAbsent(eventTypeName, k -> new ArrayList<>()).add(action);
}
+ if (action.hasCelCondition()) {
+ actionsWithCel.add(action);
+ }
+ }
+
+ /**
+ * Rebuilds {@link #actionsWithCel} from {@link #actions}. Used after deserialization (where
+ * actionsWithCel is transient).
+ */
+ private void rebuildActionsWithCel() {
+ if (actionsWithCel == null) {
+ actionsWithCel = new ArrayList<>();
+ } else {
+ actionsWithCel.clear();
+ }
+ if (actions == null) {
+ return;
+ }
+ for (Action action : actions.values()) {
+ if (action.hasCelCondition()) {
+ actionsWithCel.add(action);
+ }
+ }
+ }
+
+ /** Returns the list of actions that require CEL evaluation. */
+ public List getActionsWithCel() {
+ return actionsWithCel;
}
private void extractActionsFromAgent(Agent agent) throws Exception {
diff --git a/plan/src/main/java/org/apache/flink/agents/plan/actions/Action.java b/plan/src/main/java/org/apache/flink/agents/plan/actions/Action.java
index 18e3b83b4..4f8da0155 100644
--- a/plan/src/main/java/org/apache/flink/agents/plan/actions/Action.java
+++ b/plan/src/main/java/org/apache/flink/agents/plan/actions/Action.java
@@ -23,11 +23,16 @@
import org.apache.flink.agents.api.Event;
import org.apache.flink.agents.api.context.RunnerContext;
import org.apache.flink.agents.plan.Function;
+import org.apache.flink.agents.plan.condition.ParsedCondition;
+import org.apache.flink.agents.plan.condition.ParsedCondition.CelExpression;
+import org.apache.flink.agents.plan.condition.ParsedCondition.TypeMatch;
import org.apache.flink.agents.plan.serializer.ActionJsonDeserializer;
import org.apache.flink.agents.plan.serializer.ActionJsonSerializer;
import javax.annotation.Nullable;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -35,8 +40,8 @@
/**
* Representation of an agent action with unified trigger conditions.
*
- *
Each entry of {@code triggerConditions} is an event type name string. Multiple entries combine
- * with OR.
+ *
Each entry of {@code triggerConditions} is either a plain event-type name (matched against
+ * {@code event.getType()}) or a CEL expression. Multiple entries combine with OR.
*/
@JsonSerialize(using = ActionJsonSerializer.class)
@JsonDeserialize(using = ActionJsonDeserializer.class)
@@ -45,6 +50,13 @@ public class Action {
private final Function exec;
private final List triggerConditions;
+ /**
+ * Derived from {@link #triggerConditions}; not part of the persisted state because the CEL AST
+ * objects inside CelExpression are not Kryo-serialisable. Rebuilt lazily on first access after
+ * deserialization via {@link #parsedConditions()}.
+ */
+ private transient List parsedConditions;
+
// TODO: support nested map/list with non primitive type value.
@Nullable private final Map config;
@@ -54,13 +66,35 @@ public Action(
List triggerConditions,
@Nullable Map config)
throws Exception {
+ if (triggerConditions == null || triggerConditions.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Action '" + name + "' must have at least one entry in 'triggerConditions'");
+ }
this.name = name;
this.exec = exec;
this.triggerConditions = triggerConditions;
this.config = config;
+
+ // Eagerly build (and validate) parsedConditions at construction time so any classifier
+ // error fires early. Stored transiently; rebuilt on first access after deserialization.
+ this.parsedConditions = buildParsedConditions(name, triggerConditions);
+
exec.checkSignature(new Class[] {Event.class, RunnerContext.class});
}
+ private static List buildParsedConditions(
+ String name, List triggerConditions) {
+ List parsed = new ArrayList<>(triggerConditions.size());
+ for (String entry : triggerConditions) {
+ if (entry == null || entry.isEmpty()) {
+ throw new IllegalArgumentException(
+ "Action '" + name + "' has a null/empty trigger entry");
+ }
+ parsed.add(ParsedCondition.classify(entry));
+ }
+ return Collections.unmodifiableList(parsed);
+ }
+
public Action(String name, Function exec, List triggerConditions) throws Exception {
this(name, exec, triggerConditions, null);
}
@@ -73,19 +107,43 @@ public Function getExec() {
return exec;
}
- /** Returns the full trigger conditions list. */
+ /** Returns the full trigger conditions list (type names and CEL expressions). */
public List getTriggerConditions() {
return triggerConditions;
}
- /**
- * Returns event-type names. Kept for callers that still consume the old naming; in this PR all
- * trigger entries are plain event-type names so the list is identical to {@link
- * #getTriggerConditions()}. A follow-up PR introduces CEL expressions and overrides this to
- * filter out non-type entries.
- */
+ /** Returns parsed conditions in declaration order (unmodifiable). */
+ public List getParsedConditions() {
+ return parsedConditions();
+ }
+
+ /** Lazily rebuilds parsedConditions on first access after deserialization. */
+ private synchronized List parsedConditions() {
+ if (parsedConditions == null) {
+ parsedConditions = buildParsedConditions(name, triggerConditions);
+ }
+ return parsedConditions;
+ }
+
+ /** Returns event-type names extracted from {@link TypeMatch} entries (CEL entries skipped). */
public List getListenEventTypes() {
- return triggerConditions;
+ List typeNames = new ArrayList<>();
+ for (ParsedCondition pc : parsedConditions()) {
+ if (pc instanceof TypeMatch) {
+ typeNames.add(pc.source());
+ }
+ }
+ return typeNames;
+ }
+
+ /** Returns whether this action carries at least one CEL expression entry. */
+ public boolean hasCelCondition() {
+ for (ParsedCondition pc : parsedConditions()) {
+ if (pc instanceof CelExpression) {
+ return true;
+ }
+ }
+ return false;
}
@Nullable
diff --git a/plan/src/main/java/org/apache/flink/agents/plan/condition/CelMacroPolicy.java b/plan/src/main/java/org/apache/flink/agents/plan/condition/CelMacroPolicy.java
new file mode 100644
index 000000000..46597b91c
--- /dev/null
+++ b/plan/src/main/java/org/apache/flink/agents/plan/condition/CelMacroPolicy.java
@@ -0,0 +1,126 @@
+/*
+ * 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.plan.condition;
+
+import com.google.common.collect.ImmutableList;
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.ast.CelExpr;
+import dev.cel.common.navigation.CelNavigableAst;
+import dev.cel.parser.CelMacro;
+import dev.cel.parser.CelMacroExprFactory;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * CEL macro rules for trigger conditions: the custom {@code has()} macro, the macro whitelist, and
+ * the reserved identifiers.
+ */
+public final class CelMacroPolicy {
+
+ /**
+ * Custom parse-time {@code has()} macro: {@code has(a.b)} tests field presence on {@code a};
+ * {@code has(score)} tests presence of key {@code score} in the {@code attributes} map.
+ */
+ public static final CelMacro HAS = CelMacro.newGlobalMacro("has", 1, CelMacroPolicy::expandHas);
+
+ private static Optional expandHas(
+ CelMacroExprFactory exprFactory, CelExpr target, ImmutableList arguments) {
+ CelExpr arg = arguments.get(0);
+ if (arg.exprKind().getKind() == CelExpr.ExprKind.Kind.SELECT && !arg.select().testOnly()) {
+ // has(a.b) → field presence on operand a.
+ return Optional.of(
+ exprFactory.newSelect(arg.select().operand(), arg.select().field(), true));
+ }
+ if (arg.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT
+ && !RESERVED_IDENTIFIERS.contains(arg.ident().name())) {
+ // has(score) → key presence in the attributes map.
+ return Optional.of(
+ exprFactory.newSelect(
+ exprFactory.newIdentifier("attributes"), arg.ident().name(), true));
+ }
+ return Optional.of(
+ exprFactory.reportError(
+ "invalid argument to has() macro: expected a field selection like"
+ + " has(a.b) or an attribute name like has(score)"));
+ }
+
+ /** The complete set of CEL standard macro names. */
+ public static final Set CEL_STANDARD_MACROS =
+ Set.of("has", "exists", "exists_one", "all", "filter", "map");
+
+ /** Macros allowed in trigger condition expressions. */
+ public static final Set ALLOWED_MACROS = Set.of("has");
+
+ /** Returns the first disallowed macro call found in {@code ast}, or empty if none. */
+ public static Optional findFirstDisallowedMacro(CelAbstractSyntaxTree ast) {
+ return CelNavigableAst.fromAst(ast)
+ .getRoot()
+ .allNodes()
+ .filter(node -> node.getKind() == CelExpr.ExprKind.Kind.CALL)
+ .map(node -> node.expr().call().function())
+ .filter(fn -> CEL_STANDARD_MACROS.contains(fn) && !ALLOWED_MACROS.contains(fn))
+ .findFirst();
+ }
+
+ /** Formats the disallowed-macro error message; kept aligned with the Python template. */
+ public static String formatDisallowedMessage(String macro, String source) {
+ return "CEL expression uses disallowed macro '"
+ + macro
+ + "': \""
+ + source
+ + "\". Only allows: "
+ + new TreeSet<>(ALLOWED_MACROS)
+ + ".";
+ }
+
+ /** Names rejected as bare event-type aliases and never shadowed by user attributes. */
+ public static final Set RESERVED_IDENTIFIERS;
+
+ static {
+ Set set =
+ new HashSet<>(
+ Set.of(
+ // Framework-owned activation variables.
+ "type",
+ "attributes",
+ "EventType",
+ // CEL literals.
+ "true",
+ "false",
+ "null",
+ // CEL operators / type-conversion functions / container types.
+ "in",
+ "int",
+ "uint",
+ "double",
+ "string",
+ "bool",
+ "bytes",
+ "list"));
+ // All CEL standard macro names.
+ set.addAll(CEL_STANDARD_MACROS);
+ RESERVED_IDENTIFIERS = Collections.unmodifiableSet(set);
+ }
+
+ private CelMacroPolicy() {}
+}
diff --git a/plan/src/main/java/org/apache/flink/agents/plan/condition/ParsedCondition.java b/plan/src/main/java/org/apache/flink/agents/plan/condition/ParsedCondition.java
new file mode 100644
index 000000000..3e5d903da
--- /dev/null
+++ b/plan/src/main/java/org/apache/flink/agents/plan/condition/ParsedCondition.java
@@ -0,0 +1,140 @@
+/*
+ * 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.plan.condition;
+
+import dev.cel.common.CelAbstractSyntaxTree;
+import dev.cel.common.CelValidationException;
+import dev.cel.common.ast.CelExpr;
+import dev.cel.parser.CelParser;
+import dev.cel.parser.CelParserFactory;
+
+import java.util.Objects;
+
+/**
+ * A parsed {@code Action.triggerConditions} entry — either {@link TypeMatch} or {@link
+ * CelExpression}. {@link #classify} turns a raw entry string into one of the two.
+ */
+public interface ParsedCondition {
+
+ /** Original user-written entry string. */
+ String source();
+
+ /** Parser with the custom {@code has()} macro; same dialect as the runtime facade parser. */
+ CelParser CEL_PARSER =
+ CelParserFactory.standardCelParserBuilder().addMacros(CelMacroPolicy.HAS).build();
+
+ /**
+ * Parses a {@code triggerConditions} entry: a non-reserved bare-identifier root becomes a
+ * {@link TypeMatch}, everything else a {@link CelExpression}.
+ */
+ static ParsedCondition classify(String source) {
+ if (source == null || source.isEmpty()) {
+ throw new IllegalArgumentException(
+ "ParsedCondition.classify: source must be non-null and non-empty");
+ }
+ CelAbstractSyntaxTree ast;
+ try {
+ ast = CEL_PARSER.parse(source).getAst();
+ } catch (CelValidationException e) {
+ throw new IllegalArgumentException(
+ "Invalid CEL expression: \"" + source + "\" — " + e.getMessage(), e);
+ }
+ CelExpr root = ast.getExpr();
+ if (root.exprKind().getKind() == CelExpr.ExprKind.Kind.IDENT) {
+ String name = root.ident().name();
+ if (CelMacroPolicy.RESERVED_IDENTIFIERS.contains(name)) {
+ throw new IllegalArgumentException(
+ "'"
+ + name
+ + "' is a CEL reserved keyword and cannot be used as an "
+ + "event type name. Did you mean: @action(\""
+ + name
+ + " == 'xxx'\") or @action(\"attributes."
+ + name
+ + "\")?");
+ }
+ return new TypeMatch(name);
+ }
+ return new CelExpression(source);
+ }
+
+ /** A plain event-type match. {@link #source()} is compared against {@code Event.getType()}. */
+ final class TypeMatch implements ParsedCondition {
+
+ private final String source;
+
+ public TypeMatch(String source) {
+ this.source = source;
+ }
+
+ @Override
+ public String source() {
+ return source;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof TypeMatch)) return false;
+ return source.equals(((TypeMatch) o).source);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(source);
+ }
+
+ @Override
+ public String toString() {
+ return "TypeMatch{source=" + source + "}";
+ }
+ }
+
+ /** A CEL expression. Source-only; compiled elsewhere. */
+ final class CelExpression implements ParsedCondition {
+
+ private final String source;
+
+ public CelExpression(String source) {
+ this.source = source;
+ }
+
+ @Override
+ public String source() {
+ return source;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof CelExpression)) return false;
+ return source.equals(((CelExpression) o).source);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(source);
+ }
+
+ @Override
+ public String toString() {
+ return "CelExpression{source=" + source + "}";
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index e77b0a51c..916ac91c2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,6 +52,7 @@ under the License.
3.27.75.14.21.15.4
+ 0.12.0true
diff --git a/runtime/pom.xml b/runtime/pom.xml
index 422c5c320..c7b78ccab 100644
--- a/runtime/pom.xml
+++ b/runtime/pom.xml
@@ -193,9 +193,22 @@ under the License.
jackson-datatype-jsr310${jackson.version}
+
+ dev.cel
+ cel
+ ${cel.version}
+
+
+
+ src/test/resources
+
+
+ ${project.basedir}/../e2e-test/cel-fixtures
+
+
diff --git a/runtime/src/main/java/org/apache/flink/agents/runtime/condition/ActionRouter.java b/runtime/src/main/java/org/apache/flink/agents/runtime/condition/ActionRouter.java
new file mode 100644
index 000000000..3b941a7dc
--- /dev/null
+++ b/runtime/src/main/java/org/apache/flink/agents/runtime/condition/ActionRouter.java
@@ -0,0 +1,124 @@
+/*
+ * 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.runtime.condition;
+
+import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.plan.AgentPlan;
+import org.apache.flink.agents.plan.actions.Action;
+import org.apache.flink.agents.plan.condition.ParsedCondition;
+import org.apache.flink.agents.plan.condition.ParsedCondition.CelExpression;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Routes an event to matching actions: type-index fast path first, then CEL slow path.
+ *
+ *
Each action fires at most once per event; typed hits ordered before CEL hits.
+ */
+public final class ActionRouter {
+
+ private final AgentPlan agentPlan;
+
+ /** Null when the plan contains no CEL expressions. */
+ private CelConditionEvaluator conditionEvaluator;
+
+ public ActionRouter(AgentPlan agentPlan) {
+ if (agentPlan == null) {
+ throw new IllegalArgumentException("ActionRouter: agentPlan must not be null");
+ }
+ this.agentPlan = agentPlan;
+ }
+
+ /** Pre-compiles all CEL expressions in the plan. */
+ public void open() {
+ List celExpressions = new ArrayList<>();
+ for (Action action : agentPlan.getActions().values()) {
+ for (ParsedCondition pc : action.getParsedConditions()) {
+ if (pc instanceof CelExpression) {
+ celExpressions.add((CelExpression) pc);
+ }
+ }
+ }
+ if (celExpressions.isEmpty()) {
+ return;
+ }
+ conditionEvaluator = new CelConditionEvaluator();
+ conditionEvaluator.initPrograms(celExpressions);
+ }
+
+ /** Returns actions to fire for {@code event}: typed hits first, then CEL hits. */
+ public List route(Event event) {
+ List typedHits =
+ agentPlan
+ .getActionsByEvent()
+ .getOrDefault(event.getType(), Collections.emptyList());
+
+ // CEL candidates = actions with at least one CEL entry, excluding those already
+ // matched by typed routing for this event type. This avoids double-firing.
+ List celCandidates;
+ List withCel = agentPlan.getActionsWithCel();
+ if (withCel.isEmpty()) {
+ celCandidates = Collections.emptyList();
+ } else {
+ celCandidates = new ArrayList<>();
+ for (Action a : withCel) {
+ if (!typedHits.contains(a)) {
+ celCandidates.add(a);
+ }
+ }
+ }
+
+ if (celCandidates.isEmpty()) {
+ return typedHits;
+ }
+
+ // Preserves typed-first ordering and deduplicates.
+ LinkedHashSet matched = new LinkedHashSet<>(typedHits);
+
+ Map activation = null;
+ for (Action a : celCandidates) {
+ // Within-action OR: first matching CEL expression admits the action.
+ for (ParsedCondition pc : a.getParsedConditions()) {
+ if (!(pc instanceof CelExpression)) {
+ continue;
+ }
+ if (activation == null) {
+ activation = conditionEvaluator.createActivation(event);
+ }
+ if (conditionEvaluator.evaluate((CelExpression) pc, activation)) {
+ matched.add(a);
+ break;
+ }
+ }
+ }
+ return new ArrayList<>(matched);
+ }
+
+ /** Idempotent. */
+ public void close() {
+ if (conditionEvaluator != null) {
+ conditionEvaluator.close();
+ conditionEvaluator = null;
+ }
+ }
+}
diff --git a/runtime/src/main/java/org/apache/flink/agents/runtime/condition/CelConditionEvaluator.java b/runtime/src/main/java/org/apache/flink/agents/runtime/condition/CelConditionEvaluator.java
new file mode 100644
index 000000000..3734b2e15
--- /dev/null
+++ b/runtime/src/main/java/org/apache/flink/agents/runtime/condition/CelConditionEvaluator.java
@@ -0,0 +1,252 @@
+/*
+ * 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.runtime.condition;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.cel.runtime.CelEvaluationException;
+import dev.cel.runtime.CelRuntime;
+import org.apache.flink.agents.api.Event;
+import org.apache.flink.agents.plan.condition.ParsedCondition.CelExpression;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Evaluates CEL condition expressions against event data. */
+public class CelConditionEvaluator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CelConditionEvaluator.class);
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ /**
+ * Behaviour for both runtime exceptions and non-Boolean return values — both mean "no verdict".
+ */
+ public enum EvaluationFailurePolicy {
+ /** Log WARN and treat the action as not matching. */
+ WARN_AND_SKIP,
+ /** Rethrow as {@link IllegalStateException}; triggers Flink task failover. */
+ FAIL
+ }
+
+ /** Frozen after {@link #initPrograms}; cleared by {@link #close}. */
+ @Nullable private Map programCache;
+
+ private final EvaluationFailurePolicy failurePolicy;
+
+ public CelConditionEvaluator() {
+ this(EvaluationFailurePolicy.WARN_AND_SKIP);
+ }
+
+ public CelConditionEvaluator(EvaluationFailurePolicy failurePolicy) {
+ this.failurePolicy = failurePolicy;
+ }
+
+ /** Pre-compiles {@code expressions} and freezes the cache. Nulls are skipped. */
+ public void initPrograms(Collection expressions) {
+ Map programs = new HashMap<>();
+ for (CelExpression expression : expressions) {
+ if (expression == null) {
+ continue;
+ }
+ String source = expression.source();
+ programs.computeIfAbsent(source, CelExpressionFacade::toProgram);
+ }
+ this.programCache = Collections.unmodifiableMap(programs);
+ }
+
+ public void close() {
+ programCache = null;
+ }
+
+ /** Evaluates {@code expression} (which must have been pre-compiled). Null returns true. */
+ public boolean evaluate(@Nullable CelExpression expression, Map activation) {
+ if (expression == null) {
+ return true;
+ }
+ String source = expression.source();
+ try {
+ CelRuntime.Program program = programCache.get(source);
+ if (program == null) {
+ throw new IllegalStateException(
+ "CEL condition was not pre-compiled via initPrograms(): \""
+ + source
+ + "\"");
+ }
+ return evaluateProgram(source, program, activation);
+ } catch (CelEvaluationException e) {
+ if (failurePolicy == EvaluationFailurePolicy.FAIL) {
+ throw new IllegalStateException(
+ "CEL condition evaluation failed for '" + source + "'", e);
+ }
+ LOG.warn("CEL condition evaluation failed for '{}', skipping action", source, e);
+ return false;
+ }
+ }
+
+ private boolean evaluateProgram(
+ String condition, CelRuntime.Program program, Map activation)
+ throws CelEvaluationException {
+ Object result = program.eval(activation);
+ if (result instanceof Boolean) {
+ return (Boolean) result;
+ }
+ String msg =
+ String.format(
+ "CEL condition '%s' returned non-boolean type %s, treating as false",
+ condition, result == null ? "null" : result.getClass().getName());
+ if (failurePolicy == EvaluationFailurePolicy.FAIL) {
+ throw new IllegalStateException(msg);
+ }
+ LOG.warn(msg);
+ return false;
+ }
+
+ /**
+ * Builds the CEL activation. Contract (mirror of Python {@code cel_facade}):
+ *
+ *
+ *
{@code type} and {@code EventType} are framework-owned and always win.
+ *
{@code attributes} holds the single-level merge of user data: {@code output.*} subkeys,
+ * then root attribute fields, then {@code input.*} subkeys ({@code output > root > input}
+ * on collision, via {@link Map#putIfAbsent}). Only one level is flattened — nested fields
+ * stay nested ({@code mylist.name}, not {@code name}).
+ *
Every merged attribute is also promoted to the activation top level, so conditions can
+ * use bare identifiers ({@code score > 0.8}) without any AST rewriting. Framework keys
+ * are never shadowed.
+ *
{@code id} is the user-supplied {@code id} attribute when present, otherwise falls back
+ * to the event UUID.
+ *
+ *
+ *
JSON-shaped strings auto-parse first; narrow numerics widen to long/double.
+ */
+ @SuppressWarnings("unchecked")
+ public Map createActivation(Event event) {
+ Map activation = new HashMap<>();
+ activation.put("type", event.getType());
+ activation.put("EventType", CelExpressionFacade.EVENT_TYPE_CONSTANTS);
+
+ Object normalizedAttrs = normalizeValue(event.getAttributes(), 0);
+ Map merged = new HashMap<>();
+ if (normalizedAttrs instanceof Map) {
+ Map attrs = (Map) normalizedAttrs;
+
+ // Precedence: output subkeys > root attributes > input subkeys (putIfAbsent keeps the
+ // earliest insertion). Root iteration includes the "input"/"output" maps themselves,
+ // so nested paths like input.region.width keep working.
+ Object outputObj = attrs.get("output");
+ if (outputObj instanceof Map) {
+ ((Map) outputObj).forEach(merged::putIfAbsent);
+ }
+ attrs.forEach(merged::putIfAbsent);
+ Object inputObj = attrs.get("input");
+ if (inputObj instanceof Map) {
+ ((Map) inputObj).forEach(merged::putIfAbsent);
+ }
+ }
+
+ activation.put("attributes", merged);
+ // Promote to top level for bare-identifier access; framework keys win on collision.
+ merged.forEach(activation::putIfAbsent);
+ // Event UUID only as fallback — a user-supplied id attribute takes precedence.
+ activation.putIfAbsent("id", event.getId().toString());
+
+ return activation;
+ }
+
+ /**
+ * Maximum recursion depth for {@link #normalizeValue}. Past this depth, strings are kept as
+ * plain strings rather than parsed as JSON (graceful degrade, mirror of Python {@code
+ * _MAX_NORMALIZE_DEPTH}). Prevents stack blow-up on adversarial nested JSON input.
+ */
+ static final int MAX_NORMALIZE_DEPTH = 16;
+
+ /** JSON-looking strings → Map/List; narrow numerics widened to long/double for CEL. */
+ @SuppressWarnings("unchecked")
+ private static Object normalizeValue(Object value, int depth) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof String) {
+ // Past MAX_NORMALIZE_DEPTH we stop expanding and keep the raw string,
+ // matching Python's _MAX_NORMALIZE_DEPTH graceful-degrade policy.
+ if (depth >= MAX_NORMALIZE_DEPTH) {
+ return value;
+ }
+ String s = ((String) value).trim();
+ if (s.length() >= 2
+ && ((s.charAt(0) == '{' && s.charAt(s.length() - 1) == '}')
+ || (s.charAt(0) == '[' && s.charAt(s.length() - 1) == ']'))) {
+ try {
+ return normalizeValue(MAPPER.readValue(s, Object.class), depth + 1);
+ } catch (Exception ignored) {
+ // Not valid JSON — fall through as plain string.
+ }
+ }
+ return value;
+ }
+ if (value instanceof Map) {
+ Map src = (Map) value;
+ Map dst = new HashMap<>(src.size());
+ for (Map.Entry entry : src.entrySet()) {
+ dst.put(entry.getKey(), normalizeValue(entry.getValue(), depth + 1));
+ }
+ return dst;
+ }
+ if (value instanceof List) {
+ List