feat: add poly metrics commands (list, export, add, edit, import) - #259
feat: add poly metrics commands (list, export, add, edit, import)#259bill-parker wants to merge 16 commits into
Conversation
Add files and response_format params to PlatformAPIHandler.make_request. When files is set, Content-Type is omitted so requests can set the multipart boundary automatically. YAML responses are parsed via ruamel.yaml.
Add URL constants and 5 methods to PlatformAPIHandler for custom metrics: get_custom_metrics, create_custom_metric, update_custom_metric, export_custom_metrics (YAML response), and import_custom_metrics (multipart YAML upload with dry-run support).
Add 6 static methods to AgentStudioInterface for custom metrics: get, create, update, export, preview_metrics_import (computes set diff for dry-run), and import.
Add MetricsCommand with five subcommands: - list: Rich table display with Name/Type/Active/API/Description - export: YAML to stdout or file - add: create metric via flags or interactive questionary prompts - edit: update description, api, active, expected-values - import: bulk YAML import with --dry-run preview
The custom_metrics create route in jupiter_api hardcodes api=False (routes.py:128), ignoring the client-supplied value. The Agent Studio UI also does not set the api flag on create. As a workaround, when the user passes --api, the CLI issues a follow-up PATCH to set api=True after creation.
30 tests across 8 test classes covering: - _parse_bool_flag edge cases - list, export, add, edit, import subcommands - dry-run preview display - api flag workaround (create + follow-up PATCH) - VALID_METRIC_TYPES constant
The server returns created/ignored items as dicts with name and message fields, not plain strings. Extract the name before joining for display.
Catch requests.HTTPError on create (409 → "already exists") and edit (404 → "not found") instead of showing raw HTTP errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Coverage Report
Changed file coverage
|
There was a problem hiding this comment.
My main comment is on general structure.
I feel like we should be running this through the project.py
So cli -> project -> interface -> api
Adds a little more but thats the general pattern I've been aiming for so far. I might adjust this going forward.
Also, would be good to have some of the logic in here being moved into the project.py. This file is basically just a forwarder to the correct endpoint and some error handling. Stuff like validating and reading the files should be in there
There was a problem hiding this comment.
Should this be in cli_test?
The secret-scanning gate failed on PR polyai#259 with a single line: Found verified Lob result 🐷🔑 No file, no line, no match. The trigger turned out to be a Python test function name, src/poly/tests/metrics_test.py:321: def test_add_duplicate_metric_friendly_error(...) Two independent problems. 1. The finding was noise. TruffleHog's Lob detector uses the pattern `\b((live|test)_[a-zA-Z0-9_]{35})\b` with no keyword-proximity requirement, and its Keywords() prefilter is literally {"live_", "test_"} — the pytest naming convention. So every chunk in the test suite reaches an ungated regex that matches any 40-character `test_*` identifier. It also counts HTTP 403 and 422 from api.lob.com as "verified", which is how a function name came to be reported as a verified secret. Scanning all of main's history with 3.96.0 returns 85 findings, every one of them Lob; excluding the detector takes that to zero. main already contains 36 more identifiers that match, so this would have recurred on any PR touching those lines. ADK does not use Lob, so --exclude-detectors=lob costs no coverage. The other 19 ungated detectors whose prefilter keywords appear in this repo (twilio, uri, redis, ftp, mongodb, launchdarkly, sendgrid, salesforce and friends) were checked against the source tree and have no matches and no near-misses. 2. The output was unactionable regardless. The action always passes --github-actions, whose printer emits only the detector name and verification status; file and line go into annotation metadata that never renders in the log, and the match is never printed at all. Since --json outranks --github-actions in TruffleHog's printer selection, a diagnostic step now re-runs the same scan on failure and reports file, line, commit, author and a 12-character match prefix — enough to triage, not enough to use. That step exits 0; the gate above has already failed the job. Also pins the scanner image. The action was SHA-pinned but its `version` input defaulted to `latest`, so the binary doing the scanning floated independently of the pin.
|
👋 The failing The scanner flagged a test function name, def test_add_duplicate_metric_friendly_error(self, mock_load, mock_create, mock_error):TruffleHog's Lob detector matches Nothing to change on your side — the name is fine, please don't rename it. #263 fixes this at the source (excludes the Lob detector, which we don't use, and makes the scanner actually print the file, line and match instead of a bare Sorry for the noise — the output gave you no way to tell a real leak from this. |
…#263) ## What happened The secret-scanning gate failed on #259 with exactly one line of output: ``` ##[warning]Found verified Lob result 🐷🔑 ``` No file, no line, no match. The actual trigger was a Python test function name — [`src/poly/tests/metrics_test.py:321`](https://github.com/polyai/adk/blob/main/src/poly/tests/metrics_test.py#L321): ```python def test_add_duplicate_metric_friendly_error(self, mock_load, mock_create, mock_error): ``` That's two separate problems, and this PR fixes both. ## 1. The finding was noise TruffleHog's Lob detector combines two individually-weak choices: ```go keyPat = regexp.MustCompile(`\b((live|test)_[a-zA-Z0-9_]{35})\b`) // no keyword-proximity gate func (s Scanner) Keywords() []string { return []string{"live_", "test_"} } ``` The `Keywords()` prefilter is the pytest naming convention, so **every chunk in our test suite** reaches an ungated regex that matches any 40-character `test_*` identifier. It also treats HTTP **403 and 422** from `api.lob.com` as "verified" — only 401 counts as invalid — which is how a function name got reported as a *verified* secret. Measured against this repo with TruffleHog 3.96.0 (`--no-verification`, all result types): | Scope | Findings | After `--exclude-detectors=lob` | |---|---|---| | Working tree | 51 (all Lob) | 0 | | Full `main` history | 85 (all Lob) | 0 | Lob is **100% of the finding surface**. `main` already contains 36 more identifiers that match the pattern — they only stay quiet because scans are limited to each PR's commit range, so this would have recurred on any PR touching those lines. Roughly 4% of test names in the repo land on exactly 40 characters. ADK does not use Lob (direct-mail API), so there is no coverage to lose. ### Other detectors were checked, not assumed Of 870 detectors, 133 lack a keyword-proximity gate and 74 have a prefilter keyword present in this repo. Intersecting gives 19 others that are live *and* ungated. All were tested against the source tree — **zero matches and zero near-misses**: ``` uri (user:pass@host) 0 twilio AC<32hex> 0 stripe pi_..._secret_ 0 redis:// creds 0 twilio SK<32> 0 zohocrm 1000.hex.hex 0 ftp:// creds 0 launchdarkly 0 sendgrid SG. 0 mongodb:// creds 0 salesforce 0 mailchimp <32hex>-usN 0 gemini master-/account- 0 closecrm api_<45> 0 lob (contrast) 43 ``` Two worth noting: **Twilio** (`AC`+32hex) is the one to watch given our telephony code — clean today, but a realistic-shaped dummy SID in a fixture will fire it. **Auth0** looks alarming (`\b(ey[a-zA-Z0-9._-]+)\b` with prefilter keywords `token`/`domain`) but `FromData` requires a 2000–5000 char match paired with an `*.auth0.com` domain, so it's well gated. ## 2. The output was unactionable regardless The action always passes `--github-actions`, and that printer emits only the detector name and verification status: ```go fmt.Printf("::warning file=%s,line=%d,endLine=%d::%s", out.Filename, out.StartLine, out.StartLine, message) // message = "Found verified Lob result 🐷🔑" ``` File and line go into annotation *metadata* that never renders in the log, and the match is never printed at all. `extra_args` can't remove `--github-actions`, but `--json` outranks it in TruffleHog's printer selection, so a diagnostic step re-runs the scan on failure: ``` ────────────────────────────────────────── detector: Lob [verified] file: src/poly/tests/metrics_test.py line: 321 commit: 0b99540 by Bill <bill@poly-ai.com> match: test_add_dup… (40 chars) ``` Only a 12-character prefix is printed — enough to triage, not enough to use. The step exits 0; the gate above has already failed the job. ## 3. Also: the scanner version was floating The action was SHA-pinned, but its `version` input defaulted to `latest` — the failing run logs `VERSION: latest`. The pin covered the wrapper, not the binary doing the scanning. Now pinned to `v3.96.0`. ## Testing - YAML validated. - `jq` filter tested against a real TruffleHog JSON result record. - Exclusion confirmed to take both the working tree and full `main` history to zero findings. - Diagnostic step mirrors the gate's `--exclude-detectors` so the two can't disagree. ## Note for reviewers This repo is public, and the description above documents which detector is disabled and that `--results=verified,unknown` filters out unverifiable detectors (`jwt`, `uri`, `sqlserver`). It's all derived from TruffleHog's public source. Happy to trim if anyone would rather that analysis live internally.
Summary
Add
poly metricsCLI commands for managing custom metrics in Agent Studio projects:Motivation
Custom metrics are currently only manageable through the Agent Studio UI. This adds full CLI support so teams can script metric setup, export/import metric definitions across projects, and integrate metric management into CI pipelines.
Changes
Commands
poly metrics list— display all custom metrics in a Rich table (Name, Type, Active, API, Description) with active/inactive count summary;--jsonfor machine-readable outputpoly metrics export [file]— export all metrics as YAML to stdout or a file;--jsonoutputs JSON insteadpoly metrics add— create a new metric with--name,--type,--description,--api,--expected-values; omit required flags for interactive mode (name text input, type selector, description prompt, API confirm viaquestionary)poly metrics edit <name>— update an existing metric's--description,--api(bool),--active(bool), or--expected-values;--api/--activeaccepttrue/falseor can be used bare to settrue. When called with just a metric name and no flags, enters interactive mode: fetches the metric, displays current values, and prompts withquestionary.checkbox()to select which fields to update.--expected-valuesis only offered for string-type metrics. Friendly error when metric not found (404).poly metrics import <file>— bulk-import metrics from a YAML file; creates new metrics and skips existing ones;--dry-runpreviews what would be created/skipped and warns about remote-only metrics that won't be deletedAPI flag workaround
The server's
create_custom_metricroute hardcodesapi=False, ignoring the client-supplied value (the AS UI also doesn't set this on create). When--apiis passed, under the hood the the CLI follows up with a PATCH to setapi=True.Implementation notes
PlatformAPIHandler;make_requestextended withfiles(multipart upload) andresponse_format(YAML parsing) params . These were needed for import and export YAML as file handling only previously supported JSON.import/export flow was chosen here as the implementation details of metrics require that metrics
have project scope (not branch scope) and cannot be deleted once written.
In this case, using a local file to manage additions and changes could give way to merge issues
and user confusion where deleted items are not actually removed.
For this reason I have opted to model this feature on the platform features,
with one exception that API can be set on metric creation.
Test strategy
poly metricsfor all subcommands)Manual test spreadsheet: Google Sheet
Checklist
ruff check .andruff format --check .passpytestpassespolyCLI interface (or migration path documented)Screenshots / Logs
poly metrics listpoly metrics add(interactive)poly metrics add— API flag promptpoly metrics add --expected-values(JSON output)poly metrics add --api(JSON output)