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..0c02be2e3
--- /dev/null
+++ b/api/src/main/java/org/apache/flink/agents/api/EventType.java
@@ -0,0 +1,74 @@
+/*
+ * 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.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Compile-time constants for built-in event types.
+ *
+ *
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 {
+
+ public static final String InputEvent = org.apache.flink.agents.api.InputEvent.EVENT_TYPE;
+ public static final String OutputEvent = org.apache.flink.agents.api.OutputEvent.EVENT_TYPE;
+ public static final String ChatRequestEvent =
+ org.apache.flink.agents.api.event.ChatRequestEvent.EVENT_TYPE;
+ public static final String ChatResponseEvent =
+ org.apache.flink.agents.api.event.ChatResponseEvent.EVENT_TYPE;
+ public static final String ToolRequestEvent =
+ org.apache.flink.agents.api.event.ToolRequestEvent.EVENT_TYPE;
+ public static final String ToolResponseEvent =
+ org.apache.flink.agents.api.event.ToolResponseEvent.EVENT_TYPE;
+ public static final String ContextRetrievalRequestEvent =
+ org.apache.flink.agents.api.event.ContextRetrievalRequestEvent.EVENT_TYPE;
+ 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/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/api/src/main/java/org/apache/flink/agents/api/configuration/AgentConfigOptions.java b/api/src/main/java/org/apache/flink/agents/api/configuration/AgentConfigOptions.java
index c39997da1..cb7de8069 100644
--- a/api/src/main/java/org/apache/flink/agents/api/configuration/AgentConfigOptions.java
+++ b/api/src/main/java/org/apache/flink/agents/api/configuration/AgentConfigOptions.java
@@ -33,6 +33,17 @@ public class AgentConfigOptions {
public static final ConfigOption EVENT_LOGGER_TYPE =
new ConfigOption<>("eventLoggerType", LoggerType.class, LoggerType.SLF4J);
+ /**
+ * Controls how the CEL condition evaluator handles runtime exceptions and non-Boolean results.
+ * Defaults to {@link CelEvaluationFailurePolicy#WARN_AND_SKIP} for streaming safety; set to
+ * {@link CelEvaluationFailurePolicy#FAIL} on strict-semantics pipelines to trigger failover.
+ */
+ public static final ConfigOption CEL_EVALUATION_FAILURE_POLICY =
+ new ConfigOption<>(
+ "celEvaluationFailurePolicy",
+ CelEvaluationFailurePolicy.class,
+ CelEvaluationFailurePolicy.WARN_AND_SKIP);
+
/** The config parameter specifies the directory for the FileEvent file. */
public static final ConfigOption BASE_LOG_DIR =
new ConfigOption<>("baseLogDir", String.class, null);
diff --git a/api/src/main/java/org/apache/flink/agents/api/configuration/CelEvaluationFailurePolicy.java b/api/src/main/java/org/apache/flink/agents/api/configuration/CelEvaluationFailurePolicy.java
new file mode 100644
index 000000000..8990d8224
--- /dev/null
+++ b/api/src/main/java/org/apache/flink/agents/api/configuration/CelEvaluationFailurePolicy.java
@@ -0,0 +1,26 @@
+/*
+ * 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.configuration;
+
+/** Behaviour when a CEL trigger condition throws or returns a non-Boolean at evaluation time. */
+public enum CelEvaluationFailurePolicy {
+ /** Log WARN and treat the action as not matching. Streaming-safe default. */
+ WARN_AND_SKIP,
+ /** Rethrow as {@code IllegalStateException}; triggers Flink task failover. */
+ FAIL
+}
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..08c29f958
--- /dev/null
+++ b/api/src/test/java/org/apache/flink/agents/api/EventTypeTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.Test;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/** Tests for {@link EventType}. */
+class EventTypeTest {
+
+ @Test
+ 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,
+ constants.get("ContextRetrievalRequestEvent"));
+ assertEquals(
+ ContextRetrievalResponseEvent.EVENT_TYPE,
+ constants.get("ContextRetrievalResponseEvent"));
+ }
+}
diff --git a/dist/src/main/resources/META-INF/NOTICE b/dist/src/main/resources/META-INF/NOTICE
index afc811a14..bee13e2e3 100644
--- a/dist/src/main/resources/META-INF/NOTICE
+++ b/dist/src/main/resources/META-INF/NOTICE
@@ -14,6 +14,8 @@ This project bundles the following dependencies under the Apache Software Licens
- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2
- com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2
- com.fasterxml:classmate:1.7.0
+- dev.cel:cel:0.12.0
+- dev.cel:protobuf:0.12.0
- org.apache.logging.log4j:log4j-api:2.23.1
- org.apache.logging.log4j:log4j-core:2.23.1
- org.apache.logging.log4j:log4j-slf4j-impl:2.23.1
@@ -182,6 +184,8 @@ This project bundles the following dependencies under the BSD 3-Clause license.
See bundled license files for details.
- com.google.protobuf:protobuf-java:3.25.5
+- com.google.re2j:re2j:1.8
+- org.antlr:antlr4-runtime:4.13.2
- org.ow2.asm:asm:9.3
This project bundles the following dependencies under the EPL2 license.
diff --git a/dist/src/main/resources/META-INF/licenses/LICENSE.antlr4-runtime b/dist/src/main/resources/META-INF/licenses/LICENSE.antlr4-runtime
new file mode 100644
index 000000000..5d2769415
--- /dev/null
+++ b/dist/src/main/resources/META-INF/licenses/LICENSE.antlr4-runtime
@@ -0,0 +1,28 @@
+Copyright (c) 2012-2022 The ANTLR Project. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+3. Neither name of copyright holders nor the names of its contributors
+may be used to endorse or promote products derived from this software
+without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/dist/src/main/resources/META-INF/licenses/LICENSE.re2j b/dist/src/main/resources/META-INF/licenses/LICENSE.re2j
new file mode 100644
index 000000000..b620ae68f
--- /dev/null
+++ b/dist/src/main/resources/META-INF/licenses/LICENSE.re2j
@@ -0,0 +1,32 @@
+This is a work derived from Russ Cox's RE2 in Go, whose license
+http://golang.org/LICENSE is as follows:
+
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google Inc. nor the names of its contributors
+ may be used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/docs/content/docs/development/chat_models.md b/docs/content/docs/development/chat_models.md
index f6bfe7b77..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)
@@ -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);
@@ -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)
@@ -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..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
@@ -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);
@@ -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
@@ -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..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
@@ -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();
@@ -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."""
@@ -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..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
@@ -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();
@@ -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)
@@ -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..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."""
@@ -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..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
@@ -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();
@@ -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
@@ -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..133ca9291 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.
@@ -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."""
@@ -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);
@@ -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
@@ -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
@@ -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
@@ -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));
}
```
@@ -359,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)
@@ -374,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)
@@ -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<>() {
@@ -476,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)
@@ -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"))
@@ -595,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(
@@ -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..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."""
@@ -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..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."""
@@ -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..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()
@@ -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();
@@ -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/e2e-test/cel-fixtures/cel_conformance_cases.yaml b/e2e-test/cel-fixtures/cel_conformance_cases.yaml
new file mode 100644
index 000000000..c99be4652
--- /dev/null
+++ b/e2e-test/cel-fixtures/cel_conformance_cases.yaml
@@ -0,0 +1,253 @@
+# CEL trigger condition conformance test corpus.
+# Shared by Java (CelConditionEvaluatorTest) and Python (test_cel_pipeline_conformance).
+# Each case is evaluated end-to-end: parse → validate → check → eval (no AST rewriting).
+#
+# Activation contract: type/EventType are framework-owned; attributes is the single-level
+# merge of output.* > root > input.*; merged keys are also promoted to the top level; id is
+# the user attribute when present, else the event UUID.
+
+- name: null_condition_passes
+ condition: null
+ event: { id: abc, type: test, attributes: {} }
+ expected: true
+
+- name: empty_condition_passes
+ condition: ''
+ event: { id: abc, type: test, attributes: {} }
+ expected: true
+
+- name: filter_by_type_match
+ condition: 'type == "_input_event"'
+ event: { id: abc, type: _input_event, attributes: {} }
+ expected: true
+
+- name: filter_by_type_mismatch
+ condition: 'type == "other"'
+ event: { id: abc, type: test, attributes: {} }
+ expected: false
+
+- name: filter_by_id
+ condition: 'id == "550e8400-e29b-41d4-a716-446655440000"'
+ event: { id: '550e8400-e29b-41d4-a716-446655440000', type: test, attributes: {} }
+ expected: true
+
+- name: in_operator_for_map_key
+ condition: '"score" in attributes && attributes["score"] >= 3'
+ event: { id: abc, type: test, attributes: { score: 5 } }
+ expected: true
+
+- name: in_operator_missing_key
+ condition: '"score" in attributes && attributes["score"] >= 3'
+ event: { id: abc, type: test, attributes: {} }
+ expected: false
+
+- name: has_macro_on_map
+ condition: 'has(attributes.score) && attributes.score >= 3'
+ event: { id: abc, type: test, attributes: { score: 5 } }
+ expected: true
+
+- name: has_macro_missing_key
+ condition: 'has(attributes.missing) && attributes.missing == true'
+ event: { id: abc, type: test, attributes: {} }
+ expected: false
+
+- name: nested_map_access
+ condition: 'has(attributes.meta) && attributes.meta.source == "api"'
+ event: { id: abc, type: test, attributes: { meta: { source: api } } }
+ expected: true
+
+- name: combined_type_and_attribute
+ condition: 'type == "review" && has(attributes.score) && attributes.score >= 3'
+ event: { id: abc, type: review, attributes: { score: 4 } }
+ expected: true
+
+- name: boolean_attribute
+ condition: 'has(attributes.success) && attributes.success == true'
+ event: { id: abc, type: test, attributes: { success: true } }
+ expected: true
+
+- name: string_attribute
+ condition: 'has(attributes.model) && attributes.model == "gpt-4"'
+ event: { id: abc, type: _chat_request_event, attributes: { model: gpt-4 } }
+ expected: true
+
+- name: numeric_comparison_less_than
+ condition: 'has(attributes.retry_count) && attributes.retry_count < 3'
+ event: { id: abc, type: test, attributes: { retry_count: 1 } }
+ expected: true
+
+- name: numeric_comparison_fails
+ condition: 'has(attributes.retry_count) && attributes.retry_count < 3'
+ event: { id: abc, type: test, attributes: { retry_count: 5 } }
+ expected: false
+
+- name: json_string_auto_parse_object
+ condition: 'has(attributes.input) && has(attributes.input.id) && attributes.input.id == "B000YFSR4W"'
+ event:
+ id: abc
+ type: _input_event
+ attributes:
+ input: '{"id": "B000YFSR4W", "review": "comfy fit"}'
+ expected: true
+
+- name: json_string_auto_parse_nested_mismatch
+ condition: 'has(attributes.input) && has(attributes.input.id) && attributes.input.id == "OTHER"'
+ event:
+ id: abc
+ type: _input_event
+ attributes:
+ input: '{"id": "B000YFSR4W", "review": "comfy fit"}'
+ expected: false
+
+- name: json_string_auto_parse_array
+ condition: 'has(attributes.tags) && attributes.tags[0] == "sports"'
+ event:
+ id: abc
+ type: test
+ attributes:
+ tags: '["sports", "shoes"]'
+ expected: true
+
+- name: plain_string_not_parsed
+ condition: 'has(attributes.name) && attributes.name == "hello world"'
+ event: { id: abc, type: test, attributes: { name: hello world } }
+ expected: true
+
+- name: invalid_json_string_kept_as_string
+ condition: 'has(attributes.broken) && attributes.broken == "{not json"'
+ event: { id: abc, type: test, attributes: { broken: '{not json' } }
+ expected: true
+
+- name: empty_attributes_has_returns_false
+ condition: 'has(attributes.key)'
+ event: { id: abc, type: test, attributes: {} }
+ expected: false
+
+- name: deeply_nested_json_string_3_levels
+ condition: 'has(attributes.data) && has(attributes.data.order) && attributes.data.order.id == "O100"'
+ event:
+ id: abc
+ type: test
+ attributes:
+ data: '{"order": {"id": "O100", "amount": 500}}'
+ expected: true
+
+- name: numeric_int_equals_int
+ condition: 'has(attributes.count) && attributes.count == 42'
+ event: { id: abc, type: test, attributes: { count: 42 } }
+ expected: true
+
+- name: or_condition_first_branch_true
+ condition: 'has(attributes.a) && attributes.a == 1 || has(attributes.b) && attributes.b == 2'
+ event: { id: abc, type: test, attributes: { a: 1 } }
+ expected: true
+
+- name: or_condition_second_branch_true
+ condition: '(has(attributes.a) && attributes.a == 1) || (has(attributes.b) && attributes.b == 2)'
+ event: { id: abc, type: test, attributes: { b: 2 } }
+ expected: true
+
+- name: dot_chain_deep_nested_access
+ condition: 'has(region.width) && region.width.score >= 80'
+ event: { id: abc, type: test, attributes: { region: { width: { score: 95 } } } }
+ expected: true
+
+- name: dot_chain_deep_nested_mismatch
+ condition: 'has(region.width) && region.width.score >= 80'
+ event: { id: abc, type: test, attributes: { region: { width: { score: 50 } } } }
+ expected: false
+
+- name: dot_chain_six_levels
+ condition: 'has(input.region) && input.region.width.score.month12 == 88'
+ event:
+ id: abc
+ type: test
+ attributes:
+ input: { region: { width: { score: { month12: 88 } } } }
+ expected: true
+
+- name: or_short_circuit_left_true_skips_right_error
+ condition: '(type == "hit") || (attributes.nope.deep > 3)'
+ event: { id: abc, type: hit, attributes: {} }
+ expected: true
+
+- name: and_short_circuit_has_guards_missing_field_access
+ condition: 'has(attributes.missing) && (attributes.missing.deep > 3)'
+ event: { id: abc, type: test, attributes: {} }
+ expected: false
+
+- name: empty_json_object_string_parsed_has_returns_false
+ condition: 'has(attributes.config.missing_key)'
+ event: { id: abc, type: test, attributes: { config: '{}' } }
+ expected: false
+
+- name: nested_json_string_recursively_parsed
+ condition: 'has(attributes.outer.inner) && attributes.outer.inner.id == 1'
+ event:
+ id: abc
+ type: test
+ attributes:
+ outer: '{"inner": "{\"id\": 1}"}'
+ expected: true
+
+- name: event_type_select_expr_folded_to_literal
+ condition: 'type == EventType.InputEvent'
+ event: { id: abc, type: _input_event, attributes: {} }
+ expected: true
+
+- name: int64_max_in_range
+ condition: 'attributes.x == 9223372036854775807'
+ event: { id: abc, type: test, attributes: { x: 9223372036854775807 } }
+ expected: true
+
+# ----- Flattened-activation contract (Method D) -----
+
+- name: bare_root_attribute_access
+ condition: 'score >= 3'
+ event: { id: abc, type: test, attributes: { score: 5 } }
+ expected: true
+
+- name: has_on_bare_attribute_present
+ condition: 'has(score) && score >= 3'
+ event: { id: abc, type: test, attributes: { score: 5 } }
+ expected: true
+
+- name: has_on_bare_attribute_missing
+ condition: 'has(score) && score >= 3'
+ event: { id: abc, type: test, attributes: {} }
+ expected: false
+
+- name: output_subkey_wins_over_root_attribute
+ condition: 'k == "out"'
+ event: { id: abc, type: test, attributes: { k: root, output: { k: out } } }
+ expected: true
+
+- name: root_attribute_wins_over_input_subkey
+ condition: 'k == "root"'
+ event: { id: abc, type: test, attributes: { k: root, input: { k: in } } }
+ expected: true
+
+- name: user_id_attribute_overrides_event_uuid
+ condition: 'id == "tenant-42"'
+ event: { id: abc, type: test, attributes: { id: tenant-42 } }
+ expected: true
+
+- name: framework_type_wins_over_attribute_type
+ condition: 'type == "real"'
+ event: { id: abc, type: real, attributes: { type: fake } }
+ expected: true
+
+- name: attribute_type_still_reachable_via_attributes
+ condition: 'attributes.type == "fake"'
+ event: { id: abc, type: real, attributes: { type: fake } }
+ expected: true
+
+- name: flatten_is_single_level_only
+ condition: 'mylist.name == "x"'
+ event: { id: abc, type: test, attributes: { mylist: { name: x } } }
+ expected: true
+
+- name: event_type_constant_combined_with_bare_attribute
+ condition: 'type == EventType.InputEvent && score > 0.8'
+ event: { id: abc, type: _input_event, attributes: { score: 0.9 } }
+ expected: true
diff --git a/e2e-test/cel-fixtures/disallowed_macros.yaml b/e2e-test/cel-fixtures/disallowed_macros.yaml
new file mode 100644
index 000000000..1a755945b
--- /dev/null
+++ b/e2e-test/cel-fixtures/disallowed_macros.yaml
@@ -0,0 +1,39 @@
+# CEL macro whitelist test fixture.
+# Shared by Java (CelExpressionFacadeTest) and Python (test_cel_facade).
+# Phase-1 policy: only `has()` is allowed; all others are rejected.
+
+reject:
+ # Free-form calls
+ - "exists(x, x > 0)"
+ - "exists_one(x, x == 1)"
+ - "all(x, x > 0)"
+ - "filter(x, x > 0)"
+ - "map(x, x + 1)"
+ # Method-style calls (most common CEL usage)
+ - "attributes.tags.exists(x, x == 'a')"
+ - "attributes.scores.all(s, s >= 60)"
+ - "attributes.items.filter(i, i.active == true)"
+ - "attributes.items.map(i, i.name)"
+ - "attributes.tags.exists_one(t, t == 'urgent')"
+ # Nested in logical operators
+ - "has(attributes.x) && attributes.list.all(t, t > 0)"
+ - "type == '_input_event' || attributes.items.exists(i, i.price > 100)"
+ # Nested macros inside macro arguments
+ - "attributes.groups.exists(g, g.members.all(m, m.active == true))"
+
+accept:
+ # has() is allowed
+ - "has(attributes.score)"
+ - "has(attributes.user_id) && attributes.score > 10"
+ # Basic operators, no macros
+ - "type == '_input_event'"
+ - "attributes.score >= 90"
+ - "attributes.count != 0 && type == '_output_event'"
+ # Macro name inside string literal (not a call)
+ - "type == 'exists_check_event'"
+ - "attributes.name == 'filter_result'"
+ - "attributes.label == 'has all filter map exists'"
+ # Macro name as identifier substring (not a call)
+ - "attributes.existing == true"
+ - "attributes.mapKey == 'value'"
+ - "attributes.filter_count > 5"
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/TokenMetricsE2EAgent.java b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/TokenMetricsE2EAgent.java
index 9cefc284c..22608a2c0 100644
--- a/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/TokenMetricsE2EAgent.java
+++ b/e2e-test/flink-agents-end-to-end-tests-integration/src/test/java/org/apache/flink/agents/integration/test/TokenMetricsE2EAgent.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;
@@ -60,7 +61,7 @@ public static ResourceDescriptor chatModel() {
.build();
}
- @Action(listenEventTypes = {InputEvent.EVENT_TYPE})
+ @Action(EventType.InputEvent)
public static void process(InputEvent event, RunnerContext ctx) throws Exception {
ctx.sendEvent(
new ChatRequestEvent(
@@ -69,7 +70,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/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/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/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 1bd647764..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,43 +190,71 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE
this.actionsByEvent = agentPlan.getActionsByEvent();
this.resourceProviders = agentPlan.getResourceProviders();
this.config = agentPlan.getConfig();
+ rebuildActionsWithCel();
}
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);
-
- // Add to actions map
- actions.put(action.getName(), action);
-
- // Add to actionsByEvent map
- for (String eventTypeName : eventTypeNames) {
- actionsByEvent.computeIfAbsent(eventTypeName, k -> new ArrayList<>()).add(action);
- }
+ Action action = new Action(actionName, function, triggerConditions, config);
+ 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 {
@@ -251,7 +289,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 +314,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..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,27 +23,39 @@
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;
/**
- * 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 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)
public class Action {
private final String name;
private final Function exec;
- private final List listenEventTypes;
+ 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;
@@ -51,18 +63,40 @@ public class Action {
public Action(
String name,
Function exec,
- List listenEventTypes,
+ 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.listenEventTypes = listenEventTypes;
+ 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});
}
- public Action(String name, Function exec, List listenEventTypes) throws Exception {
- this(name, exec, listenEventTypes, null);
+ 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);
}
public String getName() {
@@ -73,8 +107,43 @@ public Function getExec() {
return exec;
}
+ /** Returns the full trigger conditions list (type names and CEL expressions). */
+ public List getTriggerConditions() {
+ return triggerConditions;
+ }
+
+ /** 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 listenEventTypes;
+ 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
@@ -89,11 +158,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/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..123efb119
--- /dev/null
+++ b/plan/src/main/java/org/apache/flink/agents/plan/condition/ParsedCondition.java
@@ -0,0 +1,154 @@
+/*
+ * 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.CelOptions;
+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();
+
+ /** Mirrors the runtime facade caps so a too-deep / too-long expression fails at classify. */
+ CelOptions CEL_OPTIONS =
+ CelOptions.current()
+ .maxParseRecursionDepth(32)
+ .maxExpressionCodePointSize(8_192)
+ .build();
+
+ /**
+ * Parser with the custom {@code has()} macro and the same resource caps as the runtime facade
+ * parser.
+ */
+ CelParser CEL_PARSER =
+ CelParserFactory.standardCelParserBuilder()
+ .setOptions(CEL_OPTIONS)
+ .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/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/actions/ActionConstructionFailureTest.java b/plan/src/test/java/org/apache/flink/agents/plan/actions/ActionConstructionFailureTest.java
new file mode 100644
index 000000000..bda5a6fb3
--- /dev/null
+++ b/plan/src/test/java/org/apache/flink/agents/plan/actions/ActionConstructionFailureTest.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.actions;
+
+import org.apache.flink.agents.plan.Function;
+import org.apache.flink.agents.plan.JavaFunction;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+/** Pin the "silent → loud" contract: invalid CEL fails at Action construction time. */
+class ActionConstructionFailureTest {
+
+ /** Reusable executor — Action's constructor only validates the signature. */
+ private static Function execFn() throws Exception {
+ return new JavaFunction(
+ "org.apache.flink.agents.plan.actions.ActionConstructionFailureTest",
+ "noopExec",
+ new Class[] {
+ org.apache.flink.agents.api.Event.class,
+ org.apache.flink.agents.api.context.RunnerContext.class,
+ });
+ }
+
+ /** Dummy executor referenced reflectively from {@link #execFn()}. */
+ public static void noopExec(
+ org.apache.flink.agents.api.Event e,
+ org.apache.flink.agents.api.context.RunnerContext c) {}
+
+ // -- Loud failure on syntactically invalid CEL --
+
+ @Test
+ void construction_throwsOnInvalidCel_trailingOperator() throws Exception {
+ Function fn = execFn();
+ assertThatThrownBy(() -> new Action("bad", fn, Collections.singletonList("type ==")))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("type ==");
+ }
+
+ @Test
+ void construction_throwsOnInvalidCel_danglingPlus() throws Exception {
+ Function fn = execFn();
+ assertThatThrownBy(() -> new Action("bad", fn, Collections.singletonList("event.size +")))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("event.size +");
+ }
+
+ @Test
+ void construction_throwsOnInvalidCel_unbalancedParen() throws Exception {
+ Function fn = execFn();
+ assertThatThrownBy(
+ () -> new Action("bad", fn, Collections.singletonList("has(attributes.k")))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void construction_throwsOnInvalidCel_inMixedList() throws Exception {
+ // The bad entry is sandwiched between two well-formed ones; the loud-failure contract
+ // says we don't silently drop it just because the rest of the list is fine.
+ Function fn = execFn();
+ assertThatThrownBy(
+ () ->
+ new Action(
+ "bad",
+ fn,
+ Arrays.asList(
+ "InputEvent", "type ==", "has(attributes.k)")))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("type ==");
+ }
+
+ // -- Pre-existing structural validations --
+
+ @Test
+ void construction_throwsOnEmptyContains() throws Exception {
+ Function fn = execFn();
+ assertThatThrownBy(() -> new Action("bad", fn, Collections.emptyList()))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("must have at least one entry");
+ }
+
+ @Test
+ void construction_throwsOnNullContains() throws Exception {
+ Function fn = execFn();
+ assertThatThrownBy(() -> new Action("bad", fn, null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("must have at least one entry");
+ }
+
+ // -- Negative control: well-formed inputs --
+
+ @Test
+ void construction_succeedsForWellFormedInputs() throws Exception {
+ Function fn = execFn();
+ assertDoesNotThrow(
+ () ->
+ new Action(
+ "ok",
+ fn,
+ Arrays.asList(
+ "InputEvent",
+ "_input_event",
+ "type == 'x' && id > 0",
+ "has(attributes.user_id)")));
+ }
+}
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/actions/ActionParsedConditionsTest.java b/plan/src/test/java/org/apache/flink/agents/plan/actions/ActionParsedConditionsTest.java
new file mode 100644
index 000000000..0f829b018
--- /dev/null
+++ b/plan/src/test/java/org/apache/flink/agents/plan/actions/ActionParsedConditionsTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.actions;
+
+import org.apache.flink.agents.api.EventType;
+import org.apache.flink.agents.plan.Function;
+import org.apache.flink.agents.plan.JavaFunction;
+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.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/** Tests for parsedConditions: classify, round-trip, and derived views. */
+class ActionParsedConditionsTest {
+
+ /** Reusable executor — Action's constructor only validates the signature. */
+ private static Function execFn() throws Exception {
+ return new JavaFunction(
+ "org.apache.flink.agents.plan.actions.ActionParsedConditionsTest",
+ "noopExec",
+ new Class[] {
+ org.apache.flink.agents.api.Event.class,
+ org.apache.flink.agents.api.context.RunnerContext.class,
+ });
+ }
+
+ /** Dummy executor referenced reflectively from {@link #execFn()}. */
+ public static void noopExec(
+ org.apache.flink.agents.api.Event e,
+ org.apache.flink.agents.api.context.RunnerContext c) {}
+
+ private static List celSources(Action action) {
+ return action.getParsedConditions().stream()
+ .filter(pc -> pc instanceof CelExpression)
+ .map(pc -> ((CelExpression) pc).source())
+ .collect(Collectors.toList());
+ }
+
+ @Test
+ void parsedConditions_singleTypeName_resolvesToTypeMatch() throws Exception {
+ Action a = new Action("a", execFn(), Collections.singletonList(EventType.InputEvent));
+
+ List pcs = a.getParsedConditions();
+ assertEquals(1, pcs.size());
+ assertTrue(pcs.get(0) instanceof TypeMatch);
+
+ TypeMatch tm = (TypeMatch) pcs.get(0);
+ assertEquals(EventType.InputEvent, tm.source());
+ assertEquals(EventType.InputEvent, tm.source());
+
+ assertEquals(Collections.singletonList(EventType.InputEvent), a.getListenEventTypes());
+ assertEquals(Collections.emptyList(), celSources(a));
+ }
+
+ @Test
+ void parsedConditions_rawEventTypeString_isPreservedUnchanged() throws Exception {
+ Action a = new Action("a", execFn(), Collections.singletonList("_input_event"));
+
+ ParsedCondition pc = a.getParsedConditions().get(0);
+ assertTrue(pc instanceof TypeMatch);
+ TypeMatch tm = (TypeMatch) pc;
+ assertEquals("_input_event", tm.source());
+ assertEquals("_input_event", tm.source());
+ }
+
+ @Test
+ void parsedConditions_celExpression_resolvesToCelExpression() throws Exception {
+ Action a =
+ new Action(
+ "a",
+ execFn(),
+ Collections.singletonList("type == EventType.InputEvent && id > 0"));
+
+ List pcs = a.getParsedConditions();
+ assertEquals(1, pcs.size());
+ assertTrue(pcs.get(0) instanceof CelExpression);
+ assertEquals("type == EventType.InputEvent && id > 0", pcs.get(0).source());
+
+ assertEquals(Collections.emptyList(), a.getListenEventTypes());
+ assertEquals(
+ Collections.singletonList("type == EventType.InputEvent && id > 0"), celSources(a));
+ }
+
+ @Test
+ void parsedConditions_hasCall_isClassifiedAsCel() throws Exception {
+ Action a = new Action("a", execFn(), Collections.singletonList("has(attributes.user_id)"));
+ assertTrue(a.getParsedConditions().get(0) instanceof CelExpression);
+ }
+
+ @Test
+ void parsedConditions_mixedList_preservesOrderAndKinds() throws Exception {
+ List contains =
+ Arrays.asList(
+ EventType.InputEvent, // TypeMatch
+ "_chat_response_event", // TypeMatch (raw)
+ "type == 'x' && id > 0", // CelExpression
+ EventType.OutputEvent, // TypeMatch
+ "has(attributes.k)"); // CelExpression
+ Action a = new Action("a", execFn(), contains);
+
+ List pcs = a.getParsedConditions();
+ assertEquals(5, pcs.size());
+
+ assertTrue(pcs.get(0) instanceof TypeMatch);
+ assertEquals(EventType.InputEvent, ((TypeMatch) pcs.get(0)).source());
+
+ assertTrue(pcs.get(1) instanceof TypeMatch);
+ assertEquals("_chat_response_event", ((TypeMatch) pcs.get(1)).source());
+
+ assertTrue(pcs.get(2) instanceof CelExpression);
+
+ assertTrue(pcs.get(3) instanceof TypeMatch);
+ assertEquals(EventType.OutputEvent, ((TypeMatch) pcs.get(3)).source());
+
+ assertTrue(pcs.get(4) instanceof CelExpression);
+
+ assertEquals(
+ Arrays.asList(EventType.InputEvent, "_chat_response_event", EventType.OutputEvent),
+ a.getListenEventTypes());
+ assertEquals(Arrays.asList("type == 'x' && id > 0", "has(attributes.k)"), celSources(a));
+ }
+
+ @Test
+ void parsedConditions_rejectsNullOrEmptyEntry() throws Exception {
+ List withEmpty = Arrays.asList(EventType.InputEvent, "", "type == 'x'");
+ assertThrows(IllegalArgumentException.class, () -> new Action("a", execFn(), withEmpty));
+
+ List withNull = Arrays.asList(EventType.InputEvent, null, "type == 'x'");
+ assertThrows(IllegalArgumentException.class, () -> new Action("a", execFn(), withNull));
+ }
+
+ @Test
+ void parsedConditions_listIsUnmodifiable() throws Exception {
+ Action a = new Action("a", execFn(), Collections.singletonList(EventType.InputEvent));
+ List pcs = a.getParsedConditions();
+ assertNotNull(pcs);
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> pcs.add(new CelExpression("type == 'y'")));
+ }
+
+ @Test
+ void parsedConditions_hasCelCondition_isTrueWhenAnyCelEntryPresent() throws Exception {
+ Action a = new Action("a", execFn(), Arrays.asList(EventType.InputEvent, "type == 'x'"));
+ assertTrue(a.hasCelCondition());
+
+ Action b = new Action("b", execFn(), Collections.singletonList(EventType.InputEvent));
+ assertEquals(false, b.hasCelCondition());
+ }
+
+ @Test
+ void parsedConditions_listenEventTypes_returnsTypeMatchNamesOnly() throws Exception {
+ Action a =
+ new Action(
+ "a",
+ execFn(),
+ Arrays.asList(EventType.InputEvent, "type == 'x'", EventType.OutputEvent));
+ assertEquals(
+ Arrays.asList(EventType.InputEvent, EventType.OutputEvent),
+ a.getListenEventTypes());
+ }
+}
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/condition/ParsedConditionTest.java b/plan/src/test/java/org/apache/flink/agents/plan/condition/ParsedConditionTest.java
new file mode 100644
index 000000000..a05f4bc4d
--- /dev/null
+++ b/plan/src/test/java/org/apache/flink/agents/plan/condition/ParsedConditionTest.java
@@ -0,0 +1,266 @@
+/*
+ * 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 org.apache.flink.agents.plan.condition.ParsedCondition.CelExpression;
+import org.apache.flink.agents.plan.condition.ParsedCondition.TypeMatch;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link ParsedCondition}: value semantics of TypeMatch/CelExpression, and {@link
+ * ParsedCondition#classify} (corpus equivalence, regression guards, and routing).
+ */
+class ParsedConditionTest {
+
+ // ==================================================================
+ // Value semantics
+ // ==================================================================
+
+ @Test
+ void typeMatch_storesSource() {
+ TypeMatch tm = new TypeMatch("_input_event");
+ assertEquals("_input_event", tm.source());
+ assertTrue(((ParsedCondition) tm) instanceof TypeMatch);
+ assertFalse(((ParsedCondition) tm) instanceof CelExpression);
+ }
+
+ @Test
+ void typeMatch_equalityAndHash() {
+ TypeMatch a = new TypeMatch("_input_event");
+ TypeMatch b = new TypeMatch("_input_event");
+ TypeMatch c = new TypeMatch("_output_event");
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertNotEquals(a, c);
+ }
+
+ @Test
+ void celExpression_storesSource() {
+ CelExpression ce = new CelExpression("type == EventType.InputEvent && id > 0");
+ assertEquals("type == EventType.InputEvent && id > 0", ce.source());
+ assertFalse(((ParsedCondition) ce) instanceof TypeMatch);
+ assertTrue(((ParsedCondition) ce) instanceof CelExpression);
+ }
+
+ @Test
+ void celExpression_equalityAndHash() {
+ CelExpression a = new CelExpression("type == 'x'");
+ CelExpression b = new CelExpression("type == 'x'");
+ CelExpression c = new CelExpression("type == 'y'");
+ assertEquals(a, b);
+ assertEquals(a.hashCode(), b.hashCode());
+ assertNotEquals(a, c);
+ }
+
+ @Test
+ void celExpression_isNotEqualToTypeMatchWithSameSource() {
+ CelExpression ce = new CelExpression("InputEvent");
+ TypeMatch tm = new TypeMatch("InputEvent");
+ assertNotEquals(ce, tm);
+ assertNotEquals(tm, ce);
+ }
+
+ // ==================================================================
+ // classify(): corpus. Format: input, expected_kind, expected_event_type (null for CEL).
+ // ==================================================================
+
+ static Stream classifierCorpus() {
+ return Stream.of(
+ // ----- bare identifiers are TypeMatch (no short-name translation) -----
+ Arguments.of("InputEvent", "TypeMatch", "InputEvent"),
+ Arguments.of("OutputEvent", "TypeMatch", "OutputEvent"),
+ Arguments.of("ChatRequestEvent", "TypeMatch", "ChatRequestEvent"),
+ Arguments.of("ChatResponseEvent", "TypeMatch", "ChatResponseEvent"),
+ Arguments.of("ToolRequestEvent", "TypeMatch", "ToolRequestEvent"),
+ Arguments.of("ToolResponseEvent", "TypeMatch", "ToolResponseEvent"),
+ Arguments.of(
+ "ContextRetrievalRequestEvent",
+ "TypeMatch",
+ "ContextRetrievalRequestEvent"),
+ Arguments.of(
+ "ContextRetrievalResponseEvent",
+ "TypeMatch",
+ "ContextRetrievalResponseEvent"),
+
+ // ----- raw event-type strings & passthrough -----
+ Arguments.of("_input_event", "TypeMatch", "_input_event"),
+ Arguments.of("_my_custom_event", "TypeMatch", "_my_custom_event"),
+ Arguments.of("CustomEvent", "TypeMatch", "CustomEvent"),
+ Arguments.of("in_progress_event", "TypeMatch", "in_progress_event"),
+
+ // ----- equality / inequality -----
+ Arguments.of("type == 'x'", "CelExpression", null),
+ Arguments.of("type != 'x'", "CelExpression", null),
+
+ // ----- numeric comparisons -----
+ Arguments.of("id > 0", "CelExpression", null),
+ Arguments.of("id < 0", "CelExpression", null),
+ Arguments.of("id >= 0", "CelExpression", null),
+ Arguments.of("id <= 0", "CelExpression", null),
+
+ // ----- logical combinators -----
+ Arguments.of("a == 1 && b == 2", "CelExpression", null),
+ Arguments.of("a == 1 || b == 2", "CelExpression", null),
+
+ // ----- has() macro & in keyword -----
+ Arguments.of("has(attributes.user_id)", "CelExpression", null),
+ Arguments.of("type in ['a', 'b']", "CelExpression", null),
+
+ // ----- attribute access shapes -----
+ Arguments.of("attributes.score >= 3", "CelExpression", null),
+ Arguments.of("attributes.score < 0", "CelExpression", null),
+ Arguments.of("attributes['k'] == 'v'", "CelExpression", null),
+ Arguments.of("type in ['_input_event']", "CelExpression", null),
+
+ // ----- size / null / has compounds -----
+ Arguments.of("size(attributes) > 0", "CelExpression", null),
+ Arguments.of("category == 'premium' && score > 5", "CelExpression", null),
+ Arguments.of("id != null", "CelExpression", null),
+ Arguments.of("has(attributes.k)", "CelExpression", null),
+
+ // ----- EventType-qualified comparisons -----
+ Arguments.of("type == EventType.InputEvent", "CelExpression", null),
+ Arguments.of("type == EventType.InputEvent && id > 0", "CelExpression", null));
+ }
+
+ @ParameterizedTest(name = "[{index}] classify(\"{0}\") -> {1}")
+ @MethodSource("classifierCorpus")
+ void astClassifierMatchesCorpus(String input, String expectedKind, String expectedEventType) {
+ ParsedCondition pc = ParsedCondition.classify(input);
+
+ switch (expectedKind) {
+ case "TypeMatch":
+ assertThat(pc)
+ .as("expected TypeMatch for input %s", input)
+ .isInstanceOf(TypeMatch.class);
+ assertThat(pc.source())
+ .as("source must round-trip unchanged for input %s", input)
+ .isEqualTo(input);
+ assertThat(((TypeMatch) pc).source())
+ .as("eventType disagreement on input %s", input)
+ .isEqualTo(expectedEventType);
+ break;
+ case "CelExpression":
+ assertThat(pc)
+ .as("expected CelExpression for input %s", input)
+ .isInstanceOf(CelExpression.class);
+ assertThat(pc.source())
+ .as("source must round-trip unchanged for input %s", input)
+ .isEqualTo(input);
+ assertThat(expectedEventType)
+ .as(
+ "CelExpression cases must declare null expected_event_type (input %s)",
+ input)
+ .isNull();
+ break;
+ default:
+ throw new IllegalStateException(
+ "Unknown expected_kind '" + expectedKind + "' for input " + input);
+ }
+ }
+
+ // Regression: inputs the old regex classifier silently mis-routed as TypeMatch.
+
+ static Stream formerlySilentRegressions() {
+ return Stream.of(
+ "event.someFlag", // SELECT root
+ "!event.flag", // CALL root, op `!_`
+ "myBoolFunc()", // CALL root, function call
+ "'OrderEvent'", // CONSTANT root, string literal
+ "true", // CONSTANT root, boolean literal
+ "event.size + 1 > 0" // CALL root, comparison
+ );
+ }
+
+ @ParameterizedTest(name = "[{index}] {0} -> CelExpression (was silent TypeMatch)")
+ @MethodSource("formerlySilentRegressions")
+ void fixesSilentRegexMisclassifications(String input) {
+ ParsedCondition pc = ParsedCondition.classify(input);
+ assertThat(pc)
+ .as(
+ "input %s must classify as CelExpression — was silently TypeMatch under the"
+ + " regex classifier",
+ input)
+ .isInstanceOf(CelExpression.class);
+ assertThat(pc.source()).isEqualTo(input);
+ }
+
+ // -- Failure mode: silent → loud --
+
+ @Test
+ void invalidCelThrows_atClassifyTime() {
+ assertThatThrownBy(() -> ParsedCondition.classify("type =="))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid CEL expression");
+ assertThatThrownBy(() -> ParsedCondition.classify("event.size +"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("Invalid CEL expression");
+ }
+
+ @Test
+ void rejectsNullOrEmptyInput() {
+ assertThatThrownBy(() -> ParsedCondition.classify(null))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("non-null");
+ assertThatThrownBy(() -> ParsedCondition.classify(""))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("non-null");
+ }
+
+ // -- Reserved-keyword rejection --
+
+ @Test
+ void rejectsReservedKeywordAsEventTypeName() {
+ // "type" is a framework-owned activation variable; using it as a bare event type must
+ // fail loudly.
+ assertThatThrownBy(() -> ParsedCondition.classify("type"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("reserved keyword")
+ .hasMessageContaining("@action(\"type == ");
+ }
+
+ @Test
+ void idIsNotReservedAndClassifiesAsTypeMatch() {
+ // "id" is user-overridable, not framework-reserved — a bare "id" entry is a legal event
+ // type alias.
+ ParsedCondition pc = ParsedCondition.classify("id");
+ assertThat(pc).isInstanceOf(TypeMatch.class);
+ assertThat(pc.source()).isEqualTo("id");
+ }
+
+ @Test
+ void hasOnBareAttributeNameClassifiesAsCelExpression() {
+ // Custom has() macro accepts bare attribute names at parse time.
+ ParsedCondition pc = ParsedCondition.classify("has(score)");
+ assertThat(pc).isInstanceOf(CelExpression.class);
+ assertThat(pc.source()).isEqualTo("has(score)");
+ }
+}
diff --git a/plan/src/test/java/org/apache/flink/agents/plan/serializer/ActionJsonDeserializerTest.java b/plan/src/test/java/org/apache/flink/agents/plan/serializer/ActionJsonDeserializerTest.java
index 8f32d512b..cecdff190 100644
--- a/plan/src/test/java/org/apache/flink/agents/plan/serializer/ActionJsonDeserializerTest.java
+++ b/plan/src/test/java/org/apache/flink/agents/plan/serializer/ActionJsonDeserializerTest.java
@@ -136,4 +136,18 @@ public void testDeserializePythonConfigPreservesPrimitiveTypes() throws IOExcept
.containsKey("extra");
assertNull(result.get("extra"));
}
+
+ @Test
+ public void testDeserializeLegacyListenEventTypes() throws Exception {
+ String json = Utils.readJsonFromResource("actions/action_legacy_listen_event_types.json");
+
+ ObjectMapper mapper = new ObjectMapper();
+ Action action = mapper.readValue(json, Action.class);
+
+ assertEquals("legacyAction", action.getName());
+ assertInstanceOf(JavaFunction.class, action.getExec());
+ assertEquals(1, action.getListenEventTypes().size());
+ assertEquals(InputEvent.EVENT_TYPE, action.getListenEventTypes().get(0));
+ assertThat(action.getTriggerConditions()).containsExactly(InputEvent.EVENT_TYPE);
+ }
}
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..2f604abe1 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
@@ -37,6 +37,8 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** Test for {@link ActionJsonSerializer}. */
@@ -68,8 +70,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 +105,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 +135,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");
@@ -147,27 +149,19 @@ public void testSerializeMultipleEventTypes() throws Exception {
}
@Test
- public void testSerializeEmptyEventTypes() throws Exception {
- // Create a JavaFunction
- JavaFunction function =
- new JavaFunction(
- "org.apache.flink.agents.plan.TestAction",
- "legal",
- new Class[] {Event.class, RunnerContext.class});
-
- // Create an Action with an empty event types list
- Action action = new Action("emptyEventsAction", function, Collections.emptyList());
-
- // Serialize the action to JSON
- String json = new ObjectMapper().writeValueAsString(action);
-
- // Verify the JSON contains the expected fields
- assertTrue(
- 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");
+ public void testSerializeEmptyEventTypes() {
+ // Empty trigger conditions are now rejected at Action construction time
+ // (the CEL feature requires at least one entry to evaluate against).
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ JavaFunction function =
+ new JavaFunction(
+ "org.apache.flink.agents.plan.TestAction",
+ "legal",
+ new Class[] {Event.class, RunnerContext.class});
+ new Action("emptyEventsAction", function, Collections.emptyList());
+ });
}
@Test
@@ -234,4 +228,27 @@ public void testSerializeDeserializeConfig() throws Exception {
Assertions.assertEquals(arg1, deserializeConfig.get("arg1"));
Assertions.assertEquals(arg2, deserializeConfig.get("arg2"));
}
+
+ @Test
+ public void testDeserializeLegacyListenEventTypesKey() throws Exception {
+ String legacyJson =
+ "{"
+ + "\"name\":\"legacyAction\","
+ + "\"exec\":{"
+ + "\"func_type\":\"PythonFunction\","
+ + "\"module\":\"test_module\","
+ + "\"qualname\":\"test_function\""
+ + "},"
+ + "\"listen_event_types\":[\"_input_event\",\"_output_event\"]"
+ + "}";
+
+ ObjectMapper mapper = new ObjectMapper();
+ Action action = mapper.readValue(legacyJson, Action.class);
+
+ assertEquals("legacyAction", action.getName());
+ assertNotNull(action.getTriggerConditions());
+ assertEquals(
+ List.of(InputEvent.EVENT_TYPE, OutputEvent.EVENT_TYPE),
+ action.getTriggerConditions());
+ }
}
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_legacy_listen_event_types.json b/plan/src/test/resources/actions/action_legacy_listen_event_types.json
new file mode 100644
index 000000000..87a60c05f
--- /dev/null
+++ b/plan/src/test/resources/actions/action_legacy_listen_event_types.json
@@ -0,0 +1,10 @@
+{
+ "name": "legacyAction",
+ "exec": {
+ "func_type": "JavaFunction",
+ "qualname": "org.apache.flink.agents.plan.TestAction",
+ "method_name": "legal",
+ "parameter_types": ["org.apache.flink.agents.api.Event", "org.apache.flink.agents.api.context.RunnerContext"]
+ },
+ "listen_event_types": ["_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/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/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
new file mode 100644
index 000000000..d3ef005e8
--- /dev/null
+++ b/python/flink_agents/api/events/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.
+#################################################################################
+"""Built-in event-type constants, sourced from each ``XxxEvent.EVENT_TYPE``."""
+
+from __future__ import annotations
+
+from flink_agents.api.events.chat_event import (
+ ChatRequestEvent as _ChatRequestEvent,
+)
+from flink_agents.api.events.chat_event import (
+ ChatResponseEvent as _ChatResponseEvent,
+)
+from flink_agents.api.events.context_retrieval_event import (
+ ContextRetrievalRequestEvent as _ContextRetrievalRequestEvent,
+)
+from flink_agents.api.events.context_retrieval_event import (
+ ContextRetrievalResponseEvent as _ContextRetrievalResponseEvent,
+)
+from flink_agents.api.events.event import (
+ InputEvent as _InputEvent,
+)
+from flink_agents.api.events.event import (
+ OutputEvent as _OutputEvent,
+)
+from flink_agents.api.events.tool_event import (
+ ToolRequestEvent as _ToolRequestEvent,
+)
+from flink_agents.api.events.tool_event import (
+ ToolResponseEvent as _ToolResponseEvent,
+)
+
+
+class EventType:
+ """Namespace of built-in event-type constants.
+
+ Usage: ``@action(EventType.InputEvent)``.
+ """
+
+ InputEvent: str = _InputEvent.EVENT_TYPE
+ OutputEvent: str = _OutputEvent.EVENT_TYPE
+ ChatRequestEvent: str = _ChatRequestEvent.EVENT_TYPE
+ ChatResponseEvent: str = _ChatResponseEvent.EVENT_TYPE
+ ToolRequestEvent: str = _ToolRequestEvent.EVENT_TYPE
+ ToolResponseEvent: str = _ToolResponseEvent.EVENT_TYPE
+ ContextRetrievalRequestEvent: str = _ContextRetrievalRequestEvent.EVENT_TYPE
+ ContextRetrievalResponseEvent: str = _ContextRetrievalResponseEvent.EVENT_TYPE
+
+ 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/api/tests/test_event_type.py b/python/flink_agents/api/tests/test_event_type.py
new file mode 100644
index 000000000..a25e23fc8
--- /dev/null
+++ b/python/flink_agents/api/tests/test_event_type.py
@@ -0,0 +1,40 @@
+################################################################################
+# 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.
+#################################################################################
+"""Smoke tests for :mod:`flink_agents.api.events.event_type`."""
+
+from __future__ import annotations
+
+from flink_agents.api.events.chat_event import ChatRequestEvent, ChatResponseEvent
+from flink_agents.api.events.context_retrieval_event import (
+ ContextRetrievalRequestEvent,
+ ContextRetrievalResponseEvent,
+)
+from flink_agents.api.events.event import InputEvent, OutputEvent
+from flink_agents.api.events.event_type import EventType
+from flink_agents.api.events.tool_event import ToolRequestEvent, ToolResponseEvent
+
+
+def test_builtin_constants_match_event_class_constants() -> None:
+ assert EventType.InputEvent == InputEvent.EVENT_TYPE
+ assert EventType.OutputEvent == OutputEvent.EVENT_TYPE
+ assert EventType.ChatRequestEvent == ChatRequestEvent.EVENT_TYPE
+ assert EventType.ChatResponseEvent == ChatResponseEvent.EVENT_TYPE
+ assert EventType.ToolRequestEvent == ToolRequestEvent.EVENT_TYPE
+ assert EventType.ToolResponseEvent == ToolResponseEvent.EVENT_TYPE
+ assert EventType.ContextRetrievalRequestEvent == ContextRetrievalRequestEvent.EVENT_TYPE
+ assert EventType.ContextRetrievalResponseEvent == ContextRetrievalResponseEvent.EVENT_TYPE
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..3c69d9ca2 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:
@@ -71,7 +81,13 @@ def __serialize_config(self, config: Dict[str, Any]) -> Dict[str, Any] | None:
@model_validator(mode="before")
def __custom_deserialize(self) -> "Action":
- config = self["config"]
+ # Legacy fallback: listen_event_types → trigger_conditions
+ if "trigger_conditions" not in self or self.get("trigger_conditions") is None:
+ if self.get("listen_event_types"):
+ self["trigger_conditions"] = list(self["listen_event_types"])
+ self.pop("listen_event_types", None)
+
+ config = self.get("config")
if config is None or _CONFIG_TYPE not in config:
return self
config_type = self["config"].pop(_CONFIG_TYPE)
@@ -97,12 +113,15 @@ def __init__(
self,
name: str,
exec: Function,
- listen_event_types: List[str],
+ trigger_conditions: List[str] | None = None,
config: Dict[str, Any] | None = None,
+ **kwargs: Any,
) -> None:
"""Action will check function signature when init."""
+ if trigger_conditions is None:
+ trigger_conditions = kwargs.pop("listen_event_types", [])
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..3e7f4bb01 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},
@@ -120,3 +120,20 @@ def test_action_deserialize_java_shape_config_unwraps_primitives() -> None:
"rate": 1.5,
"label": "fast",
}
+
+
+def test_action_deserialize_legacy_listen_event_types_key() -> None:
+ legacy_json = json.dumps(
+ {
+ "name": "legacy",
+ "exec": {
+ "func_type": "PythonFunction",
+ "module": "flink_agents.plan.tests.test_action",
+ "qualname": "legal_signature",
+ },
+ "listen_event_types": [InputEvent.EVENT_TYPE],
+ }
+ )
+ action = Action.model_validate_json(legacy_json)
+ assert action.name == "legacy"
+ assert action.trigger_conditions == [InputEvent.EVENT_TYPE]
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."""
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..d4e2f819f
--- /dev/null
+++ b/runtime/src/main/java/org/apache/flink/agents/runtime/condition/ActionRouter.java
@@ -0,0 +1,129 @@
+/*
+ * 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.api.configuration.AgentConfigOptions;
+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(
+ agentPlan
+ .getConfig()
+ .get(AgentConfigOptions.CEL_EVALUATION_FAILURE_POLICY));
+ 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..a0d38a824
--- /dev/null
+++ b/runtime/src/main/java/org/apache/flink/agents/runtime/condition/CelConditionEvaluator.java
@@ -0,0 +1,243 @@
+/*
+ * 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.api.configuration.CelEvaluationFailurePolicy;
+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();
+
+ /** Frozen after {@link #initPrograms}; cleared by {@link #close}. */
+ @Nullable private Map programCache;
+
+ private final CelEvaluationFailurePolicy failurePolicy;
+
+ public CelConditionEvaluator() {
+ this(CelEvaluationFailurePolicy.WARN_AND_SKIP);
+ }
+
+ public CelConditionEvaluator(CelEvaluationFailurePolicy 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 == CelEvaluationFailurePolicy.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 == CelEvaluationFailurePolicy.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