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
12 changes: 6 additions & 6 deletions docs/agents/insight-driven-optimization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ dependencies:

Both halves of the loop read a shared per-agent profile, `optimizer.yaml`,
discovered by walking up from the current directory. The Analyst consumes only
its analysis subset (`agent`, `agent_spec`, `workspace`); the Experimenter
its analysis subset (`agent`, `ethos`, `workspace`); the Experimenter
validates the full schema:

```yaml
agent: research-agent
agent_spec: AGENT-SPEC.md # optional; falls back to AGENT-SPEC.md, then README.md
ethos: ETHOS.md # optional; falls back to ETHOS.md, then README.md
workspace: default # optional; defaults to "default"
agent_source: . # local dir or git URL with optional @ref
task_template: ./task-template
Expand Down Expand Up @@ -214,7 +214,7 @@ Its method is to survey sessions broadly, gather evidence (starting from
negative feedback and error spans), cluster similar failures across many
sessions, check for existing Insights, then emit a single result containing new
Insights and evidence appended to existing ones. Give the Analyst an optional
agent spec (`--agent-spec AGENT-SPEC.md`) so it can flag divergence from
Ethos (`--ethos ETHOS.md`) so it can flag divergence from
intended behavior.

### Experimenter
Expand Down Expand Up @@ -354,7 +354,7 @@ nemo agents analyst run \

Useful flags:

- `--agent-spec AGENT-SPEC.md`: Append a spec so the Analyst can flag
- `--ethos ETHOS.md`: Append Ethos so the Analyst can flag
divergence from intended behavior.
- `--insights-file-output tmp/insights.yaml`: Read and write Insights from a
specific local YAML file. Trace reads still hit `--base-url`.
Expand Down Expand Up @@ -541,7 +541,7 @@ Run the Analyst for one analysis of an agent's traces.
| Flag | Required | Default | Description |
|------|----------|---------|-------------|
| `--agent` | yes, unless a profile supplies it | profile `agent` | Agent under test the Analyst should focus on. |
| `--agent-spec` | no | profile `agent_spec`, else `AGENT-SPEC.md` or `README.md` beside the profile | Path to a Markdown spec for the AUT. |
| `--ethos` | no | profile `ethos`, else `ETHOS.md` or `README.md` beside the profile | Path to Ethos Markdown for the AUT. |
| `--workspace` | no | profile `workspace`, else `default` | Workspace to operate in. |
| `--base-url` | no | `NMP_BASE_URL`, else `http://localhost:8080` | Running platform instance the Analyst's tools call. |
| `--profile` | no | discovered by walking up from the current working directory | Path to `optimizer.yaml`. |
Expand Down Expand Up @@ -570,7 +570,7 @@ Run the local Experimenter loop.
| `--insight-id` | no | Not applicable | Select an exact ID, exact title, or zero-based index from a local multi-Insight file. |
| `--no-insight` | no | off | Run against a dataset directly rather than guided by an Insight. |
| `--agent` | no | profile `agent_source` | Baseline agent override: a local directory or a git URL with optional ref (`...repo.git@main`). A git source records provenance and enables opening a draft PR for the winner. |
| `--agent-spec` | no | profile `agent_spec` | Location of a Markdown file describing the AUT. |
| `--ethos` | no | profile `ethos` | Location of a Markdown file describing the AUT. |
| `--train-dataset` | yes, unless the profile supplies it | profile `datasets.train` | Train dataset. Local path for the Harbor evaluator. |
| `--validation-dataset` | yes, unless the profile supplies it | profile `datasets.validation` | Validation dataset. Local path for the Harbor evaluator. |
| `--task-template` | required with an Insight | profile `task_template` | Evaluator-specific task-template URI, used to build the Insight-specific evaluation suite. |
Expand Down
10 changes: 5 additions & 5 deletions plugins/nemo-insights/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,20 @@ The profile contract consumed by Insights is deliberately small:

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

Only `agent`, `agent_spec`, and `workspace` are consumed by Insights.
Only `agent`, `ethos`, 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
field is rejected. `agent` is required. Relative `ethos` paths are
resolved relative to the profile. When it is omitted, Insights looks for
`AGENT-SPEC.md`, then `README.md`, beside the profile.
`ETHOS.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`,
explicit command-line flags, then profile values (for `agent`, `ethos`,
and `workspace`) or `NMP_BASE_URL` (for the base URL), then the built-in
defaults. `--base-url` takes precedence over `NMP_BASE_URL`.

Expand Down
4 changes: 2 additions & 2 deletions plugins/nemo-insights/evaluation/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ async def analyze(
client = self._basic_auth_client() if cfg.get("auth") == "basic" else make_client(str(cfg["base_url"]))
return await run_analyst(
agent=cfg["agent"],
agent_spec=None,
ethos=None,
workspace=cfg["workspace"],
base_url=cfg["base_url"],
client=client,
Expand Down Expand Up @@ -297,7 +297,7 @@ async def analyze(
)
return await run_analyst(
agent=str(record["agent"]),
agent_spec=policy,
ethos=policy,
workspace=workspace,
base_url=str(record["base_url"]),
client=make_client(str(record["base_url"])),
Expand Down
4 changes: 2 additions & 2 deletions plugins/nemo-insights/evaluation/tau2run.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,12 @@ def load_tasks(data_dir: Path, domain: str) -> dict[str, dict]:


def read_policy(data_dir: Path, domain: str) -> str | None:
"""Return the domain policy markdown (the analyst's agent_spec), or None.
"""Return the domain policy markdown (the analyst's Ethos), or None.

A tau2 checkout nests domains under ``tau2/domains/<domain>/``; some data
dirs are flat (``domains/<domain>/``). For each layout, tries ``policy.md``
then ``main_policy.md`` (Telecom uses the latter). Returns the first file
found, else ``None`` (the analyst then runs without the spec).
found, else ``None`` (the analyst then runs without Ethos).
"""
for base in (data_dir / "tau2" / "domains", data_dir / "domains"):
domain_dir = base / domain
Expand Down
22 changes: 11 additions & 11 deletions plugins/nemo-insights/src/nemo_insights_plugin/analyst/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
The result is delivered through Nooa's ``return_result`` helper and validated
against the ``AnalystResult`` schema.

The analyst's persona, task, the agent-under-test name, and the optional AUT
spec are all formatted into the instructions by ``build_analyst_agent``; the
The analyst's persona, task, agent-under-test name, and optional Ethos content
are all formatted into the instructions by ``build_analyst_agent``; the
run is seeded with only the minimal ``KICKOFF`` request. The per-run config the
methods need is carried in :class:`~nemo_insights_plugin.analyst.deps.AnalystDeps`.
Workspace and base URL aren't in the instructions because the methods are already
Expand Down Expand Up @@ -50,7 +50,7 @@
#
# This is the analyst context prompt and the only long-form prompt
# the analyst gets — there is no separate user-message brief. ``{agent}`` is
# formatted in by ``build_analyst_agent`` and the optional AUT spec is appended
# formatted in by ``build_analyst_agent`` and optional Ethos content is appended
# as the final paragraph. Nooa owns the CodeAct protocol and method catalog, so
# this text covers only the analyst's persona,
# principles, and method — it deliberately does not document the tools or
Expand Down Expand Up @@ -146,12 +146,12 @@
"""


AGENT_SPEC_HEADER = """
## Agent Spec
ETHOS_HEADER = """
## Ethos

Use this as the contract for what the agent is supposed to do, what
success looks like, and what behavior should be flagged as divergence.
Flag agent divergence from the spec. The spec was authored by the
Flag agent divergence from the Ethos. The Ethos was authored by the
developer of the application and should be considered the purpose and goals.
"""

Expand All @@ -170,7 +170,7 @@ def __init__(
*,
deps: AnalystDeps,
agent: str,
agent_spec: str | None = None,
ethos: str | None = None,
**kwargs: Any,
) -> None:
super().__init__(llm=kwargs.pop("llm", None) or get_default_model(), **kwargs)
Expand All @@ -183,8 +183,8 @@ def __init__(
)

instructions = INSTRUCTIONS.format(agent=agent)
if agent_spec and agent_spec.strip():
instructions = f"{instructions}\n{AGENT_SPEC_HEADER}\n\n{agent_spec.strip()}\n"
if ethos and ethos.strip():
instructions = f"{instructions}\n{ETHOS_HEADER}\n\n{ethos.strip()}\n"
self.context["analyst_instructions"] = instructions

async def fetch_spans(
Expand Down Expand Up @@ -360,15 +360,15 @@ def build_analyst_agent(
*,
deps: AnalystDeps,
agent: str,
agent_spec: str | None = None,
ethos: str | None = None,
llm: UnifiedLLM | None = None,
**kwargs: Any,
) -> Analyst:
"""Build the analyst with per-run scope and optional Nooa runtime overrides."""
return Analyst(
deps=deps,
agent=agent,
agent_spec=agent_spec,
ethos=ethos,
llm=llm,
**kwargs,
)
6 changes: 3 additions & 3 deletions plugins/nemo-insights/src/nemo_insights_plugin/analyst/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ClientConstructionError(Exception):
async def run_analyst(
*,
agent: str,
agent_spec: str | None,
ethos: str | None,
workspace: str,
base_url: str | None,
client: AsyncNeMoPlatform,
Expand All @@ -60,7 +60,7 @@ async def run_analyst(

Args:
agent: Agent under test.
agent_spec: Optional markdown spec content for the agent under test.
ethos: Optional Ethos Markdown for the agent under test.
workspace: Platform workspace.
base_url: Platform base URL. ``None`` uses the active platform context.
client: Platform client to use. This function closes it before returning.
Expand Down Expand Up @@ -104,7 +104,7 @@ async def run_analyst(
analyst = build_analyst_agent(
deps=deps,
agent=agent,
agent_spec=agent_spec,
ethos=ethos,
)
result = await _run_agent(analyst, verbose=verbose)
return await backend.persist_result(workspace=workspace, agent=agent, result=result)
Expand Down
54 changes: 27 additions & 27 deletions plugins/nemo-insights/src/nemo_insights_plugin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@
check_environment,
check_models,
check_profile,
read_agent_spec,
read_ethos,
)
from nemo_insights_plugin.profile import AnalysisProfile, load_profile, pick_agent_spec
from nemo_insights_plugin.profile import AnalysisProfile, load_profile, pick_ethos
from nemo_platform import NeMoPlatformError
from nemo_platform_plugin.cli import NemoCLI
from nemo_platform_plugin.nooa_model_client import configured_model_refs
Expand All @@ -51,12 +51,12 @@
@dataclass(frozen=True)
class _ResolvedAnalysis:
agent: str
agent_spec: str | None
ethos: str | None
workspace: str
base_url: str
insights_output: Path | None
profile_dir: Path | None
spec_checks: tuple[CheckResult, ...]
ethos_checks: tuple[CheckResult, ...]


def _load_profile_or_error(profile_path: Path | None) -> tuple[AnalysisProfile | None, str | None]:
Expand Down Expand Up @@ -99,7 +99,7 @@ def _one_line_error(exc: BaseException) -> str:
def _resolve_analysis(
*,
agent: str | None,
agent_spec: Path | None,
ethos: Path | None,
workspace: str | None,
base_url: str | None,
profile_path: Path | None,
Expand All @@ -121,26 +121,26 @@ def _resolve_analysis(
else:
resolved_workspace = profile.workspace if profile is not None else DEFAULT_WORKSPACE

spec_path = agent_spec
spec_error: str | None = None
if spec_path is None and profile is not None:
ethos_path = ethos
ethos_error: str | None = None
if ethos_path is None and profile is not None:
try:
spec_path = pick_agent_spec(profile)
ethos_path = pick_ethos(profile)
except ProfileError as exc:
spec_error = str(exc)
spec_content, spec_checks = read_agent_spec(spec_path, spec_error)
ethos_error = str(exc)
ethos_content, ethos_checks = read_ethos(ethos_path, ethos_error)

resolved_base_url = resolve_base_url(base_url)
validate_insights_file(insights_output)

return _ResolvedAnalysis(
agent=resolved_agent,
agent_spec=spec_content,
ethos=ethos_content,
workspace=resolved_workspace,
base_url=resolved_base_url,
insights_output=insights_output,
profile_dir=profile.profile_dir if profile is not None else None,
spec_checks=tuple(spec_checks),
ethos_checks=tuple(ethos_checks),
)


Expand Down Expand Up @@ -168,7 +168,7 @@ def _prepare_mirror(insights_output: Path | None) -> Path | None:


async def _run_analysis(analysis: _ResolvedAnalysis, *, verbose: bool) -> str:
checks = list(analysis.spec_checks)
checks = list(analysis.ethos_checks)
checks.extend(check_models())
_preflight_or_exit(checks)

Expand All @@ -180,7 +180,7 @@ async def _run_analysis(analysis: _ResolvedAnalysis, *, verbose: bool) -> str:
raise ClientConstructionError(str(exc)) from None
return await run_analyst(
agent=analysis.agent,
agent_spec=analysis.agent_spec,
ethos=analysis.ethos,
workspace=analysis.workspace,
base_url=analysis.base_url,
client=client,
Expand Down Expand Up @@ -212,10 +212,10 @@ def analyze(
"--agent",
help="Name of the agent (agent under test) the analyst should focus on.",
),
agent_spec: Path | None = typer.Option(
ethos: Path | None = typer.Option(
None,
"--agent-spec",
help="Path to a markdown file describing the agent under test (its spec).",
"--ethos",
help="Path to a markdown file describing the agent under test (its Ethos).",
exists=True,
readable=True,
),
Expand Down Expand Up @@ -259,16 +259,16 @@ def analyze(
) -> None:
"""Run the analyst agent against a running NMP instance.

Builds the analyst agent with ``--agent`` (and optional
``--agent-spec``) formatted into its instructions and tools scoped
Builds the analyst agent with ``--agent`` (and optional ``--ethos``)
formatted into its instructions and tools scoped
to ``--agent`` / ``--workspace`` / ``--base-url``, runs it, and
prints whatever the agent returns. Insights are written to the
platform, and mirrored to ``--insights-file-output`` when given.
"""
try:
analysis = _resolve_analysis(
agent=agent,
agent_spec=agent_spec,
ethos=ethos,
workspace=workspace,
base_url=base_url,
profile_path=profile_path,
Expand Down Expand Up @@ -302,18 +302,18 @@ def doctor(
profile, profile_error = _load_profile_or_error(profile_path)
except ProfileError as exc:
profile, profile_error = None, str(exc)
spec_path: Path | None = None
spec_error: str | None = None
ethos_path: Path | None = None
ethos_error: str | None = None
if profile is not None:
try:
spec_path = pick_agent_spec(profile)
ethos_path = pick_ethos(profile)
except ProfileError as exc:
spec_error = str(exc)
_, spec_results = read_agent_spec(spec_path, spec_error)
ethos_error = str(exc)
_, ethos_results = read_ethos(ethos_path, ethos_error)

async def _flow() -> list[CheckResult]:
results = check_profile(profile, profile_error)
results.extend(spec_results)
results.extend(ethos_results)
results.extend(
await check_environment(
agent=profile.agent if profile is not None else None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

PROFILE_FILENAME = "optimizer.yaml"
DEFAULT_BASE_URL = "http://localhost:8080"
_AGENT_SPEC_FILENAMES = ("AGENT-SPEC.md", "README.md")
_ETHOS_FILENAMES = ("ETHOS.md", "README.md")

ProfileModel = TypeVar("ProfileModel", bound=BaseModel)

Expand Down Expand Up @@ -92,14 +92,14 @@ def load_env_file(path: Path, env: MutableMapping[str, str] = os.environ) -> lis
return loaded


def resolve_agent_spec_path(profile_dir: Path, configured: str | None) -> Path | None:
"""Resolve a configured agent spec or the conventional profile-local file."""
def resolve_ethos_path(profile_dir: Path, configured: str | None) -> Path | None:
"""Resolve a configured Ethos or the conventional profile-local file."""
if configured is not None:
path = resolve_profile_path(configured, profile_dir)
if not path.is_file():
raise ProfileError(f"Profile agent_spec {configured!r} does not exist (resolved to {path})")
raise ProfileError(f"Profile ethos {configured!r} does not exist (resolved to {path})")
return path
for name in _AGENT_SPEC_FILENAMES:
for name in _ETHOS_FILENAMES:
candidate = profile_dir / name
if candidate.is_file():
return candidate
Expand Down
Loading
Loading