purushah commented on code in PR #1042:
URL: https://github.com/apache/flink-agents/pull/1042#discussion_r3917115984


##########
plan/src/main/java/org/apache/flink/agents/plan/actions/ModelRoutingResolver.java:
##########
@@ -87,20 +111,73 @@ public Class<RoutingDecision> getResultClass() {
                     @Override
                     public RoutingDecision call() throws Exception {
                         // Timed inside the durable call so the latency is 
persisted with the
-                        // decision: a replayed run reports the original 
strategy wall time.
+                        // decision: a replayed run reports the original 
strategy wall time — and
+                        // the strategy is never re-executed on replay.
                         long start = System.nanoTime();
-                        RoutingDecision decision = 
router.route(routingContext);
+                        RoutingDecision decision = executePure(router, 
strategy, routingContext);
                         return decision.withDecisionMs((System.nanoTime() - 
start) / 1_000_000.0);
                     }
                 };
 
         RoutingDecision decision = ctx.durableExecute(routeCallable);
-        Double decisionMs = decision.getDecisionMs();
-        FlinkAgentsMetricGroup actionMetrics = ctx.getActionMetricGroup();
-        if (actionMetrics != null && decisionMs != null) {
-            
actionMetrics.getHistogram("routingDecisionLatencyMs").update(Math.round(decisionMs));
+        recordDecisionLatency(ctx, decision);
+        return normalizeAndFinish(
+                requestId, model, router, decision, 
ModelRoutingEvent.SOURCE_STRATEGY, ctx);
+    }
+
+    /** Executes the pure (engine-free) strategy types: built-in rules, or the 
user's executor. */
+    private static RoutingDecision executePure(
+            ModelRouter router, RoutingStrategy strategy, RoutingContext 
routingContext)
+            throws Exception {
+        switch (strategy.getType()) {
+            case RULE_BASED:
+                return executeRules(router, routingContext);
+            case CUSTOM:
+                return router.getCustomExecutor().route(strategy, 
routingContext);
+            default:
+                throw new IllegalStateException(
+                        "Unhandled routing strategy type: " + 
strategy.getType());
         }
+    }
 
+    /**
+     * Built-in keyword/regex rules: the first candidate whose pattern 
(pre-compiled by the router)
+     * matches the most recent user message wins, in declaration order; no 
match abstains so the
+     * router uses its default model.
+     */
+    private static RoutingDecision executeRules(ModelRouter router, 
RoutingContext context) {
+        String text = context.lastUserMessage();

Review Comment:
   Great catch — the restructure dropped exactly the test that pinned 
last-vs-first. Will add a multi-turn case that runs the rule evaluator 
end-to-end and asserts the match is against the latest user message.



##########
plan/src/main/java/org/apache/flink/agents/plan/AgentPlan.java:
##########
@@ -691,6 +698,156 @@ private void checkNoRouterModelNameClash(ResourceProvider 
provider) {
         }
     }
 
+    /**
+     * Static routing-strategy constraints fail at plan construction — never 
per record. The
+     * strategy travels as a language-neutral type tag plus arguments, so 
validation reads
+     * declaration data directly: no reflective instantiation, whose failure 
modes previously let a
+     * misconfigured strategy skip validation entirely.
+     *
+     * <p>{@code LLM_JUDGE}: the judge chat model must be registered (a typo'd 
name would otherwise
+     * fail-and-abstain on every request, silently disabling routing — cf. 
{@link
+     * #checkNoRouterModelNameClash}), and its descriptor must bind no prompt, 
tools, or skills — a
+     * bound prompt would prepend an (unfilled) task prompt ahead of the 
verdict contract, bound
+     * tools divert the reply into tool calls, and skills inject both a 
discovery prompt and tools,
+     * each silently breaking verdict parsing on every request.
+     *
+     * <p>{@code CUSTOM}: the executor class must exist, implement {@link 
CustomRoutingExecutor},
+     * and expose a supported constructor — checked without instantiation, so 
plan construction
+     * never runs user constructors (or their static initializers).
+     */
+    private void validateLlmJudgeReferences() {
+        if (resourceProviders == null) {
+            return;
+        }
+        Map<String, ResourceProvider> routers = 
resourceProviders.get(ResourceType.MODEL_ROUTER);
+        if (routers == null) {
+            return;
+        }
+        Map<String, ResourceProvider> chatModels =
+                resourceProviders.getOrDefault(ResourceType.CHAT_MODEL, 
Collections.emptyMap());
+        for (ResourceProvider provider : routers.values()) {
+            if (!(provider instanceof JavaResourceProvider)) {
+                continue;
+            }
+            ResourceDescriptor descriptor = ((JavaResourceProvider) 
provider).getDescriptor();
+            if (descriptor == null || descriptor.getInitialArguments() == 
null) {
+                continue;
+            }
+            String typeTag = 
descriptor.getArgument(ModelRouter.STRATEGY_TYPE_KEY);
+            if (typeTag == null) {
+                // Fail here, not per record on the TaskManager: ModelRouter's 
constructor
+                // unconditionally rejects a descriptor without a strategy, 
and a throwing
+                // construction is never cached, so it would re-throw on every 
routed request.
+                throw new IllegalArgumentException(
+                        String.format(
+                                "Model router '%s' declares no routing 
strategy ('%s' missing"
+                                        + " from its descriptor).",
+                                provider.getName(), 
ModelRouter.STRATEGY_TYPE_KEY));
+            }
+            // The declaration constructor re-validates the per-type argument 
rules, so a
+            // structurally invalid configuration (e.g. a judge without a 
judge model) fails
+            // plan construction with the same message as build().
+            RoutingStrategy strategy =
+                    new RoutingStrategy(
+                            RoutingStrategyType.fromTag(typeTag),
+                            descriptor.getArgument(
+                                    ModelRouter.STRATEGY_ARGS_KEY, 
Collections.emptyMap()),
+                            
descriptor.getArgument(ModelRouter.STRATEGY_EXECUTOR_CLASS_KEY));
+            switch (strategy.getType()) {
+                case LLM_JUDGE:
+                    validateJudge(provider.getName(), strategy, chatModels);
+                    break;
+                case CUSTOM:
+                    validateCustomExecutor(provider.getName(), strategy);
+                    break;
+                default:

Review Comment:
   You're right — a rule key naming a non-candidate is just as static as a 
missing judge model, and the descriptor path skips the builder check. Will add 
a RULE_BASED arm that validates rule keys against the candidates at plan 
construction, with a test for the descriptor-built case.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to