Skip to content

[api][plan][python] Support rule-based and custom model routing - #1362

Merged
wenjin272 merged 6 commits into
apache:mainfrom
sangkyoonnam:feature/python-routing-1333-rules-publish
Nov 10, 2026
Merged

wenjin272 merged 6 commits into
apache:mainfrom
sangkyoonnam:feature/python-routing-1333-rules-publish

Conversation

@sangkyoonnam

Copy link
Copy Markdown
Contributor

Linked issue: #1333

Purpose of change

Python agents can register model routers and select candidates with rules or a named custom executor. This builds on the merged chat-attempt refactor.

Runtime flow

The chat action resolves a router once, persists its decision under route:<router>, then invokes the selected candidate under chat:<router>:<candidate>. The action emits routing observations and saves metadata until the final response.

Key decisions

Descriptors use Java's keys and strategy tags. Python validates custom executor classes; Java skips Class.forName only for Python-provided routers. Both validators still check shared descriptor fields and rule syntax. Resource-existence lookup does not construct resources. Python scoped model/router names use the nearest declaration across both types; tool follow-ups execute an explicit concrete selection, as in Java.

Behavioral Semantics

Interaction decisions

Condition Result
Abstention or non-candidate selection Declared default, otherwise first candidate
Selected model exhausts retries; fallback enabled Remaining candidates in declaration order
Python child model/router shadows the same name in a parent Local declaration wins across both types; unshadowed routers inherit
Tool loop begins Concrete model reused; no later routing or fallback
Strategy exception Request failure, preserved for replay
Durable storage failure/cancellation Propagates; no fallback

Behavioral contracts and failure behavior

Rules inspect the latest user text. Custom executors select models without receiving the runner context. If the initial candidate sequence is exhausted, its causes appear in one failure response. A later tool-round failure reports that round's error. Routing events do not drive dispatch. Successful final responses for routed requests include selection/fallback metadata; failed responses and responses to direct concrete-model requests do not. Decision latency has its own histogram. LLM-judge routing, decision-model routing and Java router execution in Python agents fail during plan compilation. YAML declarations remain separate.

Tests

Contract Tests
Registration, collisions and descriptor validation Registration and routing-validation suites
Selection, ordered fallback and failure causes test_model_routing.py
Scoped lookup, inherited routers and pinned tool follow-ups test_scoped_model_requests_and_tool_followups, cache inheritance tests
Success metadata and later tool-round failure boundary Tool-round success/cleanup and test_fallback_tool_failure_reports_current_round_without_routing_metadata
Durable route/model replay Runtime journal and action replay tests
Python descriptors/events accepted by Java Committed snapshots and Java readers
Custom-provider bridge and regex limits Java routing validation tests

Focused Python action/API/plan/runtime checks passed 521 tests without skips or deselections. Java snapshot/validation checks executed 76 cases, with 18 snapshot-generation skips and no failures. Ruff 0.11.13 and JDK 11 Spotless passed; Java tests used JDK 21 with current reactor sources. Runtime replay tests use the real Python durable runner with a fake Java journal. Full suites, live LLMs, restarted Flink clusters and the complete version matrix were not tested. Regex matching has documented Python/JDK differences.

API

Adds router/strategy declarations, routing context/decision types, ModelRoutingEvent and RunnerContext.has_resource. Custom runner-context implementations must implement the new lookup. Python sub-agent declarations shadow inherited model/router names across both types. Other concrete model requests retain their behavior.

Documentation

  • doc-included

Was this patch authored or co-authored using generative AI tooling?

  • Yes
  • No

Generated-by: Codex CLI 0.166.0 (gpt-6-astra)
Generated-by: Codex CLI 0.166.0 (gpt-6.1-sol)

Generated-by: Codex CLI 0.166.0 (gpt-6-astra)
Generated-by: Codex CLI 0.166.0 (gpt-6.1-sol)
@github-actions github-actions Bot added doc-label-missing The Bot applies this label either because none or multiple labels were provided. fixVersion/1.0.0 The feature or bug should be implemented/fixed in the 1.0.0 version. priority/major Default priority of the PR or issue. labels Nov 6, 2026
@wenjin272 wenjin272 added doc-included Your PR already contains the necessary documentation updates. and removed doc-label-missing The Bot applies this label either because none or multiple labels were provided. labels Nov 9, 2026

@wenjin272 wenjin272 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding Python model routing support and the detailed test coverage!

Could we add an end-to-end test that runs a Flink job with a Python model router? The existing component tests are useful, but we should also verify the full path from router declaration and plan compilation through resource initialization, routing, candidate invocation, and final output.

Mock chat models would be sufficient; no external service or API key is needed. Ideally, the test would cover rule-based selection, default selection when no rule matches, candidate fallback, and a custom executor. The Java DecisionModelRoutingE2ETest could serve as a reference.

I also left an inline suggestion about integrating the Python documentation into the existing sections and keeping it focused on user-facing behavior.

All of them register their resources on the execution environment. The first three use the {{< gh_link file="examples/src/main/java/org/apache/flink/agents/examples/agents/ModelRoutingAgent.java" name="`ModelRoutingAgent`" >}} to send requests and handle responses.


## Python routers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we integrate the Python documentation into the existing sections instead of adding a separate “Python routers” section? Registration, invocation, and custom executor examples could use Java/Python tabs, while shared behavior such as fallback, tool-call handling, and recovery could be described once. Python-specific limitations and regex differences could stay in the relevant sections.

It would also help to keep the documentation focused on user-facing behavior. For example, the internal persistence keys (route:<router> and chat:<router>:<candidate>) seem unnecessary; explaining that completed routing decisions and model calls are reused during recovery should be sufficient. For regex compatibility, we can state the constraints without describing the internal Java validation path.

Please also update the existing Java-only wording where it no longer applies.

Generated-by: Claude Code 2.1.319 (Claude Opus 5.5)
Generated-by: Claude Code 2.1.319 (Claude Opus 5.5)
…ing-1333-rules-publish

Generated-by: Claude Code 2.1.319 (Claude Opus 5.5)
Generated-by: Claude Code 2.1.319 (Claude Opus 5.5)
@sangkyoonnam

Copy link
Copy Markdown
Contributor Author

Thanks @wenjin272. Both points are addressed in follow-up commits.

11181ea adds model_routing_test.py, which runs a Flink job with a Python agent that declares two routers over mock chat models. The five inputs cover a rule selection, the default when no rule matches, fallback from a candidate that always fails, and a custom executor that selects on one input and abstains on another. Each answer names the model that produced it, and the output also checks selected_model, final_model, decision_source and fallback_models_tried from model_routing.

c634e93 removes the separate Python section. Registration, rules, custom executor, calling a router and reading the routing attribute now use Python/Java tabs, and shared behavior is described once. The persistence keys and the Java validation path are gone; the Python tab only states that patterns must also be valid Java regular expressions. LLM judge and decision model now say Python doesn't support them yet, and the all-candidates-fail row describes each language's error.

I also merged main to resolve the conflict with #1352. The routed path now passes user_context to the strategy-failure and all-candidates-fail responses, and f271286 adds Python tests for those paths plus fallback followed by a tool round, matching the Java cases #1352 added.

@wenjin272 wenjin272 closed this Nov 9, 2026
@wenjin272 wenjin272 reopened this Nov 9, 2026
Generated-by: Claude Code 2.1.319 (Claude Opus 5.5)

@wenjin272 wenjin272 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing my comments. LGTM

@wenjin272
wenjin272 merged commit 2464023 into apache:main Nov 10, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-included Your PR already contains the necessary documentation updates. fixVersion/1.0.0 The feature or bug should be implemented/fixed in the 1.0.0 version. priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants