From 328f91812cea5fc31932c29ff662ae37bfa157fc Mon Sep 17 00:00:00 2001 From: Paulo Lacerda Date: Thu, 4 Jun 2026 11:12:11 -0300 Subject: [PATCH 1/2] fix: handle blank init wizard values gracefully (#232) agentops init could continue after required wizard values were left blank, then fail later while persisting agentops.yaml. In clean installs that path also exposed an undeclared PyYAML import and printed an ugly traceback (No module named 'yaml'). Re-prompt required blank values when no default exists, validate blank scripted flags with a friendly message, catch final persistence failures without traceback, and use the repository's ruamel.yaml helpers for agentops.yaml read/write. Add focused init and setup wizard coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 16 ++++++ src/agentops/cli/app.py | 36 +++++++++++-- src/agentops/services/setup_wizard.py | 38 ++++++++----- tests/unit/test_init_command.py | 14 ++++- tests/unit/test_setup_wizard.py | 78 +++++++++++++++++++++++++-- 5 files changed, 158 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85d2b25e..44e0d690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres ## [Unreleased] +### Fixed +- **`agentops init` now handles blank required wizard values gracefully.** If + the user presses Enter without an existing Foundry endpoint or agent default, + the wizard explains that AgentOps needs the missing value and re-prompts + instead of proceeding to a later persistence failure. Scripted blank flags + such as `--agent ""` now exit with the same friendly message and no traceback. +- **`agentops init` no longer depends on undeclared PyYAML.** The setup wizard + now reads and writes `agentops.yaml` through the repository's `ruamel.yaml` + helpers, fixing the ugly `No module named 'yaml'` traceback seen in clean + installs. + +### Changed +- **AgentOps brand tagline sequence now reads `Evaluate :: Ship :: Observe :: + Own`.** The startup/explain banner now matches the intended product story + order. + ## [0.3.7] - 2026-06-01 ### Fixed diff --git a/src/agentops/cli/app.py b/src/agentops/cli/app.py index dafdc3e3..23da7de4 100644 --- a/src/agentops/cli/app.py +++ b/src/agentops/cli/app.py @@ -1365,6 +1365,7 @@ def cmd_init( AGENT_TITLE, DATASET_TITLE, PROJECT_ENDPOINT_TITLE, + REQUIRED_CONFIGURATION_MESSAGE, WizardAnswers, apply_answers, discover_defaults, @@ -1476,16 +1477,37 @@ def cmd_init( if any_flag: # Scripted mode — validate then apply. if project_endpoint is not None: + if not project_endpoint.strip(): + typer.echo( + f"{_cli_error('Error')}: --project-endpoint is required. " + f"{REQUIRED_CONFIGURATION_MESSAGE}", + err=True, + ) + raise typer.Exit(code=1) err = validate_project_endpoint(project_endpoint) if err: typer.echo(f"{_cli_error('Error')}: --project-endpoint: {err}", err=True) raise typer.Exit(code=1) if agent is not None: + if not agent.strip(): + typer.echo( + f"{_cli_error('Error')}: --agent is required. " + f"{REQUIRED_CONFIGURATION_MESSAGE}", + err=True, + ) + raise typer.Exit(code=1) err = validate_agent(agent) if err: typer.echo(f"{_cli_error('Error')}: --agent: {err}", err=True) raise typer.Exit(code=1) if dataset is not None: + if not dataset.strip(): + typer.echo( + f"{_cli_error('Error')}: --dataset is required. " + f"{REQUIRED_CONFIGURATION_MESSAGE}", + err=True, + ) + raise typer.Exit(code=1) err = validate_dataset(dataset, workspace) if err: typer.echo(f"{_cli_error('Error')}: --dataset: {err}", err=True) @@ -1605,8 +1627,14 @@ def _wizard_echo(msg: str) -> None: default_env_name=target_env_name, azd_env_name=azd_env_name, ) - except RuntimeError as exc: - typer.echo(f"{_cli_error('Error')}: {exc}", err=True) + except Exception as exc: # noqa: BLE001 + typer.echo( + f"{_cli_error('Error')}: could not save AgentOps configuration. " + f"{REQUIRED_CONFIGURATION_MESSAGE}", + err=True, + ) + if str(exc): + typer.echo(f"{_cli_warn('Details')}: {exc}", err=True) raise typer.Exit(code=1) typer.echo("") @@ -4362,8 +4390,8 @@ def _colorize_block( # fallback keeps the same words on a single line. def _agentops_tagline() -> str: if _terminal_unicode_enabled(): - return "Evaluate · Observe · Diagnose · Ship — every Foundry agent." - return "Evaluate :: Observe :: Diagnose :: Ship -- every Foundry agent." + return "Evaluate · Ship · Observe · Own — every Foundry agent." + return "Evaluate :: Ship :: Observe :: Own -- every Foundry agent." def _render_brand_block( diff --git a/src/agentops/services/setup_wizard.py b/src/agentops/services/setup_wizard.py index 3979d4d6..5e0615a2 100644 --- a/src/agentops/services/setup_wizard.py +++ b/src/agentops/services/setup_wizard.py @@ -67,6 +67,12 @@ ENV_KEY_PROJECT_ENDPOINT = "AZURE_AI_FOUNDRY_PROJECT_ENDPOINT" ENV_KEY_APPINSIGHTS = "APPLICATIONINSIGHTS_CONNECTION_STRING" +REQUIRED_CONFIGURATION_MESSAGE = ( + "AgentOps needs a Foundry project endpoint, an agent, and a dataset path " + "before it can finish configuration. Enter the missing value, or press " + "Ctrl+C to cancel and re-run `agentops init` later." +) + # --------------------------------------------------------------------------- # Data models @@ -356,18 +362,16 @@ def _read_agentops_yaml(workspace: Path) -> dict: if not path.exists(): return {} try: - import yaml # noqa: PLC0415 - except Exception: # noqa: BLE001 - return {} - try: - data = yaml.safe_load(path.read_text(encoding="utf-8")) + from agentops.utils.yaml import load_yaml # noqa: PLC0415 + + data = load_yaml(path) except Exception: # noqa: BLE001 return {} return data if isinstance(data, dict) else {} def _write_agentops_yaml(path: Path, data: dict) -> None: - import yaml # noqa: PLC0415 + from agentops.utils.yaml import save_yaml # noqa: PLC0415 path.parent.mkdir(parents=True, exist_ok=True) # Preserve simple field order for readability: version, agent, dataset, @@ -381,10 +385,7 @@ def _write_agentops_yaml(path: Path, data: dict) -> None: for key, value in data.items(): if key not in ordered: ordered[key] = value - path.write_text( - yaml.safe_dump(ordered, sort_keys=False, default_flow_style=False), - encoding="utf-8", - ) + save_yaml(path, ordered) _AGENTOPS_ENV_HEADER = ( @@ -601,7 +602,11 @@ def _confirm_existing(label: str, value: str, secret: bool = False) -> None: raw = prompt("Foundry project endpoint", effective_endpoint_default) value = raw.strip() if not value: - break # keep current / leave blank + if effective_endpoint_default: + break # keep current + echo(" ! Foundry project endpoint is required.") + echo(" ! " + REQUIRED_CONFIGURATION_MESSAGE) + continue err = validate_project_endpoint(value) if err: echo(" ! " + err) @@ -623,7 +628,11 @@ def _confirm_existing(label: str, value: str, secret: bool = False) -> None: raw = prompt("Agent", defaults.agent) value = raw.strip() if not value: - break + if defaults.agent: + break # keep current + echo(" ! Agent is required.") + echo(" ! " + REQUIRED_CONFIGURATION_MESSAGE) + continue err = validate_agent(value) if err: echo(" ! " + err) @@ -641,11 +650,12 @@ def _confirm_existing(label: str, value: str, secret: bool = False) -> None: echo("") echo(DATASET_TITLE) echo(_indent(DATASET_HELP)) + dataset_default = defaults.dataset or ".agentops/data/smoke.jsonl" while True: - raw = prompt("Dataset path", defaults.dataset or ".agentops/data/smoke.jsonl") + raw = prompt("Dataset path", dataset_default) value = raw.strip() if not value: - break + value = dataset_default err = validate_dataset(value, workspace) if err: echo(" ! " + err) diff --git a/tests/unit/test_init_command.py b/tests/unit/test_init_command.py index f8ae31d7..1320983b 100644 --- a/tests/unit/test_init_command.py +++ b/tests/unit/test_init_command.py @@ -119,6 +119,16 @@ def test_init_scripted_validates_project_endpoint(tmp_path: Path) -> None: assert "Project endpoint" in result.output +def test_init_scripted_blank_required_value_is_friendly(tmp_path: Path) -> None: + """Blank scripted values fail with a concise message, not a traceback.""" + result = runner.invoke(app, ["init", "--dir", str(tmp_path), "--agent", ""]) + + assert result.exit_code == 1 + assert "--agent is required" in result.output + assert "AgentOps needs a Foundry project endpoint" in result.output + assert "Traceback" not in result.output + + def test_init_scripted_with_custom_azd_env(tmp_path: Path) -> None: """`--azd-env qa` writes to the explicit azd env instead of the local env.""" runner.invoke(app, ["init", "--dir", str(tmp_path), "--no-prompt"]) @@ -293,7 +303,7 @@ def test_init_prints_brand_banner(tmp_path: Path, monkeypatch) -> None: # ASCII letterforms from _AGENTOPS_PLAIN_BANNER (figlet "Standard"). assert "____ _____ _ _ _____" in text # The catchphrase, ASCII fallback variant. - assert "Evaluate :: Observe :: Diagnose :: Ship -- every Foundry agent." in text + assert "Evaluate :: Ship :: Observe :: Own -- every Foundry agent." in text def test_brand_tagline_is_used_by_explain_pages(monkeypatch) -> None: @@ -305,7 +315,7 @@ def test_brand_tagline_is_used_by_explain_pages(monkeypatch) -> None: result = runner.invoke(app, ["init", "explain", "--no-pager"]) assert result.exit_code == 0 text = _strip_ansi(result.stdout) - assert "Evaluate :: Observe :: Diagnose :: Ship -- every Foundry agent." in text + assert "Evaluate :: Ship :: Observe :: Own -- every Foundry agent." in text # --------------------------------------------------------------------------- diff --git a/tests/unit/test_setup_wizard.py b/tests/unit/test_setup_wizard.py index 5353a50f..9cc3ff22 100644 --- a/tests/unit/test_setup_wizard.py +++ b/tests/unit/test_setup_wizard.py @@ -4,6 +4,7 @@ import json import os +import builtins from pathlib import Path import pytest @@ -242,6 +243,29 @@ def test_apply_answers_writes_agent_and_dataset_to_yaml(tmp_path: Path): assert "dataset: .agentops/data/smoke.jsonl" in text +def test_apply_answers_writes_yaml_without_pyyaml( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + real_import = builtins.__import__ + + def _import(name, *args, **kwargs): # noqa: ANN001 + if name == "yaml": + raise ModuleNotFoundError("No module named 'yaml'") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", _import) + + result = apply_answers( + tmp_path, + WizardAnswers(agent="my-bot:7", dataset=".agentops/data/smoke.jsonl"), + ) + + assert result.yaml_updated is True + assert "agent: my-bot:7" in (tmp_path / "agentops.yaml").read_text( + encoding="utf-8" + ) + + def test_apply_answers_does_not_write_project_endpoint_to_yaml(tmp_path: Path): """Environment-specific endpoints stay out of agentops.yaml.""" answers = WizardAnswers( @@ -465,7 +489,7 @@ def test_run_wizard_does_not_prompt_for_appinsights( assert "Application Insights" not in "\n".join(messages) -def test_run_wizard_empty_input_keeps_current( +def test_run_wizard_empty_input_reprompts_required_missing_endpoint( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ): monkeypatch.delenv("AZURE_AI_FOUNDRY_PROJECT_ENDPOINT", raising=False) @@ -479,14 +503,46 @@ def test_run_wizard_empty_input_keeps_current( # With idempotent skip-on-default, agent/dataset are silently reused. # Only the still-empty project endpoint gets asked; App Insights is left # for runtime discovery or explicit non-interactive configuration. - prompt = _scripted_prompt([""]) + prompt = _scripted_prompt(["", "https://acct.services.ai.azure.com/api/projects/p"]) + messages: list[str] = [] answers = run_wizard( - tmp_path, prompt=prompt, echo=lambda _msg: None, reconfigure=False + tmp_path, prompt=prompt, echo=messages.append, reconfigure=False ) - assert answers.project_endpoint is None + assert answers.project_endpoint == "https://acct.services.ai.azure.com/api/projects/p" assert answers.agent is None assert answers.dataset is None assert answers.appinsights_connection_string is None + assert "Foundry project endpoint is required." in "\n".join(messages) + + +def test_run_wizard_reprompts_blank_required_values( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +): + monkeypatch.delenv("AZURE_AI_FOUNDRY_PROJECT_ENDPOINT", raising=False) + monkeypatch.delenv("APPLICATIONINSIGHTS_CONNECTION_STRING", raising=False) + + data_dir = tmp_path / ".agentops" / "data" + data_dir.mkdir(parents=True) + (data_dir / "smoke.jsonl").write_text("{}\n", encoding="utf-8") + prompt = _scripted_prompt( + [ + "", + "https://acct.services.ai.azure.com/api/projects/p", + "", + "my-bot:9", + "", + ] + ) + messages: list[str] = [] + + answers = run_wizard(tmp_path, prompt=prompt, echo=messages.append) + + assert answers.project_endpoint == "https://acct.services.ai.azure.com/api/projects/p" + assert answers.agent == "my-bot:9" + assert answers.dataset == ".agentops/data/smoke.jsonl" + output = "\n".join(messages) + assert "Foundry project endpoint is required." in output + assert "Agent is required." in output def test_run_wizard_force_prompt_fields_reasks_seed_agent_and_dataset( @@ -543,6 +599,20 @@ def test_run_wizard_appinsights_is_not_interactive_even_when_missing( """The wizard should not ask for App Insights just to leave it blank.""" monkeypatch.delenv("AZURE_AI_FOUNDRY_PROJECT_ENDPOINT", raising=False) monkeypatch.delenv("APPLICATIONINSIGHTS_CONNECTION_STRING", raising=False) + _seed_azd_env( + tmp_path, + "dev", + { + "AZURE_AI_FOUNDRY_PROJECT_ENDPOINT": ( + "https://acct.services.ai.azure.com/api/projects/p" + ), + }, + ) + (tmp_path / "agentops.yaml").write_text( + "version: 1\nagent: keep:1\ndataset: keep.jsonl\n", + encoding="utf-8", + ) + (tmp_path / "keep.jsonl").write_text("{}\n", encoding="utf-8") messages: list[str] = [] prompt_calls: list[str] = [] From d37d8c9ed2e69b6a18134701bdd76566f013c25e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:14:06 +0000 Subject: [PATCH 2/2] chore: prepare release 0.3.8 --- .claude-plugin/marketplace.json | 2 +- .github/plugin/marketplace.json | 2 +- CHANGELOG.md | 2 ++ plugins/agentops/package.json | 2 +- plugins/agentops/plugin.json | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8cb848a7..867782d1 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "agentops-accelerator", "source": "../../plugins/agentops", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Toolkit and Microsoft Foundry agents.", - "version": "0.3.7", + "version": "0.3.8", "keywords": [ "agentops", "evaluation", diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 8cb848a7..867782d1 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -13,7 +13,7 @@ "name": "agentops-accelerator", "source": "../../plugins/agentops", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Toolkit and Microsoft Foundry agents.", - "version": "0.3.7", + "version": "0.3.8", "keywords": [ "agentops", "evaluation", diff --git a/CHANGELOG.md b/CHANGELOG.md index 44e0d690..11c5aab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ This format follows [Keep a Changelog](https://keepachangelog.com/) and adheres ## [Unreleased] +## [0.3.8] - 2026-06-04 + ### Fixed - **`agentops init` now handles blank required wizard values gracefully.** If the user presses Enter without an existing Foundry endpoint or agent default, diff --git a/plugins/agentops/package.json b/plugins/agentops/package.json index 94cd453d..3357c6b2 100644 --- a/plugins/agentops/package.json +++ b/plugins/agentops/package.json @@ -2,7 +2,7 @@ "name": "agentops-accelerator", "displayName": "AgentOps Accelerator — Skills for GitHub Copilot", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Accelerator and Microsoft Foundry agents.", - "version": "0.3.7", + "version": "0.3.8", "publisher": "AgentOpsAccelerator", "icon": "icon.png", "license": "MIT", diff --git a/plugins/agentops/plugin.json b/plugins/agentops/plugin.json index 96548523..4626d03f 100644 --- a/plugins/agentops/plugin.json +++ b/plugins/agentops/plugin.json @@ -1,7 +1,7 @@ { "name": "agentops-accelerator", "description": "Copilot agent skills for running standardized evaluation workflows with AgentOps Accelerator and Microsoft Foundry agents.", - "version": "0.3.7", + "version": "0.3.8", "author": { "name": "AgentOps Accelerator", "url": "https://github.com/Azure/agentops"