[api][runtime][python] Propagate metric groups to cross-language resources#860
Conversation
weiqingy
left a comment
There was a problem hiding this comment.
Thanks for taking this on — a few questions inline.
|
|
||
| @Override | ||
| public void setMetricGroup(Object pythonResource, FlinkAgentsMetricGroup metricGroup) { | ||
| interpreter.invoke(SET_METRIC_GROUP, pythonResource, metricGroup); |
There was a problem hiding this comment.
This now forwards the action metric group through to the Python provider, which is exactly what issue #857 asked for — and #857 also called out "define metric ownership clearly to avoid double-counting when action-level and provider-level metrics both observe the same response." From what I can see, ownership already looks singular: token metrics are recorded in exactly one place per language — the action handler (ChatModelAction.recordChatTokenMetrics → chatModel.recordTokenMetrics on the Java side, chat_model_action → _record_token_metrics on the Python side), each reading the resource's bound group. The provider integrations under integrations/ record nothing, so there's no second recorder to collide with — this PR widens that single existing recording across the language boundary rather than introducing a second one. So the #857 concern reads as already closed by construction. Would it be worth a sentence in the PR description making that explicit, so the "no double-counting" reasoning is traceable back to the issue?
There was a problem hiding this comment.
@weiqingy One ownership detail I think is still worth calling out: this is not only about avoiding a second recorder after forwarding the metric group. It is also about which effective metric group the action-level recorder should use after the resource call returns.
For example, in the Java action -> Python chat model path, the Python resource may wrap the forwarded action metric group with a provider-specific dimension such as modelProvider. Metrics recorded inside the Python resource would use that wrapped group, but token metrics are still recorded later by the Java action/model-setup path, which only sees the Java wrapper's metric group. That means action-recorded token metrics may still miss the provider-specific dimension.
So I think the ownership question also needs to define whether a resource-derived metric scope should flow back to the action-level recorder. This PR fixes forwarding into the resource, but not that return path.
There was a problem hiding this comment.
Thanks, that makes sense. My earlier comment was focused on the double-recording angle, but I agree there is a separate ownership question around the effective metric scope.
My takeaway is that #860 currently fixes the forward propagation path into the cross-language resource, but it does not settle whether a resource/provider-derived metric scope should flow back to the action-level token recorder. For example, if the Python resource wraps the forwarded group with provider-specific dimensions, Java-side token recording after the call may still use only the Java wrapper/action scope.
If we keep #860 scoped to forward propagation, it may be good to call this out in the PR description or track it as a follow-up, so it is clear that the return-path/effective-scope behavior is still unresolved. Does that match what you had in mind?
There was a problem hiding this comment.
Thanks, that matches my understanding.
I agree that #860 can stay focused on the forward propagation path, and the return-path / effective-scope behavior can be tracked separately.
@goutamadwant Since this PR already calls out that limitation, would you mind filing a follow-up issue for it? That would make the remaining ownership question easier to continue without blocking this PR.
| public void setMetricGroup(FlinkAgentsMetricGroup metricGroup) { | ||
| this.metricGroup = metricGroup; | ||
| if (this instanceof PythonResourceWrapper) { | ||
| ((PythonResourceWrapper) this).setPythonResourceMetricGroup(metricGroup); |
There was a problem hiding this comment.
The abstract base Resource (package ...api.resource) now imports and instanceof-checks PythonResourceWrapper from the more specialized ...api.resource.python sub-package. That inverts the usual dependency direction — every Java-only resource (chat models, tools, prompts, vector stores) now carries a compile-time edge to the Python-bridge interface in its base type, and a hypothetical third forwarding flavor would mean editing this base method again rather than overriding it.
One option that keeps the base oblivious to the bridge: have each Python* wrapper override setMetricGroup to call super.setMetricGroup(...) and then setPythonResourceMetricGroup(...), pushing the Python concern down into the wrappers where getPythonResourceAdapter() already lives. The PR already adds a new getPythonResourceAdapter() override to all eight wrappers, so a per-wrapper setMetricGroup override would touch the same set of files — just in the Python layer instead of the base class. Was the centralized instanceof chosen deliberately, or mainly to avoid touching the wrappers?
| ), | ||
| ], | ||
| ) | ||
| def test_java_resource_wrappers_forward_metric_group(resource): |
There was a problem hiding this comment.
This new file covers the Python→Java direction well, but only that direction — set_java_resource_metric_group plus the four Java*Impl wrappers. The Java→Python direction's Python half, python_java_utils.set_metric_group (python_java_utils.py:382), has no direct test: it's the function that wraps the raw Java group in FlinkMetricGroup and owns the only None branch in the feature, and the Java half is mock-verify only (testSetMetricGroup asserts the invoke string, it doesn't execute Python). So that seam isn't exercised end-to-end today.
Would a small unit test in this file's style be worth adding — a fake resource that captures its set_metric_group arg, then set_metric_group(fake, sentinel_j_group) asserting the captured arg is a FlinkMetricGroup whose _j_metric_group is sentinel_j_group, plus a None case asserting None is forwarded? That would pin the wrap-and-None behavior that's currently uncovered.
| """Bind the underlying Java metric group to a wrapped Java resource.""" | ||
| if j_resource is None: | ||
| return | ||
| j_metric_group = getattr(metric_group, "_j_metric_group", metric_group) |
There was a problem hiding this comment.
In the real flow this is correct — metric_group is always a FlinkMetricGroup (it comes from action_metric_group), so the unwrap hits _j_metric_group and forwards the genuine Java group. Low confidence that the edge below matters in practice, so genuinely a question rather than a flag.
RunnerContext.get_resource(name, type, metric_group=...) is a documented public API accepting any MetricGroup — an abstract base a user could subclass without a _j_metric_group. If such a custom group reaches a Java resource wrapper, the silent , metric_group fallback would forward a raw Python object into Java setMetricGroup(FlinkAgentsMetricGroup), failing opaquely at the pemja boundary rather than with a clear error. Worth making the fallback explicit (unwrap only a real FlinkMetricGroup, else fail loudly), or is that out of scope today given no in-tree caller hits it? (The None case is benign — get_resource substitutes action_metric_group first, and a direct None just clears the field — so no concern there.)
5f17848 to
591728c
Compare
|
@weiqingy @joeyutong Pushed an update addressing the review comments. The base Resource class no longer depends on the Python bridge. Metric forwarding now lives in the Python-backed wrappers, which call super.setMetricGroup(...) and then forward to the wrapped Python resource. I also added direct coverage for python_java_utils.set_metric_group(...), including the raw Java group wrapping path and the None path. On the Python-to-Java side, set_java_resource_metric_group(...) now accepts only FlinkMetricGroup or None and raises a clear TypeError for unsupported custom metric groups. I also updated the PR description to make the scope explicit: this PR handles forward metric-group propagation and keeps token recording single-recorder/no double-counting. The return-path/effective-scope behavior is still separate follow-up work. Let me know if you have any suggestions. thanks! |
# 影响范围 - Java/Python `BaseEmbeddingModelConnection` 与 `BaseEmbeddingModelSetup` 保留 `embedWithUsage`/`embed_with_usage` 的结果携带链路;普通 `embed` API 的返回类型不变。 - `BaseVectorStore.query`、自动补 embedding 以及 Python 对应实现恢复仅调用 `embed`,不再进入指标组或计数器。 - OpenAI、Tongyi、Bedrock 的 provider usage 提取及批量聚合能力保持不变;测试改为断言 usage 结果本身。 # 改动影响面 - 主链路:embedding provider -> `EmbeddingResult` -> 调用方显式消费 usage;本提交不再把 usage 写入 `MetricGroup`。 - 向量库扩展点:恢复 `BaseEmbeddingModelSetup.embed` 覆盖语义,并且仅在确有缺失 embedding 时延迟解析模型,避免 Mem0 无模型路径报错。 - 不受影响范围:apache#860/apache#861 的 resource metric-group 传播和 action-scoped metric 语义未改动;未引入新的 metric abstraction。 # 功能改进/开发/新增 - 类型: refactor - 触发条件: 普通资源 API 可并发调用,无法安全保证 `MetricGroup`/`SimpleCounter` 的线程模型;Python vector-store 还绕过了 `embed` 扩展点。 - 根因类别: 指标写入边界不明确、公共 API 扩展点回归、惰性资源解析边界遗漏。 - 行为变化: 有;PR 仅暴露 provider usage 返回 API,不再自动记录 embedding token metrics。 # 验证 - `mvn --batch-mode --no-transfer-progress -pl api -Dtest=BaseEmbeddingModelSetupEmbeddingResultTest test` PASS - `mvn --batch-mode --no-transfer-progress -pl integrations/embedding-models/bedrock -am -Dtest=BedrockEmbeddingModelTest -Dsurefire.failIfNoSpecifiedTests=false test` PASS - `uv run --python 3.12 --extra test pytest ...vector_stores... -q` PASS (49 passed, 4 skipped) - `./tools/lint.sh --check` PASS - `uv run --python 3.12 --extra lint ruff format --check ... && uv run --python 3.12 --extra lint ruff check ...` PASS - `git diff --check` PASS
Linked issue: #857
Purpose of change
This change propagates the action metric group across Java/Python resource boundaries so cross-language resources record metrics under the caller's action scope.
The forwarding is handled by the Python-backed wrapper classes rather than by adding Python-specific logic to the base
Resourceclass. On the Python-to-Java path, Java resource wrappers now unwrap onlyFlinkMetricGroupinstances orNone, and fail clearly for unsupported custom metric groups instead of passing an opaque Python object through pemja.Metric ownership remains single-recorder: this PR forwards the bound metric group but does not add provider-side token recording. It also keeps the return-path/effective-scope question out of scope. If a provider/resource wraps the metric group with provider-specific dimensions before action-level token recording, that behavior should be tracked separately.
Tests
uv run --no-sync pytest flink_agents/runtime/tests/test_cross_language_metric_group.pymvn --batch-mode --no-transfer-progress -pl api,plan,runtime -am -DskipTests -Dspotless.skip=true test-compilemvn --batch-mode --no-transfer-progress -pl api -Dtest=PythonChatModelSetupTest -Dspotless.skip=true testmvn --batch-mode --no-transfer-progress -pl runtime -am -Dtest=PythonResourceAdapterImplTest -Dsurefire.failIfNoSpecifiedTests=false -Dspotless.skip=true test./tools/lint.sh -cAPI
Touches the Java/Python bridge interfaces by adding default metric-forwarding hooks on
PythonResourceAdapterandPythonResourceWrapper. There are no user-facing configuration changes.Documentation
doc-neededdoc-not-neededdoc-included