[runtime][java][python] Add CEL Action condition filtering - #821
[runtime][java][python] Add CEL Action condition filtering#821rosemarYuan wants to merge 6 commits into
Conversation
4d01e78 to
b4d2ebb
Compare
weiqingy
left a comment
There was a problem hiding this comment.
Thanks for taking this on — the serialization discipline stands out: keeping the compiled Programs off the Java-serialization path (router built on the TaskManager) and rebuilding the transient actionsWithCel in both readObject and the constructors are the easy-to-miss details that quietly break on failover. The macro-whitelist-via-CALL-node approach is a neat, well-tested call too. A few questions inline, plus one CI issue worth a look before merge.
- The dist uber-jar now shades
dev.celand its transitive graph (antlr4-runtime, re2j, protobuf-java). Is the LICENSE/NOTICE update for those tracked somewhere (release cut, or PR4), or expected in this PR? Just so it doesn't fall through the Apache release gate.
b4d2ebb to
0a42523
Compare
bd9859f to
ebc1438
Compare
wenjin272
left a comment
There was a problem hiding this comment.
Thanks for taking this on @rosemarYuan.
I left some comments regarding certain class names and JavaDoc. Additionally, I believe we are missing end-to-end (E2E) tests.
3b6e040 to
cda3875
Compare
|
For The first one is the large-number failure. That part has been fixed: when a number larger than int64 enters condition evaluation, we now convert it to The second one is reducing the amount of data passed into condition evaluation. I agree that we should avoid normalizing every field when the condition only needs a small subset. There are two possible directions here. 1. Shrink the map we build for condition evaluation. At plan/open time, we can analyze each condition and collect the attribute paths it may read. At runtime, we build the input map only from the union of fields needed by the candidate conditions for that event, and normalize only those selected values. If a condition cannot be narrowed safely, we can fall back to the current full-map behavior. So my plan is to implement the first approach now: partial map construction with conservative fallback to full normalization. Later, once the Google CEL Python implementation supports Python 3.10 and exposes the required lazy access capability, we can consider migrating to the second approach. @wenjin272 does this direction sound reasonable? |
wenjin272
left a comment
There was a problem hiding this comment.
After a thorough review, I believe the current PR has issues in API design, architectural organization, and specific function implementation details. I have provided detailed comments on these issues.
|
Thanks for the detailed review, @wenjin272. I’ve reworked the implementation based on your feedback and force-pushed the updated commit stack. The existing trigger_conditions API is retained as agreed, while classification and expression validation are now separated: Plan owns deterministic classification and fail-fast validation, and Runtime compiles expressions from source during initialization. I also simplified the runtime matching path, renamed it to ActionMatcher, and expanded the E2E and cross-language coverage. These failures are expected for the current intermediate state of the stack, but they are not unrelated Python-side failures. This PR removes actions_by_event from the shared AgentPlan wire format and derives action routing from trigger_conditions on the Java side. Python still serializes and requires actions_by_event, and does not yet classify or evaluate condition expressions. Therefore, the cross-language snapshot tests are correctly exposing a temporary Java/Python contract mismatch. This PR should not be considered independently merge-ready until the corresponding Python Plan/runtime changes are integrated, or the existing wire format is retained until both sides can be migrated together. Could you please take another look when you have time? Thanks! |
b9b4154 to
05c8547
Compare
wenjin272
left a comment
There was a problem hiding this comment.
The main blocker for me is that the PR does not define a clear Event -> condition activation contract before implementing it in buildConditionVariables().
The current code treats output, root attributes, and input as three merge sources and introduces output > root > input precedence. This does not match the event model:
InputEvent: attributes = {"input": payload}
OutputEvent: attributes = {"output": payload}
Other Event: attributes = user-defined fields
InputEvent and OutputEvent should not have sibling user attributes, so this precedence should not exist. Unexpected fields should be rejected or ignored consistently rather than
assigned merge semantics.
The raw input/output instanceof Map checks also make direct Java POJO payloads behave differently from their JSON/cross-language representation. Conditions should use the
Jackson-compatible JSON view consistently. For example, InputEvent(new Result("ok", 42)) should support both input.status and the promoted status, just as the equivalent
base Event with a Map payload does. Payload conversion should remain lazy so type-only conditions do not serialize arbitrary POJOs.
Please also keep the explicit attributes namespace faithful to the real event envelope. For an InputEvent, the expected paths should be:
attributes.input.status // original event structure
input.status // promoted root attribute
status // promoted payload field
attributes.status should not appear merely because payload fields were flattened.
The tests should be updated around this contract rather than preserving the current merge implementation:
- remove the
output > root > inputprecedence cases that construct invalid built-in event shapes; - cover Java POJO and JSON-shaped/base-Event parity;
- add a full
EventRouter -> ActionMatcher -> ConditionEvaluator -> actiontest with a POJO input; - cover multiple trigger entries and single action execution when more than one entry matches;
- rename
ConditionTriggerIntegrationAgent/TesttoTriggerConditionIntegrationAgent/Test.
Once this activation contract is explicit, the implementation and tests should become substantially simpler and cross-language behavior will be deterministic.
05c8547 to
0b7ef54
Compare
wenjin272
left a comment
There was a problem hiding this comment.
Thanks for the updates. Only three comments remain.
| <groupId>org.apache.logging.log4j</groupId> | ||
| <artifactId>log4j-slf4j-impl</artifactId> | ||
| <version>${log4j2.version}</version> | ||
| <scope>test</scope> |
There was a problem hiding this comment.
Why was the scope changed to 'test'? This is currently causing cross-language CI failures: NoClassDefFoundError: org/apache/logging/log4j/core/Layout
There was a problem hiding this comment.
Changing the Log4j dependencies to test scope may be reasonable on its own, but it is unrelated to CEL trigger conditions and changes the runtime packaging contract. It has also required compensating Log4j dependencies in the cross-language E2E module.
I think the scope changes and their E2E compensation should be reverted from this PR and handled separately, together with the corresponding dist/NOTICE updates and deployment verification.
eb59dc9 to
c1f0558
Compare
c1f0558 to
fe7c257
Compare
fe7c257 to
76a70cd
Compare
| Map<String, Object> conditionVariables = new HashMap<>(); | ||
| conditionVariables.put("type", event.getType()); | ||
| conditionVariables.put("EventType", EventType.allConstants()); | ||
| if (event.getId() != null) { |
There was a problem hiding this comment.
Why should ConditionEvaluator support an event with a missing ID? Every event reaching condition matching through the normal Java and cross-language runtime paths has a non-null ID. This test constructs a state that is only possible through incomplete JSON or an explicitly null ID, and unnecessarily broadens the evaluator contract. I think the null-handling branch and this test should be removed; any missing-ID policy belongs at the Event deserialization boundary.
|
|
||
| private static List<String> parseTriggerConditions( | ||
| String actionName, JsonNode triggerConditionsNode) { | ||
| if (triggerConditionsNode == null || triggerConditionsNode.isNull()) { |
There was a problem hiding this comment.
Java YAML validation should match the Python model and generated schema. parseTriggerConditions() currently preserves null and whitespace-only entries, while Python/schema require every entry to be a string containing at least one non-whitespace character. This makes the same YAML pass YamlLoader.buildAgents() in Java but fail immediately in Python or schema validation.
We should reject null and whitespace-only entries here.
| <groupId>org.apache.logging.log4j</groupId> | ||
| <artifactId>log4j-slf4j-impl</artifactId> | ||
| <version>${log4j2.version}</version> | ||
| <scope>test</scope> |
There was a problem hiding this comment.
Changing the Log4j dependencies to test scope may be reasonable on its own, but it is unrelated to CEL trigger conditions and changes the runtime packaging contract. It has also required compensating Log4j dependencies in the cross-language E2E module.
I think the scope changes and their E2E compensation should be reverted from this PR and handled separately, together with the corresponding dist/NOTICE updates and deployment verification.
|
|
||
| self.__agent_plan = AgentPlan.from_agent(agent, self.__config) | ||
| candidate_plan = AgentPlan.from_agent(agent, self.__config) | ||
| candidate_plan_json = candidate_plan.model_dump_json(serialize_as_any=True) |
There was a problem hiding this comment.
The same AgentPlan is serialized in apply() for Java validation and then serialized again in to_datastream() for connectToAgent(). Store the validated JSON and pass that exact value to connectToAgent() instead.
This also guarantees that the submitted plan is exactly the snapshot that passed validation, even if the shared configuration is modified after apply().
Suggest cadidate_plan -> agent_plan and candidate_plan_json -> agent_plan_json
| _CONFIG_FILE_NAME = "config.yaml" | ||
| _LEGACY_CONFIG_FILE_NAME = "flink-conf.yaml" | ||
| _AGENT_PLAN_PREFLIGHT_CLASS = "org.apache.flink.agents.plan.AgentPlanPreflight" | ||
| _AGENT_PLAN_PREFLIGHT_METHOD = "findValidationError" |
There was a problem hiding this comment.
AgentPlanPreflight sounds broader than its actual responsibility. This class does not perform general pre-execution checks; it validates Python-produced AgentPlan JSON by deserializing it with the authoritative Java model. A name such as AgentPlanJsonValidator and validateAgentPlan would make that scope clearer.
Linked issue: #754
Purpose of change
This is the second PR in the four-PR stack tracked by #754:
It adds the Java-side CEL runtime for
@Actiontrigger conditions, enabling actions to filter events by field-level CEL expressions (e.g.event.tokenCount > 100).It adds
trigger_conditionsto the Python@actiondecorator,Agent.add_action, and YAML. Python keeps the conditions in their original order as raw strings inAgentPlanJSON.Tests
API
trigger_conditionsnow accepts event types or CEL Boolean expressions in Java, Python, and YAML. Multiple entries use OR semantics.WARN_AND_SKIP(default) orFAIL.trigger_conditions; legacylisten_event_types-only JSON is rejected.Documentation
doc-neededdoc-not-needed(documentation will be covered in PR4)doc-included