-
Notifications
You must be signed in to change notification settings - Fork 22
feat: Auto wire agent telemetry #1863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
347e4ba
feat(agents): auto-wire Intake telemetry for agent jobs and deployments
mikeknep c3c38e6
test(agents): cover auto-wired Intake telemetry at both tiers
mikeknep a4487df
lint
mikeknep b81e2aa
fix(agents): only wire telemetry for adapters that support Relay
mikeknep c404a6a
docs(agents): explain the re-validation after telemetry wiring
mikeknep 4a06fc1
feat(insights): carry Analyst telemetry to Intake through Relay
mikeknep db07a02
test(insights): give the adapter's logging tests a real RuntimeContext
mikeknep 8c7edbf
fix(agents,insights): scope telemetry side effects to their caller an…
mikeknep 60d7b04
fix(agents,insights): close the gaps found in review of telemetry aut…
mikeknep 8436969
fix(agents): leave a config that declares any export destination alone
mikeknep bb147ed
feat(agents): let a config ask for an Intake destination it does not …
mikeknep 42434d1
fix(agents): decide ATIF wiring from the ATIF block alone
mikeknep f334e46
sort
mikeknep daf9529
lint
mikeknep da33733
Shore up a test
mikeknep File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
plugins/nemo-agents/src/nemo_agents_plugin/telemetry/intake_export.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Auto-wire an agent's Relay ATIF export to the platform's Intake ingest. | ||
|
|
||
| Filling in an export destination by hand is a poor thing to ask of anyone | ||
| writing an agent config: the reachable platform URL differs per deployment | ||
| context, and the same config should work whether it is deployed or run as a | ||
| job. So the backend wires it, and the config carries at most a name. | ||
|
|
||
| The two contexts differ only in how identity reaches Intake. A deployment | ||
| routes through a loopback auth-proxy sidecar that stamps the principal on the | ||
| way out. A job has one creator for its whole life and is handed that principal | ||
| directly, so it names environment variables the exporter reads instead. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Any | ||
|
|
||
| from nemo_agents_plugin.agent_config import TelemetryConfig | ||
| from pydantic import ValidationError | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| INTAKE_ATIF_INGEST_PATH = "/apis/intake/v2/workspaces/{workspace}/ingest/atif" | ||
|
|
||
|
|
||
| def configure_intake_atif_export( | ||
| config: dict[str, Any], | ||
| *, | ||
| workspace: str, | ||
| base_url: str, | ||
| header_env: dict[str, str] | None = None, | ||
| ) -> bool: | ||
| """Point *config*'s ATIF export at *workspace*'s Intake ingest. | ||
|
|
||
| Mutates *config* in place. Returns whether telemetry was wired. | ||
|
|
||
| Takes the config as a mapping rather than an ``AgentConfig`` because the | ||
| deployments path reaches this point with a config already resolved for its | ||
| runtime, whose harnesses no longer round-trip through the model. The | ||
| telemetry section does round-trip, so it is manipulated as a | ||
| :class:`TelemetryConfig` rather than by poking at keys. | ||
|
|
||
| ``telemetry.enabled`` is tri-state: unset means "wire it for me", ``False`` | ||
| is an explicit opt-out, and ``True`` turns it on while still letting the | ||
| backend fill in anything the config left out. An agent that already names | ||
| its own ATIF storage keeps it — an explicit destination beats an inferred | ||
| one. | ||
|
|
||
| Args: | ||
| config: Agent config to wire, modified in place. | ||
| workspace: Workspace whose Intake receives the trajectory. | ||
| base_url: Platform URL reachable from wherever the agent will run. | ||
| header_env: Header name to environment variable name, for contexts | ||
| with no auth proxy to stamp identity. The variables must exist in | ||
| the agent process; the values deliberately never enter the config, | ||
| which is written into the run's artifacts. | ||
| """ | ||
| section = config.get("telemetry") | ||
| try: | ||
| telemetry = TelemetryConfig.model_validate(section if isinstance(section, dict) else {}) | ||
| except ValidationError as exc: | ||
| # Leave a section we do not understand exactly as we found it. The jobs | ||
| # path validates the whole config moments later and will report this | ||
| # properly; deployments do not, and a telemetry key is no reason to | ||
| # fail one. | ||
| logger.warning("Leaving an unrecognized telemetry section unwired: %s", exc) | ||
| return False | ||
|
|
||
| if telemetry.enabled is False: | ||
| return False | ||
| if _declares_atif_storage(telemetry): | ||
| return False | ||
|
|
||
| storage: dict[str, object] = { | ||
| "type": "http", | ||
| "endpoint": f"{base_url.rstrip('/')}{INTAKE_ATIF_INGEST_PATH.format(workspace=workspace)}", | ||
| } | ||
| if header_env: | ||
| storage["header_env"] = dict(header_env) | ||
|
|
||
| atif = dict(telemetry.atif or {}) | ||
|
mikeknep marked this conversation as resolved.
|
||
| atif["enabled"] = True | ||
| atif["storage"] = [storage] | ||
|
|
||
| wired = telemetry.model_copy( | ||
| update={ | ||
| "enabled": True, | ||
| "provider": telemetry.provider or "relay", | ||
| "agent_name": telemetry.agent_name or config.get("name"), | ||
| "atif": atif, | ||
| } | ||
| ) | ||
| config["telemetry"] = wired.model_dump(exclude_none=True) | ||
| return True | ||
|
|
||
|
|
||
| def _declares_atif_storage(telemetry: TelemetryConfig) -> bool: | ||
| """Whether the config already names somewhere to send trajectories.""" | ||
| return isinstance(telemetry.atif, dict) and bool(telemetry.atif.get("storage")) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.