Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 17 additions & 17 deletions .github/workflows/insights-testbed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ on:
description: "Override subject num_trials (empty = testbeds.toml value)"
type: string
default: ""
publish_state:
description: "produce only: upload the new state version to the testbed-state release"
type: boolean
default: true
reason:
description: "produce only: why this fixture exists (one line for the release catalog)"
type: string
Expand All @@ -43,6 +39,7 @@ on:
env:
TAU2_JUDGE_LLM: ${{ vars.TAU2_JUDGE_LLM || 'openai/nvidia/nvidia/evals-nemotron-ultra' }}
TAU2_BENCH_REF: 8ebb7499622fc2be9b9d510d6f7a7653461f4f29
TESTBED_STATE_REPO: ${{ vars.TESTBED_STATE_REPO || 'NVIDIA-dev/NeMo-Optimizer' }}

jobs:
plugin-tests:
Expand Down Expand Up @@ -108,9 +105,11 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 180
concurrency: { group: testbed-state-produce, cancel-in-progress: false }
permissions: { contents: write } # release asset upload
permissions: { contents: read }
# Keep these credentials exclusively in this required-review environment,
# never as repository- or organization-level Actions secrets.
environment: insights-testbed
env:
GH_TOKEN: ${{ github.token }}
INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_KEY }}
# Same gateway key; litellm/tau2 read it under the OpenAI-conventional name.
OPENAI_API_KEY: ${{ secrets.NVIDIA_INFERENCE_KEY }}
Expand Down Expand Up @@ -150,25 +149,19 @@ jobs:
- name: Round-trip fidelity guard
working-directory: nemo-platform/plugins/nemo-insights
# Re-ingest the candidate into scratch workspaces on the in-job stack, re-export,
# doc-diff; any mismatch fails the job before the candidate can be published.
# doc-diff; any mismatch fails the job and marks the uploaded candidate unverified.
# Maintainers publish manually only after confirming this step passed.
# platform-root auto-resolves to the containing nemo-platform checkout.
run: uv run --project ../.. python -m testbed roundtrip "$RUNNER_TEMP/bundles/candidate.tar.zst" --base http://localhost:8080
- name: Upload state artifact
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
# run_attempt suffix: upload-artifact@v4 409s on duplicate names within a
# run, which would fail this always() step on a re-run and block publish.
# run, which would otherwise hide the diagnostic candidate on a re-run.
name: state-candidate-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/bundles/candidate.tar.zst
if-no-files-found: warn
- name: Publish to testbed-state release
if: success() && inputs.publish_state
working-directory: nemo-platform/plugins/nemo-insights
env:
REASON: ${{ inputs.reason }}
# --no-verify: the round-trip fidelity guard already ran as its own step above.
run: uv run --project ../.. python -m testbed publish "$RUNNER_TEMP/bundles/candidate.tar.zst" --reason "$REASON" --no-verify
- name: Platform log on failure
if: failure()
run: tail -200 "$RUNNER_TEMP/platform.log" || true
Expand All @@ -184,6 +177,9 @@ jobs:
(github.event.action == 'labeled' &&
github.event.label.name == 'run-insights' &&
github.event.pull_request.head.repo.full_name == github.repository))
# Keep these credentials exclusively in this required-review environment:
# analyze executes same-repository PR code with inference and fixture-read secrets.
environment: insights-testbed
runs-on: ubuntu-latest
timeout-minutes: 60
permissions: { contents: read }
Expand All @@ -192,11 +188,14 @@ jobs:
matrix:
subject: ${{ fromJSON(needs.plan.outputs.subjects) }}
env:
GH_TOKEN: ${{ github.token }}
INFERENCE_API_KEY: ${{ secrets.NVIDIA_INFERENCE_KEY }}
steps:
- name: Require secrets
run: '[ -n "$INFERENCE_API_KEY" ] || { echo "secret NVIDIA_INFERENCE_KEY is not set"; exit 1; }'
env:
GH_TOKEN: ${{ secrets.TESTBED_STATE_GH_READ_TOKEN }}
run: |
[ -n "$GH_TOKEN" ] || { echo "secret TESTBED_STATE_GH_READ_TOKEN is not set"; exit 1; }
[ -n "$INFERENCE_API_KEY" ] || { echo "secret NVIDIA_INFERENCE_KEY is not set"; exit 1; }
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with: { path: nemo-platform, persist-credentials: false }
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
Expand All @@ -211,6 +210,7 @@ jobs:
- name: Generate insights
working-directory: nemo-platform/plugins/nemo-insights
env:
GH_TOKEN: ${{ secrets.TESTBED_STATE_GH_READ_TOKEN }}
SUBJECT: ${{ matrix.subject }}
STATE: ${{ inputs.state }}
# Empty STATE = bare analyze = the subject's state.lock pin (the
Expand Down
35 changes: 35 additions & 0 deletions plugins/nemo-insights/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,41 @@ The plugin is intentionally not part of `enabled-plugins`.

## CLI

From an agent directory, Insights discovers `optimizer.yaml` in the current
directory or its parents. Start by checking the profile and its environment,
then run analysis:

```bash
cd <agent-directory>
uv run nemo insights doctor
uv run nemo insights analyze
```

The profile contract consumed by Insights is deliberately small:

```yaml
agent: research-agent
agent_spec: AGENT-SPEC.md # optional
workspace: default # optional; defaults to "default"
```

Only `agent`, `agent_spec`, and `workspace` are consumed by Insights.
Unknown experiment-owned fields are ignored, while the reserved `profile_dir`
field is rejected. `agent` is required. Relative `agent_spec` paths are
resolved relative to the profile. When it is omitted, Insights looks for
`AGENT-SPEC.md`, then `README.md`, beside the profile.

An adjacent `.env` is loaded when a profile is found, without replacing
variables already set in the shell. For this shared profile workflow,
`NMP_BASE_URL` is the only base-URL environment variable. Resolution order is
explicit command-line flags, then profile values (for `agent`, `agent_spec`,
and `workspace`) or `NMP_BASE_URL` (for the base URL), then the built-in
defaults. `--base-url` takes precedence over `NMP_BASE_URL`.
Comment thread
callingmedic911 marked this conversation as resolved.

With a discovered profile, analysis reads and writes the shared local output at
`.nemo-optimizer/insights.yaml` beside `optimizer.yaml`. Pass
`--insights-file-output` to use a different file explicitly.

```bash
uv run nemo insights analyze \
--agent research-agent \
Expand Down
4 changes: 0 additions & 4 deletions plugins/nemo-insights/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,3 @@ packages = ["src/nemo_insights_plugin"]
asyncio_mode = "auto"
pythonpath = ["src", "."]
testpaths = ["tests"]

[tool.uv.sources]
nemo-platform = { workspace = true }
nemo-platform-plugin = { workspace = true }
Original file line number Diff line number Diff line change
Expand Up @@ -473,12 +473,14 @@ def __init__(self, *, client: AsyncNeMoPlatform, path: Path) -> None:
def _read_records(self) -> list[dict]:
if not self.path.exists():
return []
raw = yaml.safe_load(self.path.read_text()) or {}
raw = yaml.safe_load(self.path.read_text(encoding="utf-8")) or {}
return list(raw.get("insights", []))

def _write_records(self, records: list[dict]) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(yaml.safe_dump({"insights": records}, sort_keys=False, allow_unicode=True))
self.path.write_text(
yaml.safe_dump({"insights": records}, sort_keys=False, allow_unicode=True), encoding="utf-8"
)
Comment thread
callingmedic911 marked this conversation as resolved.

async def persist_result(self, *, workspace: str, agent: str, result: AnalystResult) -> str:
records = self._read_records()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
_VERBOSE_TRUNCATE = 2000


class ClientConstructionError(Exception):
"""The analyst's NeMo Platform client could not be constructed."""


async def run_analyst(
*,
agent: str,
Expand All @@ -56,7 +60,10 @@ async def run_analyst(
since: Optional incremental lower bound enforced on trace/span reads.
evaluation_id: Optional run scope; AND-pinned onto every span read.
"""
client = make_client(base_url)
try:
client = make_client(base_url)
except (RuntimeError, ValueError) as exc:
raise ClientConstructionError(str(exc)) from None
observability = None
insights_output_path = str(insights_output) if insights_output else None
backend = make_analyst_backend(
Expand Down
Loading