Skip to content

[api][runtime][python] Propagate metric groups to cross-language resources#860

Open
goutamadwant wants to merge 1 commit into
apache:mainfrom
goutamadwant:codex/flink-agents-857-cross-language-metrics
Open

[api][runtime][python] Propagate metric groups to cross-language resources#860
goutamadwant wants to merge 1 commit into
apache:mainfrom
goutamadwant:codex/flink-agents-857-cross-language-metrics

Conversation

@goutamadwant

@goutamadwant goutamadwant commented Jun 26, 2026

Copy link
Copy Markdown

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 Resource class. On the Python-to-Java path, Java resource wrappers now unwrap only FlinkMetricGroup instances or None, 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.py
  • mvn --batch-mode --no-transfer-progress -pl api,plan,runtime -am -DskipTests -Dspotless.skip=true test-compile
  • mvn --batch-mode --no-transfer-progress -pl api -Dtest=PythonChatModelSetupTest -Dspotless.skip=true test
  • mvn --batch-mode --no-transfer-progress -pl runtime -am -Dtest=PythonResourceAdapterImplTest -Dsurefire.failIfNoSpecifiedTests=false -Dspotless.skip=true test
  • ./tools/lint.sh -c

API

Touches the Java/Python bridge interfaces by adding default metric-forwarding hooks on PythonResourceAdapter and PythonResourceWrapper. There are no user-facing configuration changes.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

@github-actions github-actions Bot added doc-not-needed Your PR changes do not impact docs fixVersion/0.4.0 priority/major Default priority of the PR or issue. labels Jun 26, 2026

@weiqingy weiqingy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for taking this on — a few questions inline.


@Override
public void setMetricGroup(Object pythonResource, FlinkAgentsMetricGroup metricGroup) {
interpreter.invoke(SET_METRIC_GROUP, pythonResource, metricGroup);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.recordChatTokenMetricschatModel.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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.)

@goutamadwant goutamadwant force-pushed the codex/flink-agents-857-cross-language-metrics branch from 5f17848 to 591728c Compare July 8, 2026 06:34
@goutamadwant goutamadwant changed the title [api][runtime][python] Propagate metric groups to cross-language reso… [api][runtime][python] Propagate metric groups to cross-language resources Jul 8, 2026
@github-actions github-actions Bot added doc-not-needed Your PR changes do not impact docs and removed doc-not-needed Your PR changes do not impact docs labels Jul 8, 2026
@goutamadwant

Copy link
Copy Markdown
Author

@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!

zxs1633079383 added a commit to zxs1633079383/flink-agents that referenced this pull request Jul 10, 2026
# 影响范围
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-not-needed Your PR changes do not impact docs fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants